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",
"iot_class": "cloud_polling",
"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:
await airzone.get_version()
except AirzoneError as err:
except (AirzoneError, TimeoutError) as err:
raise AbortFlow("cannot_connect") from err
return await self.async_step_discovered_connection()

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"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._inputs: list[Input] = []
self._presets: list[Preset] = []
self._is_online = False
self._muted = False
self._master: BluesoundPlayer | None = None
self._is_master = False
@ -312,20 +311,24 @@ class BluesoundPlayer(MediaPlayerEntity):
async def _start_poll_command(self):
"""Loop which polls the status of the player."""
try:
while True:
while True:
try:
await self.async_update_status()
except (TimeoutError, ClientError):
_LOGGER.error("Node %s:%s is offline, retrying later", self.host, self.port)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
self.start_polling()
except CancelledError:
_LOGGER.debug("Stopping the polling of node %s:%s", self.host, self.port)
except Exception:
_LOGGER.exception("Unexpected error in %s:%s", self.host, self.port)
raise
except (TimeoutError, ClientError):
_LOGGER.error(
"Node %s:%s is offline, retrying later", self.host, self.port
)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
except CancelledError:
_LOGGER.debug(
"Stopping the polling of node %s:%s", self.host, self.port
)
return
except Exception:
_LOGGER.exception(
"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:
"""Start the polling task."""
@ -348,7 +351,7 @@ class BluesoundPlayer(MediaPlayerEntity):
async def async_update(self) -> None:
"""Update internal status of the entity."""
if not self._is_online:
if not self.available:
return
with suppress(TimeoutError):
@ -365,7 +368,7 @@ class BluesoundPlayer(MediaPlayerEntity):
try:
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._status = status
@ -394,7 +397,7 @@ class BluesoundPlayer(MediaPlayerEntity):
self.async_write_ha_state()
except (TimeoutError, ClientError):
self._is_online = False
self._attr_available = False
self._last_status_update = None
self._status = None
self.async_write_ha_state()

View File

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

View File

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

View File

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

View File

@ -2,9 +2,8 @@
"domain": "concord232",
"name": "Concord232",
"codeowners": [],
"disabled": "This integration is disabled because it uses non-open source code to operate.",
"documentation": "https://www.home-assistant.io/integrations/concord232",
"iot_class": "local_polling",
"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",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.13.2"],
"requirements": ["pydaikin==2.13.4"],
"zeroconf": ["_dkapi._tcp.local."]
}

View File

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

View File

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

View File

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

View File

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

View File

@ -45,15 +45,13 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except exceptions.GlancesApiError as err:
raise UpdateFailed from err
# Update computed values
uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None
uptime: datetime | 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
# new uptime (i.e. server restarted)
if (
self.data is None
or self.data["computed"]["uptime_duration"] > up_duration
):
if uptime is None or self.data["computed"]["uptime_duration"] > up_duration:
uptime = utcnow() - up_duration
data["computed"] = {"uptime_duration": up_duration, "uptime": uptime}
return data or {}

View File

