This commit is contained in:
Franck Nijhof 2024-08-16 18:43:41 +02:00 committed by GitHub
commit 94516de724
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 993 additions and 304 deletions

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aemet", "documentation": "https://www.home-assistant.io/integrations/aemet",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aemet_opendata"], "loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.5.3"] "requirements": ["AEMET-OpenData==0.5.4"]
} }

View File

@ -114,7 +114,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
) )
try: try:
await airzone.get_version() await airzone.get_version()
except AirzoneError as err: except (AirzoneError, TimeoutError) as err:
raise AbortFlow("cannot_connect") from err raise AbortFlow("cannot_connect") from err
return await self.async_step_discovered_connection() return await self.async_step_discovered_connection()

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"], "loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.1"] "requirements": ["aioairzone-cloud==0.6.2"]
} }

View File

@ -244,7 +244,6 @@ class BluesoundPlayer(MediaPlayerEntity):
self._status: Status | None = None self._status: Status | None = None
self._inputs: list[Input] = [] self._inputs: list[Input] = []
self._presets: list[Preset] = [] self._presets: list[Preset] = []
self._is_online = False
self._muted = False self._muted = False
self._master: BluesoundPlayer | None = None self._master: BluesoundPlayer | None = None
self._is_master = False self._is_master = False
@ -312,20 +311,24 @@ class BluesoundPlayer(MediaPlayerEntity):
async def _start_poll_command(self): async def _start_poll_command(self):
"""Loop which polls the status of the player.""" """Loop which polls the status of the player."""
try:
while True: while True:
try:
await self.async_update_status() await self.async_update_status()
except (TimeoutError, ClientError): except (TimeoutError, ClientError):
_LOGGER.error("Node %s:%s is offline, retrying later", self.host, self.port) _LOGGER.error(
"Node %s:%s is offline, retrying later", self.host, self.port
)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
self.start_polling()
except CancelledError: except CancelledError:
_LOGGER.debug("Stopping the polling of node %s:%s", self.host, self.port) _LOGGER.debug(
"Stopping the polling of node %s:%s", self.host, self.port
)
return
except Exception: except Exception:
_LOGGER.exception("Unexpected error in %s:%s", self.host, self.port) _LOGGER.exception(
raise "Unexpected error in %s:%s, retrying later", self.host, self.port
)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Start the polling task.""" """Start the polling task."""
@ -348,7 +351,7 @@ class BluesoundPlayer(MediaPlayerEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update internal status of the entity.""" """Update internal status of the entity."""
if not self._is_online: if not self.available:
return return
with suppress(TimeoutError): with suppress(TimeoutError):
@ -365,7 +368,7 @@ class BluesoundPlayer(MediaPlayerEntity):
try: try:
status = await self._player.status(etag=etag, poll_timeout=120, timeout=125) status = await self._player.status(etag=etag, poll_timeout=120, timeout=125)
self._is_online = True self._attr_available = True
self._last_status_update = dt_util.utcnow() self._last_status_update = dt_util.utcnow()
self._status = status self._status = status
@ -394,7 +397,7 @@ class BluesoundPlayer(MediaPlayerEntity):
self.async_write_ha_state() self.async_write_ha_state()
except (TimeoutError, ClientError): except (TimeoutError, ClientError):
self._is_online = False self._attr_available = False
self._last_status_update = None self._last_status_update = None
self._status = None self._status = None
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -16,7 +16,7 @@
"requirements": [ "requirements": [
"bleak==0.22.2", "bleak==0.22.2",
"bleak-retry-connector==3.5.0", "bleak-retry-connector==3.5.0",
"bluetooth-adapters==0.19.3", "bluetooth-adapters==0.19.4",
"bluetooth-auto-recovery==1.4.2", "bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.19.4", "bluetooth-data-tools==1.19.4",
"dbus-fast==2.22.1", "dbus-fast==2.22.1",

View File

@ -1,12 +1,11 @@
"""Support for Concord232 alarm control panels.""" """Support for Concord232 alarm control panels."""
# mypy: ignore-errors
from __future__ import annotations from __future__ import annotations
import datetime import datetime
import logging import logging
# from concord232 import client as concord232_client from concord232 import client as concord232_client
import requests import requests
import voluptuous as vol import voluptuous as vol

View File

@ -1,12 +1,11 @@
"""Support for exposing Concord232 elements as sensors.""" """Support for exposing Concord232 elements as sensors."""
# mypy: ignore-errors
from __future__ import annotations from __future__ import annotations
import datetime import datetime
import logging import logging
# from concord232 import client as concord232_client from concord232 import client as concord232_client
import requests import requests
import voluptuous as vol import voluptuous as vol

View File

@ -2,9 +2,8 @@
"domain": "concord232", "domain": "concord232",
"name": "Concord232", "name": "Concord232",
"codeowners": [], "codeowners": [],
"disabled": "This integration is disabled because it uses non-open source code to operate.",
"documentation": "https://www.home-assistant.io/integrations/concord232", "documentation": "https://www.home-assistant.io/integrations/concord232",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["concord232", "stevedore"], "loggers": ["concord232", "stevedore"],
"requirements": ["concord232==0.15"] "requirements": ["concord232==0.15.1"]
} }

View File

@ -1,5 +0,0 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]

View File

@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin", "documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pydaikin"], "loggers": ["pydaikin"],
"requirements": ["pydaikin==2.13.2"], "requirements": ["pydaikin==2.13.4"],
"zeroconf": ["_dkapi._tcp.local."] "zeroconf": ["_dkapi._tcp.local."]
} }

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from functools import partial
import logging import logging
import ssl import ssl
from typing import Any, cast from typing import Any, cast
@ -105,12 +106,15 @@ async def _validate_input(
if not user_input.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url: if not user_input.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url:
ssl_context = get_default_no_verify_context() ssl_context = get_default_no_verify_context()
mqtt_config = create_mqtt_config( mqtt_config = await hass.async_add_executor_job(
partial(
create_mqtt_config,
device_id=device_id, device_id=device_id,
country=country, country=country,
override_mqtt_url=mqtt_url, override_mqtt_url=mqtt_url,
ssl_context=ssl_context, ssl_context=ssl_context,
) )
)
client = MqttClient(mqtt_config, authenticator) client = MqttClient(mqtt_config, authenticator)
cannot_connect_field = CONF_OVERRIDE_MQTT_URL if mqtt_url else "base" cannot_connect_field = CONF_OVERRIDE_MQTT_URL if mqtt_url else "base"

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from functools import partial
import logging import logging
import ssl import ssl
from typing import Any from typing import Any
@ -64,32 +65,28 @@ class EcovacsController:
if not config.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url: if not config.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url:
ssl_context = get_default_no_verify_context() ssl_context = get_default_no_verify_context()
self._mqtt = MqttClient( self._mqtt_config_fn = partial(
create_mqtt_config( create_mqtt_config,
device_id=self._device_id, device_id=self._device_id,
country=country, country=country,
override_mqtt_url=mqtt_url, override_mqtt_url=mqtt_url,
ssl_context=ssl_context, ssl_context=ssl_context,
),
self._authenticator,
) )
self._mqtt_client: MqttClient | None = None
self._added_legacy_entities: set[str] = set() self._added_legacy_entities: set[str] = set()
async def initialize(self) -> None: async def initialize(self) -> None:
"""Init controller.""" """Init controller."""
mqtt_config_verfied = False
try: try:
devices = await self._api_client.get_devices() devices = await self._api_client.get_devices()
credentials = await self._authenticator.authenticate() credentials = await self._authenticator.authenticate()
for device_config in devices: for device_config in devices:
if isinstance(device_config, DeviceInfo): if isinstance(device_config, DeviceInfo):
# MQTT device # MQTT device
if not mqtt_config_verfied:
await self._mqtt.verify_config()
mqtt_config_verfied = True
device = Device(device_config, self._authenticator) device = Device(device_config, self._authenticator)
await device.initialize(self._mqtt) mqtt = await self._get_mqtt_client()
await device.initialize(mqtt)
self._devices.append(device) self._devices.append(device)
else: else:
# Legacy device # Legacy device
@ -116,7 +113,8 @@ class EcovacsController:
await device.teardown() await device.teardown()
for legacy_device in self._legacy_devices: for legacy_device in self._legacy_devices:
await self._hass.async_add_executor_job(legacy_device.disconnect) await self._hass.async_add_executor_job(legacy_device.disconnect)
await self._mqtt.disconnect() if self._mqtt_client is not None:
await self._mqtt_client.disconnect()
await self._authenticator.teardown() await self._authenticator.teardown()
def add_legacy_entity(self, device: VacBot, component: str) -> None: def add_legacy_entity(self, device: VacBot, component: str) -> None:
@ -127,6 +125,16 @@ class EcovacsController:
"""Check if legacy entity is added.""" """Check if legacy entity is added."""
return f"{device.vacuum['did']}_{component}" in self._added_legacy_entities return f"{device.vacuum['did']}_{component}" in self._added_legacy_entities
async def _get_mqtt_client(self) -> MqttClient:
"""Return validated MQTT client."""
if self._mqtt_client is None:
config = await self._hass.async_add_executor_job(self._mqtt_config_fn)
mqtt = MqttClient(config, self._authenticator)
await mqtt.verify_config()
self._mqtt_client = mqtt
return self._mqtt_client
@property @property
def devices(self) -> list[Device]: def devices(self) -> list[Device]:
"""Return devices.""" """Return devices."""

View File

@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["openwebif"], "loggers": ["openwebif"],
"requirements": ["openwebifpy==4.2.5"] "requirements": ["openwebifpy==4.2.7"]
} }

View File

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY, ATTR_CONDITION_CLOUDY,
@ -190,10 +192,12 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None:
if not (half_days := ec_data.daily_forecasts): if not (half_days := ec_data.daily_forecasts):
return None return None
def get_day_forecast(fcst: list[dict[str, str]]) -> Forecast: def get_day_forecast(
fcst: list[dict[str, Any]],
) -> Forecast:
high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None
return { return {
ATTR_FORECAST_TIME: fcst[0]["timestamp"], ATTR_FORECAST_TIME: fcst[0]["timestamp"].isoformat(),
ATTR_FORECAST_NATIVE_TEMP: high_temp, ATTR_FORECAST_NATIVE_TEMP: high_temp,
ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]), ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(

View File

@ -45,15 +45,13 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except exceptions.GlancesApiError as err: except exceptions.GlancesApiError as err:
raise UpdateFailed from err raise UpdateFailed from err
# Update computed values # Update computed values
uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None uptime: datetime | None = None
up_duration: timedelta | None = None up_duration: timedelta | None = None
if up_duration := parse_duration(data.get("uptime")): if "uptime" in data and (up_duration := parse_duration(data["uptime"])):
uptime = self.data["computed"]["uptime"] if self.data else None
# Update uptime if previous value is None or previous uptime is bigger than # Update uptime if previous value is None or previous uptime is bigger than
# new uptime (i.e. server restarted) # new uptime (i.e. server restarted)
if ( if uptime is None or self.data["computed"]["uptime_duration"] > up_duration:
self.data is None
or self.data["computed"]["uptime_duration"] > up_duration
):
uptime = utcnow() - up_duration uptime = utcnow() - up_duration
data["computed"] = {"uptime_duration": up_duration, "uptime": uptime} data["computed"] = {"uptime_duration": up_duration, "uptime": uptime}
return data or {} return data or {}

View File

@ -325,6 +325,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
entity_description: GlancesSensorEntityDescription entity_description: GlancesSensorEntityDescription
_attr_has_entity_name = True _attr_has_entity_name = True
_data_valid: bool = False
def __init__( def __init__(
self, self,
@ -351,14 +352,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Set sensor unavailable when native value is invalid.""" """Set sensor unavailable when native value is invalid."""
if super().available: return super().available and self._data_valid
return (
not self._numeric_state_expected
or isinstance(value := self.native_value, (int, float))
or isinstance(value, str)
and value.isnumeric()
)
return False
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
@ -368,10 +362,19 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
def _update_native_value(self) -> None: def _update_native_value(self) -> None:
"""Update sensor native value from coordinator data.""" """Update sensor native value from coordinator data."""
data = self.coordinator.data[self.entity_description.type] data = self.coordinator.data.get(self.entity_description.type)
if dict_val := data.get(self._sensor_label): if data and (dict_val := data.get(self._sensor_label)):
self._attr_native_value = dict_val.get(self.entity_description.key) self._attr_native_value = dict_val.get(self.entity_description.key)
elif self.entity_description.key in data: elif data and (self.entity_description.key in data):
self._attr_native_value = data.get(self.entity_description.key) self._attr_native_value = data.get(self.entity_description.key)
else: else:
self._attr_native_value = None self._attr_native_value = None
self._update_data_valid()
def _update_data_valid(self) -> None:
self._data_valid = self._attr_native_value is not None and (
not self._numeric_state_expected
or isinstance(self._attr_native_value, (int, float))
or isinstance(self._attr_native_value, str)
and self._attr_native_value.isnumeric()
)

View File

@ -60,8 +60,11 @@
"integration_not_found": { "integration_not_found": {
"title": "Integration {domain} not found", "title": "Integration {domain} not found",
"fix_flow": { "fix_flow": {
"abort": {
"issue_ignored": "Not existing integration {domain} ignored."
},
"step": { "step": {
"remove_entries": { "init": {
"title": "[%key:component::homeassistant::issues::integration_not_found::title%]", "title": "[%key:component::homeassistant::issues::integration_not_found::title%]",
"description": "The integration `{domain}` could not be found. This happens when a (custom) integration was removed from Home Assistant, but there are still configurations for this `integration`. Please use the buttons below to either remove the previous configurations for `{domain}` or ignore this.", "description": "The integration `{domain}` could not be found. This happens when a (custom) integration was removed from Home Assistant, but there are still configurations for this `integration`. Please use the buttons below to either remove the previous configurations for `{domain}` or ignore this.",
"menu_options": { "menu_options": {

View File

@ -22,6 +22,7 @@ from homeassistant.components import (
sensor, sensor,
) )
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
DOMAIN as MEDIA_PLAYER_DOMAIN, DOMAIN as MEDIA_PLAYER_DOMAIN,
@ -167,9 +168,11 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
vol.Optional( vol.Optional(
CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE
): cv.positive_int, ): cv.positive_int,
vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(binary_sensor.DOMAIN), vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(
[binary_sensor.DOMAIN, EVENT_DOMAIN]
),
vol.Optional(CONF_LINKED_DOORBELL_SENSOR): cv.entity_domain( vol.Optional(CONF_LINKED_DOORBELL_SENSOR): cv.entity_domain(
binary_sensor.DOMAIN [binary_sensor.DOMAIN, EVENT_DOMAIN]
), ),
} }
) )

View File

@ -845,21 +845,41 @@ class HKDevice:
async def async_update(self, now: datetime | None = None) -> None: async def async_update(self, now: datetime | None = None) -> None:
"""Poll state of all entities attached to this bridge/accessory.""" """Poll state of all entities attached to this bridge/accessory."""
to_poll = self.pollable_characteristics
accessories = self.entity_map.accessories
if ( if (
len(self.entity_map.accessories) == 1 len(accessories) == 1
and self.available and self.available
and not (self.pollable_characteristics - self.watchable_characteristics) and not (to_poll - self.watchable_characteristics)
and self.pairing.is_available and self.pairing.is_available
and await self.pairing.controller.async_reachable( and await self.pairing.controller.async_reachable(
self.unique_id, timeout=5.0 self.unique_id, timeout=5.0
) )
): ):
# If its a single accessory and all chars are watchable, # If its a single accessory and all chars are watchable,
# we don't need to poll. # only poll the firmware version to keep the connection alive
_LOGGER.debug("Accessory is reachable, skip polling: %s", self.unique_id) # https://github.com/home-assistant/core/issues/123412
return #
# Firmware revision is used here since iOS does this to keep camera
# connections alive, and the goal is to not regress
# https://github.com/home-assistant/core/issues/116143
# by polling characteristics that are not normally polled frequently
# and may not be tested by the device vendor.
#
_LOGGER.debug(
"Accessory is reachable, limiting poll to firmware version: %s",
self.unique_id,
)
first_accessory = accessories[0]
accessory_info = first_accessory.services.first(
service_type=ServicesTypes.ACCESSORY_INFORMATION
)
assert accessory_info is not None
firmware_iid = accessory_info[CharacteristicsTypes.FIRMWARE_REVISION].iid
to_poll = {(first_accessory.aid, firmware_iid)}
if not self.pollable_characteristics: if not to_poll:
self.async_update_available_state() self.async_update_available_state()
_LOGGER.debug( _LOGGER.debug(
"HomeKit connection not polling any characteristics: %s", self.unique_id "HomeKit connection not polling any characteristics: %s", self.unique_id
@ -892,9 +912,7 @@ class HKDevice:
_LOGGER.debug("Starting HomeKit device update: %s", self.unique_id) _LOGGER.debug("Starting HomeKit device update: %s", self.unique_id)
try: try:
new_values_dict = await self.get_characteristics( new_values_dict = await self.get_characteristics(to_poll)
self.pollable_characteristics
)
except AccessoryNotFoundError: except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not # Not only did the connection fail, but also the accessory is not
# visible on the network. # visible on the network.

View File

@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"], "loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.1"], "requirements": ["aiohomekit==3.2.2"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homeworks", "documentation": "https://www.home-assistant.io/integrations/homeworks",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyhomeworks"], "loggers": ["pyhomeworks"],
"requirements": ["pyhomeworks==1.1.0"] "requirements": ["pyhomeworks==1.1.1"]
} }

View File

@ -533,7 +533,7 @@ class HTML5NotificationService(BaseNotificationService):
elif response.status_code > 399: elif response.status_code > 399:
_LOGGER.error( _LOGGER.error(
"There was an issue sending the notification %s: %s", "There was an issue sending the notification %s: %s",
response.status, response.status_code,
response.text, response.text,
) )

View File

@ -147,18 +147,10 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Start the KNX integration.""" """Start the KNX integration."""
hass.data[DATA_HASS_CONFIG] = config hass.data[DATA_HASS_CONFIG] = config
conf: ConfigType | None = config.get(DOMAIN) if (conf := config.get(DOMAIN)) is not None:
hass.data[DATA_KNX_CONFIG] = dict(conf)
if conf is None:
# If we have a config entry, setup is done by that config entry.
# If there is no config entry, this should fail.
return bool(hass.config_entries.async_entries(DOMAIN))
conf = dict(conf)
hass.data[DATA_KNX_CONFIG] = conf
register_knx_services(hass) register_knx_services(hass)
return True return True

View File

@ -5,7 +5,11 @@ from __future__ import annotations
from typing import Any from typing import Any
from xknx import XKNX from xknx import XKNX
from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode from xknx.devices import (
Climate as XknxClimate,
ClimateMode as XknxClimateMode,
Device as XknxDevice,
)
from xknx.dpt.dpt_20 import HVACControllerMode from xknx.dpt.dpt_20 import HVACControllerMode
from homeassistant import config_entries from homeassistant import config_entries
@ -241,12 +245,9 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
if self._device.supports_on_off and not self._device.is_on: if self._device.supports_on_off and not self._device.is_on:
return HVACMode.OFF return HVACMode.OFF
if self._device.mode is not None and self._device.mode.supports_controller_mode: if self._device.mode is not None and self._device.mode.supports_controller_mode:
hvac_mode = CONTROLLER_MODES.get( return CONTROLLER_MODES.get(
self._device.mode.controller_mode, self.default_hvac_mode self._device.mode.controller_mode, self.default_hvac_mode
) )
if hvac_mode is not HVACMode.OFF:
self._last_hvac_mode = hvac_mode
return hvac_mode
return self.default_hvac_mode return self.default_hvac_mode
@property @property
@ -261,11 +262,15 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
if self._device.supports_on_off: if self._device.supports_on_off:
if not ha_controller_modes: if not ha_controller_modes:
ha_controller_modes.append(self.default_hvac_mode) ha_controller_modes.append(self._last_hvac_mode)
ha_controller_modes.append(HVACMode.OFF) ha_controller_modes.append(HVACMode.OFF)
hvac_modes = list(set(filter(None, ha_controller_modes))) hvac_modes = list(set(filter(None, ha_controller_modes)))
return hvac_modes if hvac_modes else [self.default_hvac_mode] return (
hvac_modes
if hvac_modes
else [self.hvac_mode] # mode read-only -> fall back to only current mode
)
@property @property
def hvac_action(self) -> HVACAction | None: def hvac_action(self) -> HVACAction | None:
@ -354,3 +359,13 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
self._device.mode.unregister_device_updated_cb(self.after_update_callback) self._device.mode.unregister_device_updated_cb(self.after_update_callback)
self._device.mode.xknx.devices.async_remove(self._device.mode) self._device.mode.xknx.devices.async_remove(self._device.mode)
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
def after_update_callback(self, _device: XknxDevice) -> None:
"""Call after device was updated."""
if self._device.mode is not None and self._device.mode.supports_controller_mode:
hvac_mode = CONTROLLER_MODES.get(
self._device.mode.controller_mode, self.default_hvac_mode
)
if hvac_mode is not HVACMode.OFF:
self._last_hvac_mode = hvac_mode
super().after_update_callback(_device)

View File

@ -226,7 +226,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight
group_address_color_temp_state = None group_address_color_temp_state = None
color_temperature_type = ColorTemperatureType.UINT_2_BYTE color_temperature_type = ColorTemperatureType.UINT_2_BYTE
if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP): if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP):
if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE: if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE.value:
group_address_tunable_white = ga_color_temp[CONF_GA_WRITE] group_address_tunable_white = ga_color_temp[CONF_GA_WRITE]
group_address_tunable_white_state = [ group_address_tunable_white_state = [
ga_color_temp[CONF_GA_STATE], ga_color_temp[CONF_GA_STATE],
@ -239,7 +239,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight
ga_color_temp[CONF_GA_STATE], ga_color_temp[CONF_GA_STATE],
*ga_color_temp[CONF_GA_PASSIVE], *ga_color_temp[CONF_GA_PASSIVE],
] ]
if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT: if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT.value:
color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE
_color_dpt = get_dpt(CONF_GA_COLOR) _color_dpt = get_dpt(CONF_GA_COLOR)

View File

@ -11,9 +11,9 @@
"loggers": ["xknx", "xknxproject"], "loggers": ["xknx", "xknxproject"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": [ "requirements": [
"xknx==3.0.0", "xknx==3.1.0",
"xknxproject==3.7.1", "xknxproject==3.7.1",
"knx-frontend==2024.8.6.211307" "knx-frontend==2024.8.9.225351"
], ],
"single_config_entry": true "single_config_entry": true
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/lacrosse_view", "documentation": "https://www.home-assistant.io/integrations/lacrosse_view",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["lacrosse_view"], "loggers": ["lacrosse_view"],
"requirements": ["lacrosse-view==1.0.1"] "requirements": ["lacrosse-view==1.0.2"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/lcn", "documentation": "https://www.home-assistant.io/integrations/lcn",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pypck"], "loggers": ["pypck"],
"requirements": ["pypck==0.7.17"] "requirements": ["pypck==0.7.20"]
} }

View File

@ -9,7 +9,7 @@
}, },
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pylutron_caseta"], "loggers": ["pylutron_caseta"],
"requirements": ["pylutron-caseta==0.20.0"], "requirements": ["pylutron-caseta==0.21.1"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_lutron._tcp.local.", "type": "_lutron._tcp.local.",

View File

@ -277,4 +277,15 @@ class MadvrSensor(MadVREntity, SensorEntity):
@property @property
def native_value(self) -> float | str | None: def native_value(self) -> float | str | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator) val = self.entity_description.value_fn(self.coordinator)
# check if sensor is enum
if self.entity_description.device_class == SensorDeviceClass.ENUM:
if (
self.entity_description.options
and val in self.entity_description.options
):
return val
# return None for values that are not in the options
return None
return val

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mealie", "documentation": "https://www.home-assistant.io/integrations/mealie",
"integration_type": "service", "integration_type": "service",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["aiomealie==0.8.0"] "requirements": ["aiomealie==0.8.1"]
} }

View File

@ -20,5 +20,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["google_nest_sdm"], "loggers": ["google_nest_sdm"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["google-nest-sdm==4.0.5"] "requirements": ["google-nest-sdm==4.0.6"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nextbus", "documentation": "https://www.home-assistant.io/integrations/nextbus",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["py_nextbus"], "loggers": ["py_nextbus"],
"requirements": ["py-nextbusnext==2.0.3"] "requirements": ["py-nextbusnext==2.0.4"]
} }

View File

@ -22,6 +22,7 @@ class PiHoleUpdateEntityDescription(UpdateEntityDescription):
installed_version: Callable[[dict], str | None] = lambda api: None installed_version: Callable[[dict], str | None] = lambda api: None
latest_version: Callable[[dict], str | None] = lambda api: None latest_version: Callable[[dict], str | None] = lambda api: None
has_update: Callable[[dict], bool | None] = lambda api: None
release_base_url: str | None = None release_base_url: str | None = None
title: str | None = None title: str | None = None
@ -34,6 +35,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
installed_version=lambda versions: versions.get("core_current"), installed_version=lambda versions: versions.get("core_current"),
latest_version=lambda versions: versions.get("core_latest"), latest_version=lambda versions: versions.get("core_latest"),
has_update=lambda versions: versions.get("core_update"),
release_base_url="https://github.com/pi-hole/pi-hole/releases/tag", release_base_url="https://github.com/pi-hole/pi-hole/releases/tag",
), ),
PiHoleUpdateEntityDescription( PiHoleUpdateEntityDescription(
@ -43,6 +45,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
installed_version=lambda versions: versions.get("web_current"), installed_version=lambda versions: versions.get("web_current"),
latest_version=lambda versions: versions.get("web_latest"), latest_version=lambda versions: versions.get("web_latest"),
has_update=lambda versions: versions.get("web_update"),
release_base_url="https://github.com/pi-hole/AdminLTE/releases/tag", release_base_url="https://github.com/pi-hole/AdminLTE/releases/tag",
), ),
PiHoleUpdateEntityDescription( PiHoleUpdateEntityDescription(
@ -52,6 +55,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
installed_version=lambda versions: versions.get("FTL_current"), installed_version=lambda versions: versions.get("FTL_current"),
latest_version=lambda versions: versions.get("FTL_latest"), latest_version=lambda versions: versions.get("FTL_latest"),
has_update=lambda versions: versions.get("FTL_update"),
release_base_url="https://github.com/pi-hole/FTL/releases/tag", release_base_url="https://github.com/pi-hole/FTL/releases/tag",
), ),
) )
@ -110,7 +114,9 @@ class PiHoleUpdateEntity(PiHoleEntity, UpdateEntity):
def latest_version(self) -> str | None: def latest_version(self) -> str | None:
"""Latest version available for install.""" """Latest version available for install."""
if isinstance(self.api.versions, dict): if isinstance(self.api.versions, dict):
if self.entity_description.has_update(self.api.versions):
return self.entity_description.latest_version(self.api.versions) return self.entity_description.latest_version(self.api.versions)
return self.installed_version
return None return None
@property @property

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw", "documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioqsw"], "loggers": ["aioqsw"],
"requirements": ["aioqsw==0.4.0"] "requirements": ["aioqsw==0.4.1"]
} }

View File

@ -77,7 +77,7 @@ class LegacyBase(DeclarativeBase):
"""Base class for tables, used for schema migration.""" """Base class for tables, used for schema migration."""
SCHEMA_VERSION = 44 SCHEMA_VERSION = 45
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -669,27 +669,58 @@ def _drop_foreign_key_constraints(
def _restore_foreign_key_constraints( def _restore_foreign_key_constraints(
session_maker: Callable[[], Session], session_maker: Callable[[], Session],
engine: Engine, engine: Engine,
dropped_constraints: list[tuple[str, str, ReflectedForeignKeyConstraint]], foreign_columns: list[tuple[str, str, str | None, str | None]],
) -> None: ) -> None:
"""Restore foreign key constraints.""" """Restore foreign key constraints."""
for table, column, dropped_constraint in dropped_constraints: for table, column, foreign_table, foreign_column in foreign_columns:
constraints = Base.metadata.tables[table].foreign_key_constraints constraints = Base.metadata.tables[table].foreign_key_constraints
for constraint in constraints: for constraint in constraints:
if constraint.column_keys == [column]: if constraint.column_keys == [column]:
break break
else: else:
_LOGGER.info( _LOGGER.info("Did not find a matching constraint for %s.%s", table, column)
"Did not find a matching constraint for %s", dropped_constraint
)
continue continue
if TYPE_CHECKING:
assert foreign_table is not None
assert foreign_column is not None
# AddConstraint mutates the constraint passed to it, we need to # AddConstraint mutates the constraint passed to it, we need to
# undo that to avoid changing the behavior of the table schema. # undo that to avoid changing the behavior of the table schema.
# https://github.com/sqlalchemy/sqlalchemy/blob/96f1172812f858fead45cdc7874abac76f45b339/lib/sqlalchemy/sql/ddl.py#L746-L748 # https://github.com/sqlalchemy/sqlalchemy/blob/96f1172812f858fead45cdc7874abac76f45b339/lib/sqlalchemy/sql/ddl.py#L746-L748
create_rule = constraint._create_rule # noqa: SLF001 create_rule = constraint._create_rule # noqa: SLF001
add_constraint = AddConstraint(constraint) # type: ignore[no-untyped-call] add_constraint = AddConstraint(constraint) # type: ignore[no-untyped-call]
constraint._create_rule = create_rule # noqa: SLF001 constraint._create_rule = create_rule # noqa: SLF001
try:
_add_constraint(session_maker, add_constraint, table, column)
except IntegrityError:
_LOGGER.exception(
(
"Could not update foreign options in %s table, will delete "
"violations and try again"
),
table,
)
_delete_foreign_key_violations(
session_maker, engine, table, column, foreign_table, foreign_column
)
_add_constraint(session_maker, add_constraint, table, column)
def _add_constraint(
session_maker: Callable[[], Session],
add_constraint: AddConstraint,
table: str,
column: str,
) -> None:
"""Add a foreign key constraint."""
_LOGGER.warning(
"Adding foreign key constraint to %s.%s. "
"Note: this can take several minutes on large databases and slow "
"machines. Please be patient!",
table,
column,
)
with session_scope(session=session_maker()) as session: with session_scope(session=session_maker()) as session:
try: try:
connection = session.connection() connection = session.connection()
@ -698,6 +729,119 @@ def _restore_foreign_key_constraints(
_LOGGER.exception("Could not update foreign options in %s table", table) _LOGGER.exception("Could not update foreign options in %s table", table)
def _delete_foreign_key_violations(
session_maker: Callable[[], Session],
engine: Engine,
table: str,
column: str,
foreign_table: str,
foreign_column: str,
) -> None:
"""Remove rows which violate the constraints."""
if engine.dialect.name not in (SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL):
raise RuntimeError(
f"_delete_foreign_key_violations not supported for {engine.dialect.name}"
)
_LOGGER.warning(
"Rows in table %s where %s references non existing %s.%s will be %s. "
"Note: this can take several minutes on large databases and slow "
"machines. Please be patient!",
table,
column,
foreign_table,
foreign_column,
"set to NULL" if table == foreign_table else "deleted",
)
result: CursorResult | None = None
if table == foreign_table:
# In case of a foreign reference to the same table, we set invalid
# references to NULL instead of deleting as deleting rows may
# cause additional invalid references to be created. This is to handle
# old_state_id referencing a missing state.
if engine.dialect.name == SupportedDialect.MYSQL:
while result is None or result.rowcount > 0:
with session_scope(session=session_maker()) as session:
# The subquery (SELECT {foreign_column} from {foreign_table}) is
# to be compatible with old MySQL versions which do not allow
# referencing the table being updated in the WHERE clause.
result = session.connection().execute(
text(
f"UPDATE {table} as t1 " # noqa: S608
f"SET {column} = NULL "
"WHERE ("
f"t1.{column} IS NOT NULL AND "
"NOT EXISTS "
"(SELECT 1 "
f"FROM (SELECT {foreign_column} from {foreign_table}) AS t2 "
f"WHERE t2.{foreign_column} = t1.{column})) "
"LIMIT 100000;"
)
)
elif engine.dialect.name == SupportedDialect.POSTGRESQL:
while result is None or result.rowcount > 0:
with session_scope(session=session_maker()) as session:
# PostgreSQL does not support LIMIT in UPDATE clauses, so we
# update matches from a limited subquery instead.
result = session.connection().execute(
text(
f"UPDATE {table} " # noqa: S608
f"SET {column} = NULL "
f"WHERE {column} in "
f"(SELECT {column} from {table} as t1 "
"WHERE ("
f"t1.{column} IS NOT NULL AND "
"NOT EXISTS "
"(SELECT 1 "
f"FROM {foreign_table} AS t2 "
f"WHERE t2.{foreign_column} = t1.{column})) "
"LIMIT 100000);"
)
)
return
if engine.dialect.name == SupportedDialect.MYSQL:
while result is None or result.rowcount > 0:
with session_scope(session=session_maker()) as session:
result = session.connection().execute(
# We don't use an alias for the table we're deleting from,
# support of the form `DELETE FROM table AS t1` was added in
# MariaDB 11.6 and is not supported by MySQL. Those engines
# instead support the from `DELETE t1 from table AS t1` which
# is not supported by PostgreSQL and undocumented for MariaDB.
text(
f"DELETE FROM {table} " # noqa: S608
"WHERE ("
f"{table}.{column} IS NOT NULL AND "
"NOT EXISTS "
"(SELECT 1 "
f"FROM {foreign_table} AS t2 "
f"WHERE t2.{foreign_column} = {table}.{column})) "
"LIMIT 100000;"
)
)
elif engine.dialect.name == SupportedDialect.POSTGRESQL:
while result is None or result.rowcount > 0:
with session_scope(session=session_maker()) as session:
# PostgreSQL does not support LIMIT in DELETE clauses, so we
# delete matches from a limited subquery instead.
result = session.connection().execute(
text(
f"DELETE FROM {table} " # noqa: S608
f"WHERE {column} in "
f"(SELECT {column} from {table} as t1 "
"WHERE ("
f"t1.{column} IS NOT NULL AND "
"NOT EXISTS "
"(SELECT 1 "
f"FROM {foreign_table} AS t2 "
f"WHERE t2.{foreign_column} = t1.{column})) "
"LIMIT 100000);"
)
)
@database_job_retry_wrapper("Apply migration update", 10) @database_job_retry_wrapper("Apply migration update", 10)
def _apply_update( def _apply_update(
instance: Recorder, instance: Recorder,
@ -1459,6 +1603,38 @@ class _SchemaVersion43Migrator(_SchemaVersionMigrator, target_version=43):
) )
FOREIGN_COLUMNS = (
(
"events",
("data_id", "event_type_id"),
(
("data_id", "event_data", "data_id"),
("event_type_id", "event_types", "event_type_id"),
),
),
(
"states",
("event_id", "old_state_id", "attributes_id", "metadata_id"),
(
("event_id", None, None),
("old_state_id", "states", "state_id"),
("attributes_id", "state_attributes", "attributes_id"),
("metadata_id", "states_meta", "metadata_id"),
),
),
(
"statistics",
("metadata_id",),
(("metadata_id", "statistics_meta", "id"),),
),
(
"statistics_short_term",
("metadata_id",),
(("metadata_id", "statistics_meta", "id"),),
),
)
class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44): class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44):
def _apply_update(self) -> None: def _apply_update(self) -> None:
"""Version specific update method.""" """Version specific update method."""
@ -1471,24 +1647,14 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44):
else "" else ""
) )
# First drop foreign key constraints # First drop foreign key constraints
foreign_columns = ( for table, columns, _ in FOREIGN_COLUMNS:
("events", ("data_id", "event_type_id")), for column in columns:
("states", ("event_id", "old_state_id", "attributes_id", "metadata_id")), _drop_foreign_key_constraints(
("statistics", ("metadata_id",)),
("statistics_short_term", ("metadata_id",)),
)
dropped_constraints = [
dropped_constraint
for table, columns in foreign_columns
for column in columns
for dropped_constraint in _drop_foreign_key_constraints(
self.session_maker, self.engine, table, column self.session_maker, self.engine, table, column
)[1] )
]
_LOGGER.debug("Dropped foreign key constraints: %s", dropped_constraints)
# Then modify the constrained columns # Then modify the constrained columns
for table, columns in foreign_columns: for table, columns, _ in FOREIGN_COLUMNS:
_modify_columns( _modify_columns(
self.session_maker, self.session_maker,
self.engine, self.engine,
@ -1518,9 +1684,24 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44):
table, table,
[f"{column} {BIG_INTEGER_SQL} {identity_sql}"], [f"{column} {BIG_INTEGER_SQL} {identity_sql}"],
) )
# Finally restore dropped constraints
class _SchemaVersion45Migrator(_SchemaVersionMigrator, target_version=45):
def _apply_update(self) -> None:
"""Version specific update method."""
# We skip this step for SQLITE, it doesn't have differently sized integers
if self.engine.dialect.name == SupportedDialect.SQLITE:
return
# Restore constraints dropped in migration to schema version 44
_restore_foreign_key_constraints( _restore_foreign_key_constraints(
self.session_maker, self.engine, dropped_constraints self.session_maker,
self.engine,
[
(table, column, foreign_table, foreign_column)
for table, _, foreign_mappings in FOREIGN_COLUMNS
for column, foreign_table, foreign_column in foreign_mappings
],
) )

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/russound_rio", "documentation": "https://www.home-assistant.io/integrations/russound_rio",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiorussound"], "loggers": ["aiorussound"],
"requirements": ["aiorussound==2.2.2"] "requirements": ["aiorussound==2.2.3"]
} }

View File

@ -128,11 +128,18 @@ class RussoundZoneDevice(MediaPlayerEntity):
self._zone = zone self._zone = zone
self._sources = sources self._sources = sources
self._attr_name = zone.name self._attr_name = zone.name
self._attr_unique_id = f"{self._controller.mac_address}-{zone.device_str()}" primary_mac_address = (
self._controller.mac_address
or self._controller.parent_controller.mac_address
)
self._attr_unique_id = f"{primary_mac_address}-{zone.device_str()}"
device_identifier = (
self._controller.mac_address
or f"{primary_mac_address}-{self._controller.controller_id}"
)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
# Use MAC address of Russound device as identifier # Use MAC address of Russound device as identifier
identifiers={(DOMAIN, self._controller.mac_address)}, identifiers={(DOMAIN, device_identifier)},
connections={(CONNECTION_NETWORK_MAC, self._controller.mac_address)},
manufacturer="Russound", manufacturer="Russound",
name=self._controller.controller_type, name=self._controller.controller_type,
model=self._controller.controller_type, model=self._controller.controller_type,
@ -143,6 +150,10 @@ class RussoundZoneDevice(MediaPlayerEntity):
DOMAIN, DOMAIN,
self._controller.parent_controller.mac_address, self._controller.parent_controller.mac_address,
) )
else:
self._attr_device_info["connections"] = {
(CONNECTION_NETWORK_MAC, self._controller.mac_address)
}
for flag, feature in MP_FEATURES_BY_FLAG.items(): for flag, feature in MP_FEATURES_BY_FLAG.items():
if flag in zone.instance.supported_features: if flag in zone.instance.supported_features:
self._attr_supported_features |= feature self._attr_supported_features |= feature

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/schlage", "documentation": "https://www.home-assistant.io/integrations/schlage",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["pyschlage==2024.6.0"] "requirements": ["pyschlage==2024.8.0"]
} }

View File

@ -9,7 +9,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aioshelly"], "loggers": ["aioshelly"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aioshelly==11.1.0"], "requirements": ["aioshelly==11.2.0"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View File

@ -44,7 +44,7 @@ async def async_setup_entry(
translation_key="request_timeout", translation_key="request_timeout",
translation_placeholders={ translation_placeholders={
"config_title": entry.title, "config_title": entry.title,
"error": e, "error": str(e),
}, },
) from e ) from e
except OpendataTransportError as e: except OpendataTransportError as e:
@ -54,7 +54,7 @@ async def async_setup_entry(
translation_placeholders={ translation_placeholders={
**PLACEHOLDERS, **PLACEHOLDERS,
"config_title": entry.title, "config_title": entry.title,
"error": e, "error": str(e),
}, },
) from e ) from e

View File

@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/synology_dsm", "documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["synology_dsm"], "loggers": ["synology_dsm"],
"requirements": ["py-synologydsm-api==2.4.4"], "requirements": ["py-synologydsm-api==2.4.5"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Synology", "manufacturer": "Synology",

View File

@ -10,6 +10,6 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["systembridgeconnector"], "loggers": ["systembridgeconnector"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["systembridgeconnector==4.1.0", "systembridgemodels==4.1.0"], "requirements": ["systembridgeconnector==4.1.5", "systembridgemodels==4.2.4"],
"zeroconf": ["_system-bridge._tcp.local."] "zeroconf": ["_system-bridge._tcp.local."]
} }

View File

@ -168,13 +168,13 @@ class TessieRearTrunkEntity(TessieEntity, CoverEntity):
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open rear trunk.""" """Open rear trunk."""
if self._value == TessieCoverStates.CLOSED: if self.is_closed:
await self.run(open_close_rear_trunk) await self.run(open_close_rear_trunk)
self.set((self.key, TessieCoverStates.OPEN)) self.set((self.key, TessieCoverStates.OPEN))
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Close rear trunk.""" """Close rear trunk."""
if self._value == TessieCoverStates.OPEN: if not self.is_closed:
await self.run(open_close_rear_trunk) await self.run(open_close_rear_trunk)
self.set((self.key, TessieCoverStates.CLOSED)) self.set((self.key, TessieCoverStates.CLOSED))

View File

@ -8,7 +8,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiounifi"], "loggers": ["aiounifi"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aiounifi==79"], "requirements": ["aiounifi==80"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -15,8 +15,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -62,9 +60,8 @@ class WolButton(ButtonEntity):
self._attr_unique_id = dr.format_mac(mac_address) self._attr_unique_id = dr.format_mac(mac_address)
self._attr_device_info = dr.DeviceInfo( self._attr_device_info = dr.DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, self._attr_unique_id)}, connections={(dr.CONNECTION_NETWORK_MAC, self._attr_unique_id)},
identifiers={(DOMAIN, self._attr_unique_id)}, default_manufacturer="Wake on LAN",
manufacturer="Wake on LAN", default_name=name,
name=name,
) )
async def async_press(self) -> None: async def async_press(self) -> None:

View File

@ -7,6 +7,6 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["wled==0.20.1"], "requirements": ["wled==0.20.2"],
"zeroconf": ["_wled._tcp.local."] "zeroconf": ["_wled._tcp.local."]
} }

View File

@ -1,6 +1,7 @@
"""Constants for the Yamaha component.""" """Constants for the Yamaha component."""
DOMAIN = "yamaha" DOMAIN = "yamaha"
DISCOVER_TIMEOUT = 3
KNOWN_ZONES = "known_zones" KNOWN_ZONES = "known_zones"
CURSOR_TYPE_DOWN = "down" CURSOR_TYPE_DOWN = "down"
CURSOR_TYPE_LEFT = "left" CURSOR_TYPE_LEFT = "left"

View File

@ -31,6 +31,7 @@ from .const import (
CURSOR_TYPE_RIGHT, CURSOR_TYPE_RIGHT,
CURSOR_TYPE_SELECT, CURSOR_TYPE_SELECT,
CURSOR_TYPE_UP, CURSOR_TYPE_UP,
DISCOVER_TIMEOUT,
DOMAIN, DOMAIN,
KNOWN_ZONES, KNOWN_ZONES,
SERVICE_ENABLE_OUTPUT, SERVICE_ENABLE_OUTPUT,
@ -125,18 +126,33 @@ def _discovery(config_info):
elif config_info.host is None: elif config_info.host is None:
_LOGGER.debug("Config No Host Supplied Zones") _LOGGER.debug("Config No Host Supplied Zones")
zones = [] zones = []
for recv in rxv.find(): for recv in rxv.find(DISCOVER_TIMEOUT):
zones.extend(recv.zone_controllers()) zones.extend(recv.zone_controllers())
else: else:
_LOGGER.debug("Config Zones") _LOGGER.debug("Config Zones")
zones = None zones = None
# Fix for upstream issues in rxv.find() with some hardware. # Fix for upstream issues in rxv.find() with some hardware.
with contextlib.suppress(AttributeError): with contextlib.suppress(AttributeError, ValueError):
for recv in rxv.find(): for recv in rxv.find(DISCOVER_TIMEOUT):
_LOGGER.debug(
"Found Serial %s %s %s",
recv.serial_number,
recv.ctrl_url,
recv.zone,
)
if recv.ctrl_url == config_info.ctrl_url: if recv.ctrl_url == config_info.ctrl_url:
_LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) _LOGGER.debug(
zones = recv.zone_controllers() "Config Zones Matched Serial %s: %s",
recv.ctrl_url,
recv.serial_number,
)
zones = rxv.RXV(
config_info.ctrl_url,
friendly_name=config_info.name,
serial_number=recv.serial_number,
model_name=recv.model_name,
).zone_controllers()
break break
if not zones: if not zones:
@ -170,7 +186,7 @@ async def async_setup_platform(
entities = [] entities = []
for zctrl in zone_ctrls: for zctrl in zone_ctrls:
_LOGGER.debug("Receiver zone: %s", zctrl.zone) _LOGGER.debug("Receiver zone: %s serial %s", zctrl.zone, zctrl.serial_number)
if config_info.zone_ignore and zctrl.zone in config_info.zone_ignore: if config_info.zone_ignore and zctrl.zone in config_info.zone_ignore:
_LOGGER.debug("Ignore receiver zone: %s %s", config_info.name, zctrl.zone) _LOGGER.debug("Ignore receiver zone: %s %s", config_info.name, zctrl.zone)
continue continue

View File

@ -62,7 +62,7 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return entity availability.""" """Return entity availability."""
return self.entity_data.device_proxy.device.available return self.entity_data.entity.available
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:

View File

@ -21,7 +21,7 @@
"zha", "zha",
"universal_silabs_flasher" "universal_silabs_flasher"
], ],
"requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.30"], "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.31"],
"usb": [ "usb": [
{ {
"vid": "10C4", "vid": "10C4",

View File

@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024 MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 8 MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "1" PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View File

@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2
aiodiscover==2.1.0 aiodiscover==2.1.0
aiodns==3.2.0 aiodns==3.2.0
aiohttp-fast-zlib==0.1.1 aiohttp-fast-zlib==0.1.1
aiohttp==3.10.2 aiohttp==3.10.3
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiozoneinfo==0.2.1 aiozoneinfo==0.2.1
astral==2.2 astral==2.2
@ -16,7 +16,7 @@ awesomeversion==24.6.0
bcrypt==4.1.3 bcrypt==4.1.3
bleak-retry-connector==3.5.0 bleak-retry-connector==3.5.0
bleak==0.22.2 bleak==0.22.2
bluetooth-adapters==0.19.3 bluetooth-adapters==0.19.4
bluetooth-auto-recovery==1.4.2 bluetooth-auto-recovery==1.4.2
bluetooth-data-tools==1.19.4 bluetooth-data-tools==1.19.4
cached_ipaddress==0.3.0 cached_ipaddress==0.3.0

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2024.8.1" version = "2024.8.2"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -24,7 +24,7 @@ classifiers = [
requires-python = ">=3.12.0" requires-python = ">=3.12.0"
dependencies = [ dependencies = [
"aiodns==3.2.0", "aiodns==3.2.0",
"aiohttp==3.10.2", "aiohttp==3.10.3",
"aiohttp_cors==0.7.0", "aiohttp_cors==0.7.0",
"aiohttp-fast-zlib==0.1.1", "aiohttp-fast-zlib==0.1.1",
"aiozoneinfo==0.2.1", "aiozoneinfo==0.2.1",

View File

@ -4,7 +4,7 @@
# Home Assistant Core # Home Assistant Core
aiodns==3.2.0 aiodns==3.2.0
aiohttp==3.10.2 aiohttp==3.10.3
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiohttp-fast-zlib==0.1.1 aiohttp-fast-zlib==0.1.1
aiozoneinfo==0.2.1 aiozoneinfo==0.2.1

View File

@ -4,7 +4,7 @@
-r requirements.txt -r requirements.txt
# homeassistant.components.aemet # homeassistant.components.aemet
AEMET-OpenData==0.5.3 AEMET-OpenData==0.5.4
# homeassistant.components.honeywell # homeassistant.components.honeywell
AIOSomecomfort==0.0.25 AIOSomecomfort==0.0.25
@ -176,7 +176,7 @@ aio-georss-gdacs==0.9
aioairq==0.3.2 aioairq==0.3.2
# homeassistant.components.airzone_cloud # homeassistant.components.airzone_cloud
aioairzone-cloud==0.6.1 aioairzone-cloud==0.6.2
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.8.1 aioairzone==0.8.1
@ -255,7 +255,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==3.2.1 aiohomekit==3.2.2
# homeassistant.components.hue # homeassistant.components.hue
aiohue==4.7.2 aiohue==4.7.2
@ -288,7 +288,7 @@ aiolookin==1.0.0
aiolyric==1.1.0 aiolyric==1.1.0
# homeassistant.components.mealie # homeassistant.components.mealie
aiomealie==0.8.0 aiomealie==0.8.1
# homeassistant.components.modern_forms # homeassistant.components.modern_forms
aiomodernforms==0.1.8 aiomodernforms==0.1.8
@ -335,7 +335,7 @@ aiopvpc==4.2.2
aiopyarr==23.4.0 aiopyarr==23.4.0
# homeassistant.components.qnap_qsw # homeassistant.components.qnap_qsw
aioqsw==0.4.0 aioqsw==0.4.1
# homeassistant.components.rainforest_raven # homeassistant.components.rainforest_raven
aioraven==0.7.0 aioraven==0.7.0
@ -350,7 +350,7 @@ aioridwell==2024.01.0
aioruckus==0.34 aioruckus==0.34
# homeassistant.components.russound_rio # homeassistant.components.russound_rio
aiorussound==2.2.2 aiorussound==2.2.3
# homeassistant.components.ruuvi_gateway # homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0 aioruuvigateway==0.1.0
@ -359,7 +359,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0 aiosenz==1.0.0
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==11.1.0 aioshelly==11.2.0
# homeassistant.components.skybell # homeassistant.components.skybell
aioskybell==22.7.0 aioskybell==22.7.0
@ -386,7 +386,7 @@ aiotankerkoenig==0.4.1
aiotractive==0.6.0 aiotractive==0.6.0
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==79 aiounifi==80
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.3.2 aiovlc==0.3.2
@ -591,7 +591,7 @@ bluemaestro-ble==0.2.3
# bluepy==1.3.0 # bluepy==1.3.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.19.3 bluetooth-adapters==0.19.4
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==1.4.2 bluetooth-auto-recovery==1.4.2
@ -672,6 +672,9 @@ colorlog==6.8.2
# homeassistant.components.color_extractor # homeassistant.components.color_extractor
colorthief==0.2.1 colorthief==0.2.1
# homeassistant.components.concord232
concord232==0.15.1
# homeassistant.components.upc_connect # homeassistant.components.upc_connect
connect-box==0.3.1 connect-box==0.3.1
@ -986,7 +989,7 @@ google-cloud-texttospeech==2.16.3
google-generativeai==0.6.0 google-generativeai==0.6.0
# homeassistant.components.nest # homeassistant.components.nest
google-nest-sdm==4.0.5 google-nest-sdm==4.0.6
# homeassistant.components.google_travel_time # homeassistant.components.google_travel_time
googlemaps==2.5.1 googlemaps==2.5.1
@ -1219,7 +1222,7 @@ kiwiki-client==0.1.1
knocki==0.3.1 knocki==0.3.1
# homeassistant.components.knx # homeassistant.components.knx
knx-frontend==2024.8.6.211307 knx-frontend==2024.8.9.225351
# homeassistant.components.konnected # homeassistant.components.konnected
konnected==1.2.0 konnected==1.2.0
@ -1228,7 +1231,7 @@ konnected==1.2.0
krakenex==2.1.0 krakenex==2.1.0
# homeassistant.components.lacrosse_view # homeassistant.components.lacrosse_view
lacrosse-view==1.0.1 lacrosse-view==1.0.2
# homeassistant.components.eufy # homeassistant.components.eufy
lakeside==0.13 lakeside==0.13
@ -1505,7 +1508,7 @@ openhomedevice==2.2.0
opensensemap-api==0.2.0 opensensemap-api==0.2.0
# homeassistant.components.enigma2 # homeassistant.components.enigma2
openwebifpy==4.2.5 openwebifpy==4.2.7
# homeassistant.components.luci # homeassistant.components.luci
openwrt-luci-rpc==1.1.17 openwrt-luci-rpc==1.1.17
@ -1653,7 +1656,7 @@ py-madvr2==1.6.29
py-melissa-climate==2.1.4 py-melissa-climate==2.1.4
# homeassistant.components.nextbus # homeassistant.components.nextbus
py-nextbusnext==2.0.3 py-nextbusnext==2.0.4
# homeassistant.components.nightscout # homeassistant.components.nightscout
py-nightscout==1.2.2 py-nightscout==1.2.2
@ -1665,7 +1668,7 @@ py-schluter==0.1.7
py-sucks==0.9.10 py-sucks==0.9.10
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==2.4.4 py-synologydsm-api==2.4.5
# homeassistant.components.zabbix # homeassistant.components.zabbix
py-zabbix==1.1.7 py-zabbix==1.1.7
@ -1789,7 +1792,7 @@ pycsspeechtts==1.0.8
# pycups==1.9.73 # pycups==1.9.73
# homeassistant.components.daikin # homeassistant.components.daikin
pydaikin==2.13.2 pydaikin==2.13.4
# homeassistant.components.danfoss_air # homeassistant.components.danfoss_air
pydanfossair==0.1.0 pydanfossair==0.1.0
@ -1909,7 +1912,7 @@ pyhiveapi==0.5.16
pyhomematic==0.1.77 pyhomematic==0.1.77
# homeassistant.components.homeworks # homeassistant.components.homeworks
pyhomeworks==1.1.0 pyhomeworks==1.1.1
# homeassistant.components.ialarm # homeassistant.components.ialarm
pyialarm==2.2.0 pyialarm==2.2.0
@ -1993,7 +1996,7 @@ pylitejet==0.6.2
pylitterbot==2023.5.0 pylitterbot==2023.5.0
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.20.0 pylutron-caseta==0.21.1
# homeassistant.components.lutron # homeassistant.components.lutron
pylutron==0.2.15 pylutron==0.2.15
@ -2100,7 +2103,7 @@ pyownet==0.10.0.post1
pypca==0.0.7 pypca==0.0.7
# homeassistant.components.lcn # homeassistant.components.lcn
pypck==0.7.17 pypck==0.7.20
# homeassistant.components.pjlink # homeassistant.components.pjlink
pypjlink2==1.2.1 pypjlink2==1.2.1
@ -2160,7 +2163,7 @@ pysabnzbd==1.1.1
pysaj==0.0.16 pysaj==0.0.16
# homeassistant.components.schlage # homeassistant.components.schlage
pyschlage==2024.6.0 pyschlage==2024.8.0
# homeassistant.components.sensibo # homeassistant.components.sensibo
pysensibo==1.0.36 pysensibo==1.0.36
@ -2703,10 +2706,10 @@ switchbot-api==2.2.1
synology-srm==0.2.0 synology-srm==0.2.0
# homeassistant.components.system_bridge # homeassistant.components.system_bridge
systembridgeconnector==4.1.0 systembridgeconnector==4.1.5
# homeassistant.components.system_bridge # homeassistant.components.system_bridge
systembridgemodels==4.1.0 systembridgemodels==4.2.4
# homeassistant.components.tailscale # homeassistant.components.tailscale
tailscale==0.6.1 tailscale==0.6.1
@ -2918,7 +2921,7 @@ wiffi==1.1.2
wirelesstagpy==0.8.1 wirelesstagpy==0.8.1
# homeassistant.components.wled # homeassistant.components.wled
wled==0.20.1 wled==0.20.2
# homeassistant.components.wolflink # homeassistant.components.wolflink
wolf-comm==0.0.9 wolf-comm==0.0.9
@ -2933,7 +2936,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.30.2 xiaomi-ble==0.30.2
# homeassistant.components.knx # homeassistant.components.knx
xknx==3.0.0 xknx==3.1.0
# homeassistant.components.knx # homeassistant.components.knx
xknxproject==3.7.1 xknxproject==3.7.1
@ -2989,7 +2992,7 @@ zeroconf==0.132.2
zeversolar==0.3.1 zeversolar==0.3.1
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.30 zha==0.0.31
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.12 zhong-hong-hvac==1.0.12

View File

@ -4,7 +4,7 @@
-r requirements_test.txt -r requirements_test.txt
# homeassistant.components.aemet # homeassistant.components.aemet
AEMET-OpenData==0.5.3 AEMET-OpenData==0.5.4
# homeassistant.components.honeywell # homeassistant.components.honeywell
AIOSomecomfort==0.0.25 AIOSomecomfort==0.0.25
@ -164,7 +164,7 @@ aio-georss-gdacs==0.9
aioairq==0.3.2 aioairq==0.3.2
# homeassistant.components.airzone_cloud # homeassistant.components.airzone_cloud
aioairzone-cloud==0.6.1 aioairzone-cloud==0.6.2
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.8.1 aioairzone==0.8.1
@ -240,7 +240,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==3.2.1 aiohomekit==3.2.2
# homeassistant.components.hue # homeassistant.components.hue
aiohue==4.7.2 aiohue==4.7.2
@ -270,7 +270,7 @@ aiolookin==1.0.0
aiolyric==1.1.0 aiolyric==1.1.0
# homeassistant.components.mealie # homeassistant.components.mealie
aiomealie==0.8.0 aiomealie==0.8.1
# homeassistant.components.modern_forms # homeassistant.components.modern_forms
aiomodernforms==0.1.8 aiomodernforms==0.1.8
@ -317,7 +317,7 @@ aiopvpc==4.2.2
aiopyarr==23.4.0 aiopyarr==23.4.0
# homeassistant.components.qnap_qsw # homeassistant.components.qnap_qsw
aioqsw==0.4.0 aioqsw==0.4.1
# homeassistant.components.rainforest_raven # homeassistant.components.rainforest_raven
aioraven==0.7.0 aioraven==0.7.0
@ -332,7 +332,7 @@ aioridwell==2024.01.0
aioruckus==0.34 aioruckus==0.34
# homeassistant.components.russound_rio # homeassistant.components.russound_rio
aiorussound==2.2.2 aiorussound==2.2.3
# homeassistant.components.ruuvi_gateway # homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0 aioruuvigateway==0.1.0
@ -341,7 +341,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0 aiosenz==1.0.0
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==11.1.0 aioshelly==11.2.0
# homeassistant.components.skybell # homeassistant.components.skybell
aioskybell==22.7.0 aioskybell==22.7.0
@ -368,7 +368,7 @@ aiotankerkoenig==0.4.1
aiotractive==0.6.0 aiotractive==0.6.0
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==79 aiounifi==80
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.3.2 aiovlc==0.3.2
@ -515,7 +515,7 @@ bluecurrent-api==1.2.3
bluemaestro-ble==0.2.3 bluemaestro-ble==0.2.3
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.19.3 bluetooth-adapters==0.19.4
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==1.4.2 bluetooth-auto-recovery==1.4.2
@ -833,7 +833,7 @@ google-cloud-pubsub==2.13.11
google-generativeai==0.6.0 google-generativeai==0.6.0
# homeassistant.components.nest # homeassistant.components.nest
google-nest-sdm==4.0.5 google-nest-sdm==4.0.6
# homeassistant.components.google_travel_time # homeassistant.components.google_travel_time
googlemaps==2.5.1 googlemaps==2.5.1
@ -1015,7 +1015,7 @@ kegtron-ble==0.4.0
knocki==0.3.1 knocki==0.3.1
# homeassistant.components.knx # homeassistant.components.knx
knx-frontend==2024.8.6.211307 knx-frontend==2024.8.9.225351
# homeassistant.components.konnected # homeassistant.components.konnected
konnected==1.2.0 konnected==1.2.0
@ -1024,7 +1024,7 @@ konnected==1.2.0
krakenex==2.1.0 krakenex==2.1.0
# homeassistant.components.lacrosse_view # homeassistant.components.lacrosse_view
lacrosse-view==1.0.1 lacrosse-view==1.0.2
# homeassistant.components.laundrify # homeassistant.components.laundrify
laundrify-aio==1.2.2 laundrify-aio==1.2.2
@ -1238,7 +1238,7 @@ openerz-api==0.3.0
openhomedevice==2.2.0 openhomedevice==2.2.0
# homeassistant.components.enigma2 # homeassistant.components.enigma2
openwebifpy==4.2.5 openwebifpy==4.2.7
# homeassistant.components.opower # homeassistant.components.opower
opower==0.6.0 opower==0.6.0
@ -1345,7 +1345,7 @@ py-madvr2==1.6.29
py-melissa-climate==2.1.4 py-melissa-climate==2.1.4
# homeassistant.components.nextbus # homeassistant.components.nextbus
py-nextbusnext==2.0.3 py-nextbusnext==2.0.4
# homeassistant.components.nightscout # homeassistant.components.nightscout
py-nightscout==1.2.2 py-nightscout==1.2.2
@ -1354,7 +1354,7 @@ py-nightscout==1.2.2
py-sucks==0.9.10 py-sucks==0.9.10
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==2.4.4 py-synologydsm-api==2.4.5
# homeassistant.components.hdmi_cec # homeassistant.components.hdmi_cec
pyCEC==0.5.2 pyCEC==0.5.2
@ -1436,7 +1436,7 @@ pycoolmasternet-async==0.1.5
pycsspeechtts==1.0.8 pycsspeechtts==1.0.8
# homeassistant.components.daikin # homeassistant.components.daikin
pydaikin==2.13.2 pydaikin==2.13.4
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==116 pydeconz==116
@ -1523,7 +1523,7 @@ pyhiveapi==0.5.16
pyhomematic==0.1.77 pyhomematic==0.1.77
# homeassistant.components.homeworks # homeassistant.components.homeworks
pyhomeworks==1.1.0 pyhomeworks==1.1.1
# homeassistant.components.ialarm # homeassistant.components.ialarm
pyialarm==2.2.0 pyialarm==2.2.0
@ -1592,7 +1592,7 @@ pylitejet==0.6.2
pylitterbot==2023.5.0 pylitterbot==2023.5.0
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.20.0 pylutron-caseta==0.21.1
# homeassistant.components.lutron # homeassistant.components.lutron
pylutron==0.2.15 pylutron==0.2.15
@ -1678,7 +1678,7 @@ pyoverkiz==1.13.14
pyownet==0.10.0.post1 pyownet==0.10.0.post1
# homeassistant.components.lcn # homeassistant.components.lcn
pypck==0.7.17 pypck==0.7.20
# homeassistant.components.pjlink # homeassistant.components.pjlink
pypjlink2==1.2.1 pypjlink2==1.2.1
@ -1723,7 +1723,7 @@ pyrympro==0.0.8
pysabnzbd==1.1.1 pysabnzbd==1.1.1
# homeassistant.components.schlage # homeassistant.components.schlage
pyschlage==2024.6.0 pyschlage==2024.8.0
# homeassistant.components.sensibo # homeassistant.components.sensibo
pysensibo==1.0.36 pysensibo==1.0.36
@ -2137,10 +2137,10 @@ surepy==0.9.0
switchbot-api==2.2.1 switchbot-api==2.2.1
# homeassistant.components.system_bridge # homeassistant.components.system_bridge
systembridgeconnector==4.1.0 systembridgeconnector==4.1.5
# homeassistant.components.system_bridge # homeassistant.components.system_bridge
systembridgemodels==4.1.0 systembridgemodels==4.2.4
# homeassistant.components.tailscale # homeassistant.components.tailscale
tailscale==0.6.1 tailscale==0.6.1
@ -2301,7 +2301,7 @@ whois==0.9.27
wiffi==1.1.2 wiffi==1.1.2
# homeassistant.components.wled # homeassistant.components.wled
wled==0.20.1 wled==0.20.2
# homeassistant.components.wolflink # homeassistant.components.wolflink
wolf-comm==0.0.9 wolf-comm==0.0.9
@ -2316,7 +2316,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.30.2 xiaomi-ble==0.30.2
# homeassistant.components.knx # homeassistant.components.knx
xknx==3.0.0 xknx==3.1.0
# homeassistant.components.knx # homeassistant.components.knx
xknxproject==3.7.1 xknxproject==3.7.1
@ -2363,7 +2363,7 @@ zeroconf==0.132.2
zeversolar==0.3.1 zeversolar==0.3.1
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.30 zha==0.0.31
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.57.0 zwave-js-server-python==0.57.0

View File

@ -124,6 +124,7 @@ EXCEPTIONS = {
"PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201 "PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201
"aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138 "aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138
"aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180 "aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180
"aiohappyeyeballs", # Python-2.0.1
"aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94 "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94
"aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8 "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8
"aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6 "aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6
@ -159,7 +160,6 @@ EXCEPTIONS = {
"pyTibber", # https://github.com/Danielhiversen/pyTibber/pull/294 "pyTibber", # https://github.com/Danielhiversen/pyTibber/pull/294
"pybbox", # https://github.com/HydrelioxGitHub/pybbox/pull/5 "pybbox", # https://github.com/HydrelioxGitHub/pybbox/pull/5
"pyeconet", # https://github.com/w1ll1am23/pyeconet/pull/41 "pyeconet", # https://github.com/w1ll1am23/pyeconet/pull/41
"pylutron-caseta", # https://github.com/gurumitts/pylutron-caseta/pull/168
"pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6 "pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6
"pyvera", # https://github.com/maximvelichko/pyvera/pull/164 "pyvera", # https://github.com/maximvelichko/pyvera/pull/164
"pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11 "pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11

View File

@ -3,7 +3,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from pyblu import SyncStatus from pyblu import Status, SyncStatus
import pytest import pytest
from homeassistant.components.bluesound.const import DOMAIN from homeassistant.components.bluesound.const import DOMAIN
@ -39,6 +39,35 @@ def sync_status() -> SyncStatus:
) )
@pytest.fixture
def status() -> Status:
"""Return a status object."""
return Status(
etag="etag",
input_id=None,
service=None,
state="playing",
shuffle=False,
album=None,
artist=None,
name=None,
image=None,
volume=10,
volume_db=22.3,
mute=False,
mute_volume=None,
mute_volume_db=None,
seconds=2,
total_seconds=123.1,
can_seek=False,
sleep=0,
group_name=None,
group_volume=None,
indexing=False,
stream_url=None,
)
@pytest.fixture @pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]: def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry.""" """Override async_setup_entry."""
@ -65,7 +94,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
@pytest.fixture @pytest.fixture
def mock_player() -> Generator[AsyncMock]: def mock_player(status: Status) -> Generator[AsyncMock]:
"""Mock the player.""" """Mock the player."""
with ( with (
patch( patch(
@ -78,7 +107,7 @@ def mock_player() -> Generator[AsyncMock]:
): ):
player = mock_player.return_value player = mock_player.return_value
player.__aenter__.return_value = player player.__aenter__.return_value = player
player.status.return_value = None player.status.return_value = status
player.sync_status.return_value = SyncStatus( player.sync_status.return_value = SyncStatus(
etag="etag", etag="etag",
id="1.1.1.1:11000", id="1.1.1.1:11000",

View File

@ -41,7 +41,7 @@ async def test_user_flow_success(
async def test_user_flow_cannot_connect( async def test_user_flow_cannot_connect(
hass: HomeAssistant, mock_player: AsyncMock hass: HomeAssistant, mock_player: AsyncMock, mock_setup_entry: AsyncMock
) -> None: ) -> None:
"""Test we handle cannot connect error.""" """Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -76,6 +76,8 @@ async def test_user_flow_cannot_connect(
CONF_PORT: 11000, CONF_PORT: 11000,
} }
mock_setup_entry.assert_called_once()
async def test_user_flow_aleady_configured( async def test_user_flow_aleady_configured(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -0,0 +1,27 @@
"""Common fixture for Environment Canada tests."""
import contextlib
from datetime import datetime
import json
import pytest
from tests.common import load_fixture
@pytest.fixture
def ec_data():
"""Load Environment Canada data."""
def date_hook(weather):
"""Convert timestamp string to datetime."""
if t := weather.get("timestamp"):
with contextlib.suppress(ValueError):
weather["timestamp"] = datetime.fromisoformat(t)
return weather
return json.loads(
load_fixture("environment_canada/current_conditions_data.json"),
object_hook=date_hook,
)

View File

@ -5,35 +5,35 @@
'forecast': list([ 'forecast': list([
dict({ dict({
'condition': 'sunny', 'condition': 'sunny',
'datetime': '2022-10-04 15:00:00+00:00', 'datetime': '2022-10-04T15:00:00+00:00',
'precipitation_probability': 0, 'precipitation_probability': 0,
'temperature': 18.0, 'temperature': 18.0,
'templow': 3.0, 'templow': 3.0,
}), }),
dict({ dict({
'condition': 'sunny', 'condition': 'sunny',
'datetime': '2022-10-05 15:00:00+00:00', 'datetime': '2022-10-05T15:00:00+00:00',
'precipitation_probability': 0, 'precipitation_probability': 0,
'temperature': 20.0, 'temperature': 20.0,
'templow': 9.0, 'templow': 9.0,
}), }),
dict({ dict({
'condition': 'partlycloudy', 'condition': 'partlycloudy',
'datetime': '2022-10-06 15:00:00+00:00', 'datetime': '2022-10-06T15:00:00+00:00',
'precipitation_probability': 0, 'precipitation_probability': 0,
'temperature': 20.0, 'temperature': 20.0,
'templow': 7.0, 'templow': 7.0,
}), }),
dict({ dict({
'condition': 'rainy', 'condition': 'rainy',
'datetime': '2022-10-07 15:00:00+00:00', 'datetime': '2022-10-07T15:00:00+00:00',
'precipitation_probability': 40, 'precipitation_probability': 40,
'temperature': 13.0, 'temperature': 13.0,
'templow': 1.0, 'templow': 1.0,
}), }),
dict({ dict({
'condition': 'partlycloudy', 'condition': 'partlycloudy',
'datetime': '2022-10-08 15:00:00+00:00', 'datetime': '2022-10-08T15:00:00+00:00',
'precipitation_probability': 0, 'precipitation_probability': 0,
'temperature': 10.0, 'temperature': 10.0,
'templow': 3.0, 'templow': 3.0,
@ -48,42 +48,42 @@
'forecast': list([ 'forecast': list([
dict({ dict({
'condition': 'clear-night', 'condition': 'clear-night',
'datetime': '2022-10-03 15:00:00+00:00', 'datetime': '2022-10-03T15:00:00+00:00',
'precipitation_probability': 0, 'precipitation_probability': 0,
'temperature': None, 'temperature': None,
'templow': -1.0, 'templow': -1.0,
}), }),
dict({ dict({
'condition': 'sunny', 'condition': 'sunny',
'datetime': '2022-10-04 15:00:00+00:00', 'datetime': '2022-10-04T15:00:00+00:00',
'precipitation_probability': 0, 'precipitation_probability': 0,
'temperature': 18.0, 'temperature': 18.0,
'templow': 3.0, 'templow': 3.0,
}), }),
dict({ dict({
'condition': 'sunny', 'condition': 'sunny',
'datetime': '2022-10-05 15:00:00+00:00', 'datetime': '2022-10-05T15:00:00+00:00',
'precipitation_probability': 0, 'precipitation_probability': 0,
'temperature': 20.0, 'temperature': 20.0,
'templow': 9.0, 'templow': 9.0,
}), }),
dict({ dict({
'condition': 'partlycloudy', 'condition': 'partlycloudy',
'datetime': '2022-10-06 15:00:00+00:00', 'datetime': '2022-10-06T15:00:00+00:00',
'precipitation_probability': 0, 'precipitation_probability': 0,
'temperature': 20.0, 'temperature': 20.0,
'templow': 7.0, 'templow': 7.0,
}), }),
dict({ dict({
'condition': 'rainy', 'condition': 'rainy',
'datetime': '2022-10-07 15:00:00+00:00', 'datetime': '2022-10-07T15:00:00+00:00',
'precipitation_probability': 40, 'precipitation_probability': 40,
'temperature': 13.0, 'temperature': 13.0,
'templow': 1.0, 'templow': 1.0,
}), }),
dict({ dict({
'condition': 'partlycloudy', 'condition': 'partlycloudy',
'datetime': '2022-10-08 15:00:00+00:00', 'datetime': '2022-10-08T15:00:00+00:00',
'precipitation_probability': 0, 'precipitation_probability': 0,
'temperature': 10.0, 'temperature': 10.0,
'templow': 3.0, 'templow': 3.0,

View File

@ -1,6 +1,7 @@
"""Test Environment Canada diagnostics.""" """Test Environment Canada diagnostics."""
import json import json
from typing import Any
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -26,6 +27,7 @@ async def test_entry_diagnostics(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
ec_data: dict[str, Any],
) -> None: ) -> None:
"""Test config entry diagnostics.""" """Test config entry diagnostics."""

View File

@ -1,6 +1,7 @@
"""Test weather.""" """Test weather."""
import json import copy
from typing import Any
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
@ -12,23 +13,17 @@ from homeassistant.core import HomeAssistant
from . import init_integration from . import init_integration
from tests.common import load_fixture
async def test_forecast_daily( async def test_forecast_daily(
hass: HomeAssistant, hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any]
snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test basic forecast.""" """Test basic forecast."""
ec_data = json.loads(
load_fixture("environment_canada/current_conditions_data.json")
)
# First entry in test data is a half day; we don't want that for this test # First entry in test data is a half day; we don't want that for this test
del ec_data["daily_forecasts"][0] local_ec_data = copy.deepcopy(ec_data)
del local_ec_data["daily_forecasts"][0]
await init_integration(hass, ec_data) await init_integration(hass, local_ec_data)
response = await hass.services.async_call( response = await hass.services.async_call(
WEATHER_DOMAIN, WEATHER_DOMAIN,
@ -44,15 +39,10 @@ async def test_forecast_daily(
async def test_forecast_daily_with_some_previous_days_data( async def test_forecast_daily_with_some_previous_days_data(
hass: HomeAssistant, hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any]
snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test forecast with half day at start.""" """Test forecast with half day at start."""
ec_data = json.loads(
load_fixture("environment_canada/current_conditions_data.json")
)
await init_integration(hass, ec_data) await init_integration(hass, ec_data)
response = await hass.services.async_call( response = await hass.services.async_call(

View File

@ -7,6 +7,7 @@ from freezegun.api import FrozenDateTimeFactory
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.glances.const import DOMAIN from homeassistant.components.glances.const import DOMAIN
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -71,3 +72,40 @@ async def test_uptime_variation(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("sensor.0_0_0_0_uptime").state == "2024-02-15T12:49:52+00:00" assert hass.states.get("sensor.0_0_0_0_uptime").state == "2024-02-15T12:49:52+00:00"
async def test_sensor_removed(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_api: AsyncMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test sensor removed server side."""
# Init with reference time
freezer.move_to(MOCK_REFERENCE_DATE)
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test")
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("sensor.0_0_0_0_ssl_disk_used").state != STATE_UNAVAILABLE
assert hass.states.get("sensor.0_0_0_0_memory_use").state != STATE_UNAVAILABLE
assert hass.states.get("sensor.0_0_0_0_uptime").state != STATE_UNAVAILABLE
# Remove some sensors from Glances API data
mock_data = HA_SENSOR_DATA.copy()
mock_data.pop("fs")
mock_data.pop("mem")
mock_data.pop("uptime")
mock_api.return_value.get_ha_sensor_data = AsyncMock(return_value=mock_data)
# Server stops providing some sensors, so state should switch to Unavailable
freezer.move_to(MOCK_REFERENCE_DATE + timedelta(minutes=2))
freezer.tick(delta=timedelta(seconds=120))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("sensor.0_0_0_0_ssl_disk_used").state == STATE_UNAVAILABLE
assert hass.states.get("sensor.0_0_0_0_memory_use").state == STATE_UNAVAILABLE
assert hass.states.get("sensor.0_0_0_0_uptime").state == STATE_UNAVAILABLE

View File

@ -7,13 +7,38 @@ import voluptuous as vol
from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.const import (
BRIDGE_NAME, BRIDGE_NAME,
CONF_AUDIO_CODEC,
CONF_AUDIO_MAP,
CONF_AUDIO_PACKET_SIZE,
CONF_FEATURE, CONF_FEATURE,
CONF_FEATURE_LIST, CONF_FEATURE_LIST,
CONF_LINKED_BATTERY_SENSOR, CONF_LINKED_BATTERY_SENSOR,
CONF_LINKED_DOORBELL_SENSOR,
CONF_LINKED_MOTION_SENSOR,
CONF_LOW_BATTERY_THRESHOLD, CONF_LOW_BATTERY_THRESHOLD,
CONF_MAX_FPS,
CONF_MAX_HEIGHT,
CONF_MAX_WIDTH,
CONF_STREAM_COUNT,
CONF_SUPPORT_AUDIO,
CONF_THRESHOLD_CO, CONF_THRESHOLD_CO,
CONF_THRESHOLD_CO2, CONF_THRESHOLD_CO2,
CONF_VIDEO_CODEC,
CONF_VIDEO_MAP,
CONF_VIDEO_PACKET_SIZE,
DEFAULT_AUDIO_CODEC,
DEFAULT_AUDIO_MAP,
DEFAULT_AUDIO_PACKET_SIZE,
DEFAULT_CONFIG_FLOW_PORT, DEFAULT_CONFIG_FLOW_PORT,
DEFAULT_LOW_BATTERY_THRESHOLD,
DEFAULT_MAX_FPS,
DEFAULT_MAX_HEIGHT,
DEFAULT_MAX_WIDTH,
DEFAULT_STREAM_COUNT,
DEFAULT_SUPPORT_AUDIO,
DEFAULT_VIDEO_CODEC,
DEFAULT_VIDEO_MAP,
DEFAULT_VIDEO_PACKET_SIZE,
DOMAIN, DOMAIN,
FEATURE_ON_OFF, FEATURE_ON_OFF,
FEATURE_PLAY_PAUSE, FEATURE_PLAY_PAUSE,
@ -178,6 +203,31 @@ def test_validate_entity_config() -> None:
assert vec({"sensor.co2": {CONF_THRESHOLD_CO2: 500}}) == { assert vec({"sensor.co2": {CONF_THRESHOLD_CO2: 500}}) == {
"sensor.co2": {CONF_THRESHOLD_CO2: 500, CONF_LOW_BATTERY_THRESHOLD: 20} "sensor.co2": {CONF_THRESHOLD_CO2: 500, CONF_LOW_BATTERY_THRESHOLD: 20}
} }
assert vec(
{
"camera.demo": {
CONF_LINKED_DOORBELL_SENSOR: "event.doorbell",
CONF_LINKED_MOTION_SENSOR: "event.motion",
}
}
) == {
"camera.demo": {
CONF_LINKED_DOORBELL_SENSOR: "event.doorbell",
CONF_LINKED_MOTION_SENSOR: "event.motion",
CONF_AUDIO_CODEC: DEFAULT_AUDIO_CODEC,
CONF_SUPPORT_AUDIO: DEFAULT_SUPPORT_AUDIO,
CONF_MAX_WIDTH: DEFAULT_MAX_WIDTH,
CONF_MAX_HEIGHT: DEFAULT_MAX_HEIGHT,
CONF_MAX_FPS: DEFAULT_MAX_FPS,
CONF_AUDIO_MAP: DEFAULT_AUDIO_MAP,
CONF_VIDEO_MAP: DEFAULT_VIDEO_MAP,
CONF_STREAM_COUNT: DEFAULT_STREAM_COUNT,
CONF_VIDEO_CODEC: DEFAULT_VIDEO_CODEC,
CONF_AUDIO_PACKET_SIZE: DEFAULT_AUDIO_PACKET_SIZE,
CONF_VIDEO_PACKET_SIZE: DEFAULT_VIDEO_PACKET_SIZE,
CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD,
}
}
def test_validate_media_player_features() -> None: def test_validate_media_player_features() -> None:

View File

@ -344,10 +344,10 @@ async def test_thread_provision_migration_failed(hass: HomeAssistant) -> None:
assert config_entry.data["Connection"] == "BLE" assert config_entry.data["Connection"] == "BLE"
async def test_skip_polling_all_watchable_accessory_mode( async def test_poll_firmware_version_only_all_watchable_accessory_mode(
hass: HomeAssistant, get_next_aid: Callable[[], int] hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None: ) -> None:
"""Test that we skip polling if available and all chars are watchable accessory mode.""" """Test that we only poll firmware if available and all chars are watchable accessory mode."""
def _create_accessory(accessory): def _create_accessory(accessory):
service = accessory.add_service(ServicesTypes.LIGHTBULB, name="TestDevice") service = accessory.add_service(ServicesTypes.LIGHTBULB, name="TestDevice")
@ -370,7 +370,10 @@ async def test_skip_polling_all_watchable_accessory_mode(
# Initial state is that the light is off # Initial state is that the light is off
state = await helper.poll_and_get_state() state = await helper.poll_and_get_state()
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert mock_get_characteristics.call_count == 0 assert mock_get_characteristics.call_count == 2
# Verify only firmware version is polled
assert mock_get_characteristics.call_args_list[0][0][0] == {(1, 7)}
assert mock_get_characteristics.call_args_list[1][0][0] == {(1, 7)}
# Test device goes offline # Test device goes offline
helper.pairing.available = False helper.pairing.available = False
@ -382,16 +385,16 @@ async def test_skip_polling_all_watchable_accessory_mode(
state = await helper.poll_and_get_state() state = await helper.poll_and_get_state()
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
# Tries twice before declaring unavailable # Tries twice before declaring unavailable
assert mock_get_characteristics.call_count == 2 assert mock_get_characteristics.call_count == 4
# Test device comes back online # Test device comes back online
helper.pairing.available = True helper.pairing.available = True
state = await helper.poll_and_get_state() state = await helper.poll_and_get_state()
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert mock_get_characteristics.call_count == 3 assert mock_get_characteristics.call_count == 6
# Next poll should not happen because its a single # Next poll should not happen because its a single
# accessory, available, and all chars are watchable # accessory, available, and all chars are watchable
state = await helper.poll_and_get_state() state = await helper.poll_and_get_state()
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert mock_get_characteristics.call_count == 3 assert mock_get_characteristics.call_count == 8

View File

@ -231,6 +231,90 @@ async def test_climate_hvac_mode(
assert hass.states.get("climate.test").state == "cool" assert hass.states.get("climate.test").state == "cool"
async def test_climate_heat_cool_read_only(
hass: HomeAssistant, knx: KNXTestKit
) -> None:
"""Test KNX climate hvac mode."""
heat_cool_state_ga = "3/3/3"
await knx.setup_integration(
{
ClimateSchema.PLATFORM: {
CONF_NAME: "test",
ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3",
ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4",
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: heat_cool_state_ga,
}
}
)
# read states state updater
# StateUpdater semaphore allows 2 concurrent requests
await knx.assert_read("1/2/3")
await knx.assert_read("1/2/5")
# StateUpdater initialize state
await knx.receive_response("1/2/3", RAW_FLOAT_20_0)
await knx.receive_response("1/2/5", RAW_FLOAT_20_0)
await knx.assert_read(heat_cool_state_ga)
await knx.receive_response(heat_cool_state_ga, True) # heat
state = hass.states.get("climate.test")
assert state.state == "heat"
assert state.attributes["hvac_modes"] == ["heat"]
assert state.attributes["hvac_action"] == "heating"
await knx.receive_write(heat_cool_state_ga, False) # cool
state = hass.states.get("climate.test")
assert state.state == "cool"
assert state.attributes["hvac_modes"] == ["cool"]
assert state.attributes["hvac_action"] == "cooling"
async def test_climate_heat_cool_read_only_on_off(
hass: HomeAssistant, knx: KNXTestKit
) -> None:
"""Test KNX climate hvac mode."""
on_off_ga = "2/2/2"
heat_cool_state_ga = "3/3/3"
await knx.setup_integration(
{
ClimateSchema.PLATFORM: {
CONF_NAME: "test",
ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3",
ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4",
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_ON_OFF_ADDRESS: on_off_ga,
ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: heat_cool_state_ga,
}
}
)
# read states state updater
# StateUpdater semaphore allows 2 concurrent requests
await knx.assert_read("1/2/3")
await knx.assert_read("1/2/5")
# StateUpdater initialize state
await knx.receive_response("1/2/3", RAW_FLOAT_20_0)
await knx.receive_response("1/2/5", RAW_FLOAT_20_0)
await knx.assert_read(heat_cool_state_ga)
await knx.receive_response(heat_cool_state_ga, True) # heat
state = hass.states.get("climate.test")
assert state.state == "off"
assert set(state.attributes["hvac_modes"]) == {"off", "heat"}
assert state.attributes["hvac_action"] == "off"
await knx.receive_write(heat_cool_state_ga, False) # cool
state = hass.states.get("climate.test")
assert state.state == "off"
assert set(state.attributes["hvac_modes"]) == {"off", "cool"}
assert state.attributes["hvac_action"] == "off"
await knx.receive_write(on_off_ga, True)
state = hass.states.get("climate.test")
assert state.state == "cool"
assert set(state.attributes["hvac_modes"]) == {"off", "cool"}
assert state.attributes["hvac_action"] == "cooling"
async def test_climate_preset_mode( async def test_climate_preset_mode(
hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry
) -> None: ) -> None:

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest
from xknx.core import XknxConnectionState from xknx.core import XknxConnectionState
from xknx.devices.light import Light as XknxLight from xknx.devices.light import Light as XknxLight
@ -1174,3 +1175,46 @@ async def test_light_ui_create(
await knx.receive_response("2/2/2", True) await knx.receive_response("2/2/2", True)
state = hass.states.get("light.test") state = hass.states.get("light.test")
assert state.state is STATE_ON assert state.state is STATE_ON
@pytest.mark.parametrize(
("color_temp_mode", "raw_ct"),
[
("7.600", (0x10, 0x68)),
("9", (0x46, 0x69)),
("5.001", (0x74,)),
],
)
async def test_light_ui_color_temp(
hass: HomeAssistant,
knx: KNXTestKit,
create_ui_entity: KnxEntityGenerator,
color_temp_mode: str,
raw_ct: tuple[int, ...],
) -> None:
"""Test creating a switch."""
await knx.setup_integration({})
await create_ui_entity(
platform=Platform.LIGHT,
entity_data={"name": "test"},
knx_data={
"ga_switch": {"write": "1/1/1", "state": "2/2/2"},
"ga_color_temp": {
"write": "3/3/3",
"dpt": color_temp_mode,
},
"_light_color_mode_schema": "default",
"sync_state": True,
},
)
await knx.assert_read("2/2/2", True)
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": "light.test", ATTR_COLOR_TEMP_KELVIN: 4200},
blocking=True,
)
await knx.assert_write("3/3/3", raw_ct)
state = hass.states.get("light.test")
assert state.state is STATE_ON
assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx(4200, abs=1)

View File

@ -487,6 +487,7 @@ async def test_if_fires_on_button_event_late_setup(
}, },
) )
with patch("homeassistant.components.lutron_caseta.Smartbridge.create_tls"):
await hass.config_entries.async_setup(config_entry_id) await hass.config_entries.async_setup(config_entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -93,3 +93,16 @@ async def test_sensor_setup_and_states(
# test get_temperature ValueError # test get_temperature ValueError
assert get_temperature(None, "temp_key") is None assert get_temperature(None, "temp_key") is None
# test startup placeholder values
update_callback({"outgoing_bit_depth": "0bit"})
await hass.async_block_till_done()
assert (
hass.states.get("sensor.madvr_envy_outgoing_bit_depth").state == STATE_UNKNOWN
)
update_callback({"outgoing_color_space": "?"})
await hass.async_block_till_done()
assert (
hass.states.get("sensor.madvr_envy_outgoing_color_space").state == STATE_UNKNOWN
)

View File

@ -33,7 +33,7 @@ ZERO_DATA = {
"unique_domains": 0, "unique_domains": 0,
} }
SAMPLE_VERSIONS = { SAMPLE_VERSIONS_WITH_UPDATES = {
"core_current": "v5.5", "core_current": "v5.5",
"core_latest": "v5.6", "core_latest": "v5.6",
"core_update": True, "core_update": True,
@ -45,6 +45,18 @@ SAMPLE_VERSIONS = {
"FTL_update": True, "FTL_update": True,
} }
SAMPLE_VERSIONS_NO_UPDATES = {
"core_current": "v5.5",
"core_latest": "v5.5",
"core_update": False,
"web_current": "v5.7",
"web_latest": "v5.7",
"web_update": False,
"FTL_current": "v5.10",
"FTL_latest": "v5.10",
"FTL_update": False,
}
HOST = "1.2.3.4" HOST = "1.2.3.4"
PORT = 80 PORT = 80
LOCATION = "location" LOCATION = "location"
@ -103,7 +115,9 @@ CONFIG_ENTRY_WITHOUT_API_KEY = {
SWITCH_ENTITY_ID = "switch.pi_hole" SWITCH_ENTITY_ID = "switch.pi_hole"
def _create_mocked_hole(raise_exception=False, has_versions=True, has_data=True): def _create_mocked_hole(
raise_exception=False, has_versions=True, has_update=True, has_data=True
):
mocked_hole = MagicMock() mocked_hole = MagicMock()
type(mocked_hole).get_data = AsyncMock( type(mocked_hole).get_data = AsyncMock(
side_effect=HoleError("") if raise_exception else None side_effect=HoleError("") if raise_exception else None
@ -118,7 +132,10 @@ def _create_mocked_hole(raise_exception=False, has_versions=True, has_data=True)
else: else:
mocked_hole.data = [] mocked_hole.data = []
if has_versions: if has_versions:
mocked_hole.versions = SAMPLE_VERSIONS if has_update:
mocked_hole.versions = SAMPLE_VERSIONS_WITH_UPDATES
else:
mocked_hole.versions = SAMPLE_VERSIONS_NO_UPDATES
else: else:
mocked_hole.versions = None mocked_hole.versions = None
return mocked_hole return mocked_hole

View File

@ -96,7 +96,7 @@ async def test_flow_user_without_api_key(hass: HomeAssistant) -> None:
async def test_flow_user_invalid(hass: HomeAssistant) -> None: async def test_flow_user_invalid(hass: HomeAssistant) -> None:
"""Test user initialized flow with invalid server.""" """Test user initialized flow with invalid server."""
mocked_hole = _create_mocked_hole(True) mocked_hole = _create_mocked_hole(raise_exception=True)
with _patch_config_flow_hole(mocked_hole): with _patch_config_flow_hole(mocked_hole):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER

View File

@ -1,7 +1,7 @@
"""Test pi_hole component.""" """Test pi_hole component."""
from homeassistant.components import pi_hole from homeassistant.components import pi_hole
from homeassistant.const import STATE_ON, STATE_UNKNOWN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import CONFIG_DATA_DEFAULTS, _create_mocked_hole, _patch_init_hole from . import CONFIG_DATA_DEFAULTS, _create_mocked_hole, _patch_init_hole
@ -80,3 +80,44 @@ async def test_update_no_versions(hass: HomeAssistant) -> None:
assert state.attributes["installed_version"] is None assert state.attributes["installed_version"] is None
assert state.attributes["latest_version"] is None assert state.attributes["latest_version"] is None
assert state.attributes["release_url"] is None assert state.attributes["release_url"] is None
async def test_update_no_updates(hass: HomeAssistant) -> None:
"""Tests update entity when no latest data available."""
mocked_hole = _create_mocked_hole(has_versions=True, has_update=False)
entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA_DEFAULTS)
entry.add_to_hass(hass)
with _patch_init_hole(mocked_hole):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("update.pi_hole_core_update_available")
assert state.name == "Pi-Hole Core update available"
assert state.state == STATE_OFF
assert state.attributes["installed_version"] == "v5.5"
assert state.attributes["latest_version"] == "v5.5"
assert (
state.attributes["release_url"]
== "https://github.com/pi-hole/pi-hole/releases/tag/v5.5"
)
state = hass.states.get("update.pi_hole_ftl_update_available")
assert state.name == "Pi-Hole FTL update available"
assert state.state == STATE_OFF
assert state.attributes["installed_version"] == "v5.10"
assert state.attributes["latest_version"] == "v5.10"
assert (
state.attributes["release_url"]
== "https://github.com/pi-hole/FTL/releases/tag/v5.10"
)
state = hass.states.get("update.pi_hole_web_update_available")
assert state.name == "Pi-Hole Web update available"
assert state.state == STATE_OFF
assert state.attributes["installed_version"] == "v5.7"
assert state.attributes["latest_version"] == "v5.7"
assert (
state.attributes["release_url"]
== "https://github.com/pi-hole/AdminLTE/releases/tag/v5.7"
)

View File

@ -831,9 +831,9 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None:
""" """
constraints_to_recreate = ( constraints_to_recreate = (
("events", "data_id"), ("events", "data_id", "event_data", "data_id"),
("states", "event_id"), # This won't be found ("states", "event_id", None, None), # This won't be found
("states", "old_state_id"), ("states", "old_state_id", "states", "state_id"),
) )
db_engine = recorder_db_url.partition("://")[0] db_engine = recorder_db_url.partition("://")[0]
@ -902,7 +902,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None:
session_maker = Mock(return_value=session) session_maker = Mock(return_value=session)
dropped_constraints_1 = [ dropped_constraints_1 = [
dropped_constraint dropped_constraint
for table, column in constraints_to_recreate for table, column, _, _ in constraints_to_recreate
for dropped_constraint in migration._drop_foreign_key_constraints( for dropped_constraint in migration._drop_foreign_key_constraints(
session_maker, engine, table, column session_maker, engine, table, column
)[1] )[1]
@ -914,7 +914,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None:
session_maker = Mock(return_value=session) session_maker = Mock(return_value=session)
dropped_constraints_2 = [ dropped_constraints_2 = [
dropped_constraint dropped_constraint
for table, column in constraints_to_recreate for table, column, _, _ in constraints_to_recreate
for dropped_constraint in migration._drop_foreign_key_constraints( for dropped_constraint in migration._drop_foreign_key_constraints(
session_maker, engine, table, column session_maker, engine, table, column
)[1] )[1]
@ -925,7 +925,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None:
with Session(engine) as session: with Session(engine) as session:
session_maker = Mock(return_value=session) session_maker = Mock(return_value=session)
migration._restore_foreign_key_constraints( migration._restore_foreign_key_constraints(
session_maker, engine, dropped_constraints_1 session_maker, engine, constraints_to_recreate
) )
# Check we do find the constrained columns again (they are restored) # Check we do find the constrained columns again (they are restored)
@ -933,7 +933,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None:
session_maker = Mock(return_value=session) session_maker = Mock(return_value=session)
dropped_constraints_3 = [ dropped_constraints_3 = [
dropped_constraint dropped_constraint
for table, column in constraints_to_recreate for table, column, _, _ in constraints_to_recreate
for dropped_constraint in migration._drop_foreign_key_constraints( for dropped_constraint in migration._drop_foreign_key_constraints(
session_maker, engine, table, column session_maker, engine, table, column
)[1] )[1]
@ -951,21 +951,7 @@ def test_restore_foreign_key_constraints_with_error(
This is not supported on SQLite This is not supported on SQLite
""" """
constraints_to_restore = [ constraints_to_restore = [("events", "data_id", "event_data", "data_id")]
(
"events",
"data_id",
{
"comment": None,
"constrained_columns": ["data_id"],
"name": "events_data_id_fkey",
"options": {},
"referred_columns": ["data_id"],
"referred_schema": None,
"referred_table": "event_data",
},
),
]
connection = Mock() connection = Mock()
connection.execute = Mock(side_effect=InternalError(None, None, None)) connection.execute = Mock(side_effect=InternalError(None, None, None))
@ -981,3 +967,88 @@ def test_restore_foreign_key_constraints_with_error(
) )
assert "Could not update foreign options in events table" in caplog.text assert "Could not update foreign options in events table" in caplog.text
@pytest.mark.skip_on_db_engine(["sqlite"])
@pytest.mark.usefixtures("skip_by_db_engine")
def test_restore_foreign_key_constraints_with_integrity_error(
recorder_db_url: str,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test we can drop and then restore foreign keys.
This is not supported on SQLite
"""
constraints = (
("events", "data_id", "event_data", "data_id", Events),
("states", "old_state_id", "states", "state_id", States),
)
engine = create_engine(recorder_db_url)
db_schema.Base.metadata.create_all(engine)
# Drop constraints
with Session(engine) as session:
session_maker = Mock(return_value=session)
for table, column, _, _, _ in constraints:
migration._drop_foreign_key_constraints(
session_maker, engine, table, column
)
# Add rows violating the constraints
with Session(engine) as session:
for _, column, _, _, table_class in constraints:
session.add(table_class(**{column: 123}))
session.add(table_class())
# Insert a States row referencing the row with an invalid foreign reference
session.add(States(old_state_id=1))
session.commit()
# Check we could insert the rows
with Session(engine) as session:
assert session.query(Events).count() == 2
assert session.query(States).count() == 3
# Restore constraints
to_restore = [
(table, column, foreign_table, foreign_column)
for table, column, foreign_table, foreign_column, _ in constraints
]
with Session(engine) as session:
session_maker = Mock(return_value=session)
migration._restore_foreign_key_constraints(session_maker, engine, to_restore)
# Check the violating row has been deleted from the Events table
with Session(engine) as session:
assert session.query(Events).count() == 1
assert session.query(States).count() == 3
engine.dispose()
assert (
"Could not update foreign options in events table, "
"will delete violations and try again"
) in caplog.text
def test_delete_foreign_key_violations_unsupported_engine(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test calling _delete_foreign_key_violations with an unsupported engine."""
connection = Mock()
connection.execute = Mock(side_effect=InternalError(None, None, None))
session = Mock()
session.connection = Mock(return_value=connection)
instance = Mock()
instance.get_session = Mock(return_value=session)
engine = Mock()
engine.dialect = Mock()
engine.dialect.name = "sqlite"
session_maker = Mock(return_value=session)
with pytest.raises(
RuntimeError, match="_delete_foreign_key_violations not supported for sqlite"
):
migration._delete_foreign_key_violations(session_maker, engine, "", "", "", "")

View File

@ -86,10 +86,18 @@ async def test_setup_host(hass: HomeAssistant, device, device2, main_zone) -> No
assert state.state == "off" assert state.state == "off"
async def test_setup_attribute_error(hass: HomeAssistant, device, main_zone) -> None: @pytest.mark.parametrize(
"""Test set up integration encountering an Attribute Error.""" ("error"),
[
AttributeError,
ValueError,
UnicodeDecodeError("", b"", 1, 0, ""),
],
)
async def test_setup_find_errors(hass: HomeAssistant, device, main_zone, error) -> None:
"""Test set up integration encountering an Error."""
with patch("rxv.find", side_effect=AttributeError): with patch("rxv.find", side_effect=error):
assert await async_setup_component(hass, MP_DOMAIN, CONFIG) assert await async_setup_component(hass, MP_DOMAIN, CONFIG)
await hass.async_block_till_done() await hass.async_block_till_done()