@ -325,6 +325,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
entity_description: GlancesSensorEntityDescription
_attr_has_entity_name = True
_data_valid: bool = False
def __init__(
self,
@ -351,14 +352,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
@property
def available(self) -> bool:
"""Set sensor unavailable when native value is invalid."""
if super().available:
return (
not self._numeric_state_expected
or isinstance(value := self.native_value, (int, float))
or isinstance(value, str)
and value.isnumeric()
)
return False
return super().available and self._data_valid
@callback
def _handle_coordinator_update(self) -> None:
@ -368,10 +362,19 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
def _update_native_value(self) -> None:
"""Update sensor native value from coordinator data."""
data = self.coordinator.data[self.entity_description.type]
if dict_val := data.get(self._sensor_label):
data = self.coordinator.data.get(self.entity_description.type)
if data and (dict_val := data.get(self._sensor_label)):
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)
else:
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": {
"title": "Integration {domain} not found",
"fix_flow": {
"abort": {
"issue_ignored": "Not existing integration {domain} ignored."
},
"step": {
"remove_entries": {
"init": {
"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.",
"menu_options": {

View File

@ -22,6 +22,7 @@ from homeassistant.components import (
sensor,
)
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.media_player import (
DOMAIN as MEDIA_PLAYER_DOMAIN,
@ -167,9 +168,11 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
vol.Optional(
CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE
): 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(
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:
"""Poll state of all entities attached to this bridge/accessory."""
to_poll = self.pollable_characteristics
accessories = self.entity_map.accessories
if (
len(self.entity_map.accessories) == 1
len(accessories) == 1
and self.available
and not (self.pollable_characteristics - self.watchable_characteristics)
and not (to_poll - self.watchable_characteristics)
and self.pairing.is_available
and await self.pairing.controller.async_reachable(
self.unique_id, timeout=5.0
)
):
# If its a single accessory and all chars are watchable,
# we don't need to poll.
_LOGGER.debug("Accessory is reachable, skip polling: %s", self.unique_id)
return
# only poll the firmware version to keep the connection alive
# https://github.com/home-assistant/core/issues/123412
#
# 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()
_LOGGER.debug(
"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)
try:
new_values_dict = await self.get_characteristics(
self.pollable_characteristics
)
new_values_dict = await self.get_characteristics(to_poll)
except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not
# visible on the network.

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homeworks",
"iot_class": "local_push",
"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:
_LOGGER.error(
"There was an issue sending the notification %s: %s",
response.status,
response.status_code,
response.text,
)

View File

@ -147,18 +147,10 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Start the KNX integration."""
hass.data[DATA_HASS_CONFIG] = config
conf: ConfigType | None = config.get(DOMAIN)
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
if (conf := config.get(DOMAIN)) is not None:
hass.data[DATA_KNX_CONFIG] = dict(conf)
register_knx_services(hass)
return True

View File

@ -5,7 +5,11 @@ from __future__ import annotations
from typing import Any
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 homeassistant import config_entries
@ -241,12 +245,9 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
if self._device.supports_on_off and not self._device.is_on:
return HVACMode.OFF
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
)
if hvac_mode is not HVACMode.OFF:
self._last_hvac_mode = hvac_mode
return hvac_mode
return self.default_hvac_mode
@property
@ -261,11 +262,15 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
if self._device.supports_on_off:
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)
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
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.xknx.devices.async_remove(self._device.mode)
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
color_temperature_type = ColorTemperatureType.UINT_2_BYTE
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_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_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_dpt = get_dpt(CONF_GA_COLOR)

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/lacrosse_view",
"iot_class": "cloud_polling",
"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",
"iot_class": "local_push",
"loggers": ["pypck"],
"requirements": ["pypck==0.7.17"]
"requirements": ["pypck==0.7.20"]
}

View File

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

View File

@ -277,4 +277,15 @@ class MadvrSensor(MadVREntity, SensorEntity):
@property
def native_value(self) -> float | str | None:
"""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",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["aiomealie==0.8.0"]
"requirements": ["aiomealie==0.8.1"]
}

View File

@ -20,5 +20,5 @@
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"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",
"iot_class": "cloud_polling",
"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
latest_version: Callable[[dict], str | None] = lambda api: None
has_update: Callable[[dict], bool | None] = lambda api: None
release_base_url: str | None = None
title: str | None = None
@ -34,6 +35,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
installed_version=lambda versions: versions.get("core_current"),
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",
),
PiHoleUpdateEntityDescription(
@ -43,6 +45,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
installed_version=lambda versions: versions.get("web_current"),
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",
),
PiHoleUpdateEntityDescription(
@ -52,6 +55,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
installed_version=lambda versions: versions.get("FTL_current"),
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",
),
)
@ -110,7 +114,9 @@ class PiHoleUpdateEntity(PiHoleEntity, UpdateEntity):
def latest_version(self) -> str | None:
"""Latest version available for install."""
if isinstance(self.api.versions, dict):
return self.entity_description.latest_version(self.api.versions)
if self.entity_description.has_update(self.api.versions):
return self.entity_description.latest_version(self.api.versions)
return self.installed_version
return None
@property

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
"iot_class": "local_polling",
"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."""
SCHEMA_VERSION = 44
SCHEMA_VERSION = 45
_LOGGER = logging.getLogger(__name__)

View File

@ -669,33 +669,177 @@ def _drop_foreign_key_constraints(
def _restore_foreign_key_constraints(
session_maker: Callable[[], Session],
engine: Engine,
dropped_constraints: list[tuple[str, str, ReflectedForeignKeyConstraint]],
foreign_columns: list[tuple[str, str, str | None, str | None]],
) -> None:
"""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
for constraint in constraints:
if constraint.column_keys == [column]:
break
else:
_LOGGER.info(
"Did not find a matching constraint for %s", dropped_constraint
)
_LOGGER.info("Did not find a matching constraint for %s.%s", table, column)
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
# 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
create_rule = constraint._create_rule # noqa: SLF001
add_constraint = AddConstraint(constraint) # type: ignore[no-untyped-call]
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)
with session_scope(session=session_maker()) as session:
try:
connection = session.connection()
connection.execute(add_constraint)
except (InternalError, OperationalError):
_LOGGER.exception("Could not update foreign options in %s table", table)
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:
try:
connection = session.connection()
connection.execute(add_constraint)
except (InternalError, OperationalError):
_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)
@ -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):
def _apply_update(self) -> None:
"""Version specific update method."""
@ -1471,24 +1647,14 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44):
else ""
)
# First drop foreign key constraints
foreign_columns = (
("events", ("data_id", "event_type_id")),
("states", ("event_id", "old_state_id", "attributes_id", "metadata_id")),
("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
)[1]
]
_LOGGER.debug("Dropped foreign key constraints: %s", dropped_constraints)
for table, columns, _ in FOREIGN_COLUMNS:
for column in columns:
_drop_foreign_key_constraints(
self.session_maker, self.engine, table, column
)
# Then modify the constrained columns
for table, columns in foreign_columns:
for table, columns, _ in FOREIGN_COLUMNS:
_modify_columns(
self.session_maker,
self.engine,
@ -1518,9 +1684,24 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44):
table,
[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(
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",
"iot_class": "local_push",
"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._sources = sources
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(
# Use MAC address of Russound device as identifier
identifiers={(DOMAIN, self._controller.mac_address)},
connections={(CONNECTION_NETWORK_MAC, self._controller.mac_address)},
identifiers={(DOMAIN, device_identifier)},
manufacturer="Russound",
name=self._controller.controller_type,
model=self._controller.controller_type,
@ -143,6 +150,10 @@ class RussoundZoneDevice(MediaPlayerEntity):
DOMAIN,
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():
if flag in zone.instance.supported_features:
self._attr_supported_features |= feature

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,6 @@
"iot_class": "local_push",
"loggers": ["systembridgeconnector"],
"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."]
}

View File

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

View File

@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["aiounifi"],
"quality_scale": "platinum",
"requirements": ["aiounifi==79"],
"requirements": ["aiounifi==80"],
"ssdp": [
{
"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.entity_platform import AddEntitiesCallback
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -62,9 +60,8 @@ class WolButton(ButtonEntity):
self._attr_unique_id = dr.format_mac(mac_address)
self._attr_device_info = dr.DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, self._attr_unique_id)},
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer="Wake on LAN",
name=name,
default_manufacturer="Wake on LAN",
default_name=name,
)
async def async_press(self) -> None:

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@
"zha",
"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": [
{
"vid": "10C4",

View File

@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
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
aiodns==3.2.0
aiohttp-fast-zlib==0.1.1
aiohttp==3.10.2
aiohttp==3.10.3
aiohttp_cors==0.7.0
aiozoneinfo==0.2.1
astral==2.2
@ -16,7 +16,7 @@ awesomeversion==24.6.0
bcrypt==4.1.3
bleak-retry-connector==3.5.0
bleak==0.22.2
bluetooth-adapters==0.19.3
bluetooth-adapters==0.19.4
bluetooth-auto-recovery==1.4.2
bluetooth-data-tools==1.19.4
cached_ipaddress==0.3.0

View File

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

View File

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

View File

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

View File

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

View File

@ -124,6 +124,7 @@ EXCEPTIONS = {
"PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201
"aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138
"aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180
"aiohappyeyeballs", # Python-2.0.1
"aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94
"aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8
"aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6
@ -159,7 +160,6 @@ EXCEPTIONS = {
"pyTibber", # https://github.com/Danielhiversen/pyTibber/pull/294
"pybbox", # https://github.com/HydrelioxGitHub/pybbox/pull/5
"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
"pyvera", # https://github.com/maximvelichko/pyvera/pull/164
"pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11

View File

@ -3,7 +3,7 @@
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from pyblu import SyncStatus
from pyblu import Status, SyncStatus
import pytest
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
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
@ -65,7 +94,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
@pytest.fixture
def mock_player() -> Generator[AsyncMock]:
def mock_player(status: Status) -> Generator[AsyncMock]:
"""Mock the player."""
with (
patch(
@ -78,7 +107,7 @@ def mock_player() -> Generator[AsyncMock]:
):
player = mock_player.return_value
player.__aenter__.return_value = player
player.status.return_value = None
player.status.return_value = status
player.sync_status.return_value = SyncStatus(
etag="etag",
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(
hass: HomeAssistant, mock_player: AsyncMock
hass: HomeAssistant, mock_player: AsyncMock, mock_setup_entry: AsyncMock
) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
@ -76,6 +76,8 @@ async def test_user_flow_cannot_connect(
CONF_PORT: 11000,
}
mock_setup_entry.assert_called_once()
async def test_user_flow_aleady_configured(
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([
dict({
'condition': 'sunny',
'datetime': '2022-10-04 15:00:00+00:00',
'datetime': '2022-10-04T15:00:00+00:00',
'precipitation_probability': 0,
'temperature': 18.0,
'templow': 3.0,
}),
dict({
'condition': 'sunny',
'datetime': '2022-10-05 15:00:00+00:00',
'datetime': '2022-10-05T15:00:00+00:00',
'precipitation_probability': 0,
'temperature': 20.0,
'templow': 9.0,
}),
dict({
'condition': 'partlycloudy',
'datetime': '2022-10-06 15:00:00+00:00',
'datetime': '2022-10-06T15:00:00+00:00',
'precipitation_probability': 0,
'temperature': 20.0,
'templow': 7.0,
}),
dict({
'condition': 'rainy',
'datetime': '2022-10-07 15:00:00+00:00',
'datetime': '2022-10-07T15:00:00+00:00',
'precipitation_probability': 40,
'temperature': 13.0,
'templow': 1.0,
}),
dict({
'condition': 'partlycloudy',
'datetime': '2022-10-08 15:00:00+00:00',
'datetime': '2022-10-08T15:00:00+00:00',
'precipitation_probability': 0,
'temperature': 10.0,
'templow': 3.0,
@ -48,42 +48,42 @@
'forecast': list([
dict({
'condition': 'clear-night',
'datetime': '2022-10-03 15:00:00+00:00',
'datetime': '2022-10-03T15:00:00+00:00',
'precipitation_probability': 0,
'temperature': None,
'templow': -1.0,
}),
dict({
'condition': 'sunny',
'datetime': '2022-10-04 15:00:00+00:00',
'datetime': '2022-10-04T15:00:00+00:00',
'precipitation_probability': 0,
'temperature': 18.0,
'templow': 3.0,
}),
dict({
'condition': 'sunny',
'datetime': '2022-10-05 15:00:00+00:00',
'datetime': '2022-10-05T15:00:00+00:00',
'precipitation_probability': 0,
'temperature': 20.0,
'templow': 9.0,
}),
dict({
'condition': 'partlycloudy',
'datetime': '2022-10-06 15:00:00+00:00',
'datetime': '2022-10-06T15:00:00+00:00',
'precipitation_probability': 0,
'temperature': 20.0,
'templow': 7.0,
}),
dict({
'condition': 'rainy',
'datetime': '2022-10-07 15:00:00+00:00',
'datetime': '2022-10-07T15:00:00+00:00',
'precipitation_probability': 40,
'temperature': 13.0,
'templow': 1.0,
}),
dict({
'condition': 'partlycloudy',
'datetime': '2022-10-08 15:00:00+00:00',
'datetime': '2022-10-08T15:00:00+00:00',
'precipitation_probability': 0,
'temperature': 10.0,
'templow': 3.0,

View File

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

View File

@ -1,6 +1,7 @@
"""Test weather."""
import json
import copy
from typing import Any
from syrupy.assertion import SnapshotAssertion
@ -12,23 +13,17 @@ from homeassistant.core import HomeAssistant
from . import init_integration
from tests.common import load_fixture
async def test_forecast_daily(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any]
) -> None:
"""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
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(
WEATHER_DOMAIN,
@ -44,15 +39,10 @@ async def test_forecast_daily(
async def test_forecast_daily_with_some_previous_days_data(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any]
) -> None:
"""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)
response = await hass.services.async_call(

View File

@ -7,6 +7,7 @@ from freezegun.api import FrozenDateTimeFactory
from syrupy import SnapshotAssertion
from homeassistant.components.glances.const import DOMAIN
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -71,3 +72,40 @@ async def test_uptime_variation(
async_fire_time_changed(hass)
await hass.async_block_till_done()
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 (
BRIDGE_NAME,
CONF_AUDIO_CODEC,
CONF_AUDIO_MAP,
CONF_AUDIO_PACKET_SIZE,
CONF_FEATURE,
CONF_FEATURE_LIST,
CONF_LINKED_BATTERY_SENSOR,
CONF_LINKED_DOORBELL_SENSOR,
CONF_LINKED_MOTION_SENSOR,
CONF_LOW_BATTERY_THRESHOLD,
CONF_MAX_FPS,
CONF_MAX_HEIGHT,
CONF_MAX_WIDTH,
CONF_STREAM_COUNT,
CONF_SUPPORT_AUDIO,
CONF_THRESHOLD_CO,
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_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,
FEATURE_ON_OFF,
FEATURE_PLAY_PAUSE,
@ -178,6 +203,31 @@ def test_validate_entity_config() -> None:
assert vec({"sensor.co2": {CONF_THRESHOLD_CO2: 500}}) == {
"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:

View File

@ -344,10 +344,10 @@ async def test_thread_provision_migration_failed(hass: HomeAssistant) -> None:
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]
) -> 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):
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
state = await helper.poll_and_get_state()
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
helper.pairing.available = False
@ -382,16 +385,16 @@ async def test_skip_polling_all_watchable_accessory_mode(
state = await helper.poll_and_get_state()
assert state.state == STATE_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
helper.pairing.available = True
state = await helper.poll_and_get_state()
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
# accessory, available, and all chars are watchable
state = await helper.poll_and_get_state()
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"
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(
hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry
) -> None:

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import timedelta
from freezegun.api import FrozenDateTimeFactory
import pytest
from xknx.core import XknxConnectionState
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)
state = hass.states.get("light.test")
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,8 +487,9 @@ async def test_if_fires_on_button_event_late_setup(
},
)
await hass.config_entries.async_setup(config_entry_id)
await hass.async_block_till_done()
with patch("homeassistant.components.lutron_caseta.Smartbridge.create_tls"):
await hass.config_entries.async_setup(config_entry_id)
await hass.async_block_till_done()
message = {
ATTR_SERIAL: device.get("serial"),

View File

@ -93,3 +93,16 @@ async def test_sensor_setup_and_states(
# test get_temperature ValueError
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,
}
SAMPLE_VERSIONS = {
SAMPLE_VERSIONS_WITH_UPDATES = {
"core_current": "v5.5",
"core_latest": "v5.6",
"core_update": True,
@ -45,6 +45,18 @@ SAMPLE_VERSIONS = {
"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"
PORT = 80
LOCATION = "location"
@ -103,7 +115,9 @@ CONFIG_ENTRY_WITHOUT_API_KEY = {
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()
type(mocked_hole).get_data = AsyncMock(
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:
mocked_hole.data = []
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:
mocked_hole.versions = None
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:
"""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):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER

View File

@ -1,7 +1,7 @@
"""Test pi_hole component."""
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 . 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["latest_version"] 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 = (
("events", "data_id"),
("states", "event_id"), # This won't be found
("states", "old_state_id"),
("events", "data_id", "event_data", "data_id"),
("states", "event_id", None, None), # This won't be found
("states", "old_state_id", "states", "state_id"),
)
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)
dropped_constraints_1 = [
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(
session_maker, engine, table, column
)[1]
@ -914,7 +914,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None:
session_maker = Mock(return_value=session)
dropped_constraints_2 = [
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(
session_maker, engine, table, column
)[1]
@ -925,7 +925,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None:
with Session(engine) as session:
session_maker = Mock(return_value=session)
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)
@ -933,7 +933,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None:
session_maker = Mock(return_value=session)
dropped_constraints_3 = [
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(
session_maker, engine, table, column
)[1]
@ -951,21 +951,7 @@ def test_restore_foreign_key_constraints_with_error(
This is not supported on SQLite
"""
constraints_to_restore = [
(
"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",
},
),
]
constraints_to_restore = [("events", "data_id", "event_data", "data_id")]
connection = Mock()
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
@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,17 +86,25 @@ async def test_setup_host(hass: HomeAssistant, device, device2, main_zone) -> No
assert state.state == "off"
async def test_setup_attribute_error(hass: HomeAssistant, device, main_zone) -> None:
"""Test set up integration encountering an Attribute Error."""
@pytest.mark.parametrize(
("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)
await hass.async_block_till_done()
state = hass.states.get("media_player.yamaha_receiver_main_zone")
state = hass.states.get("media_player.yamaha_receiver_main_zone")
assert state is not None
assert state.state == "off"
assert state is not None
assert state.state == "off"
async def test_setup_no_host(hass: HomeAssistant, device, main_zone) -> None: