mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
2025.4.1 (#142299)
* Fix blocking event loop - daikin (#141442) * fix blocking event loop * create ssl_context directly * update manifest * update manifest.json * Made Google Search enable dependent on Assist availability (#141712) * Made Google Search enable dependent on Assist availability * Show error instead of rendering again * Cleanup test code * Fix humidifier platform for Comelit (#141854) * Fix humidifier platform for Comelit * apply review comment * Bump evohome-async to 1.0.5 (#141871) bump client to 1.0.5 * Replace "to log into" with "to log in to" in `incomfort` (#142060) * Replace "to log into" with "to log in to" in `incomfort` Also fix one missing sentence-casing of "gateway". * Replace duplicate "data_description" strings with references * Avoid unnecessary reload in apple_tv reauth flow (#142079) * Add translation for hassio update entity name (#142090) * Bump pyenphase to 1.25.5 (#142107) * Hide broken ZBT-1 config entries on the hardware page (#142110) * Hide bad ZBT-1 config entries on the hardware page * Set up the bad config entry in the unit test * Roll into a list comprehension * Remove constant changes * Fix condition in unit test * Bump pysmhi to 1.0.1 (#142111) * Avoid logging a warning when replacing an ignored config entry (#142114) Replacing an ignored config entry with one from the user flow should not generate a warning. We should only warn if we are replacing a usable config entry. Followup to adjust the warning added in #130567 cc @epenet * Slow down polling in Tesla Fleet (#142130) * Slow down polling * Fix tests * Bump tesla-fleet-api to v1.0.17 (#142131) bump * Tado bump to 0.18.11 (#142175) * Bump to version 0.18.11 * Adding hassfest files * Add preset mode to SmartThings climate (#142180) * Add preset mode to SmartThings climate * Add preset mode to SmartThings climate * Do not create a HA mediaplayer for the builtin Music Assistant player (#142192) Do not create a HA mediaplayer for the builtin Music player * Do not fetch disconnected Home Connect appliances (#142200) * Do not fetch disconnected Home Connect appliances * Apply suggestions Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update docstring --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Fix fibaro setup (#142201) * Fix circular mean by always storing and using the weighted one (#142208) * Fix circular mean by always storing and using the weighted one * fix * Fix test * Bump pySmartThings to 3.0.2 (#142257) Co-authored-by: Robert Resch <robert@resch.dev> * Update frontend to 20250404.0 (#142274) * Bump forecast-solar lib to v4.1.0 (#142280) Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me> * Bump version to 2025.4.1 * Fix skyconnect tests (#142262) fix tests * Fix empty actions (#142292) * Apply fix * Add tests for alarm button cover lock * update light * add number tests * test select * add switch tests * test vacuum * update lock test --------- Co-authored-by: Fredrik Erlandsson <fredrik.e@gmail.com> Co-authored-by: Ivan Lopez Hernandez <ivan.lh.94@outlook.com> Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com> Co-authored-by: David Bonnes <zxdavb@bonnes.me> Co-authored-by: Norbert Rittel <norbert@rittel.de> Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Paul Bottein <paul.bottein@gmail.com> Co-authored-by: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> Co-authored-by: G Johansson <goran.johansson@shiftit.se> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Brett Adams <Bre77@users.noreply.github.com> Co-authored-by: Erwin Douna <e.douna@gmail.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com> Co-authored-by: J. Diego Rodríguez Royo <jdrr1998@hotmail.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: rappenze <rappenze@yahoo.com> Co-authored-by: Robert Resch <robert@resch.dev> Co-authored-by: Bram Kragten <mail@bramkragten.nl> Co-authored-by: Klaas Schoute <klaas_schoute@hotmail.com> Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me> Co-authored-by: Josef Zweck <josef@zweck.dev> Co-authored-by: Petro31 <35082313+Petro31@users.noreply.github.com>
This commit is contained in:
commit
7af6a4f493
@ -20,6 +20,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_IDENTIFIERS: list(combined_identifiers),
|
||||
},
|
||||
)
|
||||
if entry.source != SOURCE_IGNORE:
|
||||
# Don't reload ignored entries or in the middle of reauth,
|
||||
# e.g. if the user is entering a new PIN
|
||||
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
if not allow_exist:
|
||||
raise DeviceAlreadyConfigured
|
||||
|
@ -162,7 +162,7 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
if self.mode == HumidifierComelitMode.OFF:
|
||||
if not self._attr_is_on:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="humidity_while_off",
|
||||
@ -190,9 +190,13 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
||||
await self.coordinator.api.set_humidity_status(
|
||||
self._device.index, self._set_command
|
||||
)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.coordinator.api.set_humidity_status(
|
||||
self._device.index, HumidifierComelitCommand.OFF
|
||||
)
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
@ -52,7 +52,9 @@
|
||||
"rest": "Rest",
|
||||
"sabotated": "Sabotated"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"humidifier": {
|
||||
"name": "Humidifier"
|
||||
},
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.util.ssl import client_context_no_verify
|
||||
|
||||
from .const import KEY_MAC, TIMEOUT
|
||||
from .coordinator import DaikinConfigEntry, DaikinCoordinator
|
||||
@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
|
||||
key=entry.data.get(CONF_API_KEY),
|
||||
uuid=entry.data.get(CONF_UUID),
|
||||
password=entry.data.get(CONF_PASSWORD),
|
||||
ssl_context=client_context_no_verify(),
|
||||
)
|
||||
_LOGGER.debug("Connection to %s successful", host)
|
||||
except TimeoutError as err:
|
||||
|
@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.14.1"],
|
||||
"requirements": ["pydaikin==2.15.0"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.25.1"],
|
||||
"requirements": ["pyenphase==1.25.5"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evohome-async==1.0.4"]
|
||||
"requirements": ["evohome-async==1.0.5"]
|
||||
}
|
||||
|
@ -301,6 +301,7 @@ class FibaroController:
|
||||
device.ha_id = (
|
||||
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
|
||||
)
|
||||
platform = None
|
||||
if device.enabled and (not device.is_plugin or self._import_plugins):
|
||||
platform = self._map_device_to_platform(device)
|
||||
if platform is None:
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["forecast-solar==4.0.0"]
|
||||
"requirements": ["forecast-solar==4.1.0"]
|
||||
}
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250401.0"]
|
||||
"requirements": ["home-assistant-frontend==20250404.0"]
|
||||
}
|
||||
|
@ -179,28 +179,30 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if user_input[CONF_LLM_HASS_API] == "none":
|
||||
user_input.pop(CONF_LLM_HASS_API)
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
if not (
|
||||
user_input.get(CONF_LLM_HASS_API, "none") != "none"
|
||||
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
|
||||
):
|
||||
# Don't allow to save options that enable the Google Seearch tool with an Assist API
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
errors[CONF_USE_GOOGLE_SEARCH_TOOL] = "invalid_google_search_option"
|
||||
|
||||
# Re-render the options again, now with the recommended options shown/hidden
|
||||
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
||||
|
||||
options = {
|
||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
||||
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
|
||||
}
|
||||
options = user_input
|
||||
|
||||
schema = await google_generative_ai_config_option_schema(
|
||||
self.hass, options, self._genai_client
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(schema),
|
||||
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||
)
|
||||
|
||||
|
||||
|
@ -43,6 +43,9 @@
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -265,6 +265,11 @@
|
||||
"version_latest": {
|
||||
"name": "Newest version"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"update": {
|
||||
"name": "[%key:component::update::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -39,7 +39,7 @@ from .entity import (
|
||||
from .update_helper import update_addon, update_core
|
||||
|
||||
ENTITY_DESCRIPTION = UpdateEntityDescription(
|
||||
name="Update",
|
||||
translation_key="update",
|
||||
key=ATTR_VERSION_LATEST,
|
||||
)
|
||||
|
||||
|
@ -73,6 +73,19 @@ class HomeConnectApplianceData:
|
||||
self.settings.update(other.settings)
|
||||
self.status.update(other.status)
|
||||
|
||||
@classmethod
|
||||
def empty(cls, appliance: HomeAppliance) -> HomeConnectApplianceData:
|
||||
"""Return empty data."""
|
||||
return cls(
|
||||
commands=set(),
|
||||
events={},
|
||||
info=appliance,
|
||||
options={},
|
||||
programs=[],
|
||||
settings={},
|
||||
status={},
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectCoordinator(
|
||||
DataUpdateCoordinator[dict[str, HomeConnectApplianceData]]
|
||||
@ -358,15 +371,7 @@ class HomeConnectCoordinator(
|
||||
model=appliance.vib,
|
||||
)
|
||||
if appliance.ha_id not in self.data:
|
||||
self.data[appliance.ha_id] = HomeConnectApplianceData(
|
||||
commands=set(),
|
||||
events={},
|
||||
info=appliance,
|
||||
options={},
|
||||
programs=[],
|
||||
settings={},
|
||||
status={},
|
||||
)
|
||||
self.data[appliance.ha_id] = HomeConnectApplianceData.empty(appliance)
|
||||
else:
|
||||
self.data[appliance.ha_id].info.connected = appliance.connected
|
||||
old_appliances.remove(appliance.ha_id)
|
||||
@ -402,6 +407,15 @@ class HomeConnectCoordinator(
|
||||
name=appliance.name,
|
||||
model=appliance.vib,
|
||||
)
|
||||
if not appliance.connected:
|
||||
_LOGGER.debug(
|
||||
"Appliance %s is not connected, skipping data fetch",
|
||||
appliance.ha_id,
|
||||
)
|
||||
if appliance_data_to_update:
|
||||
appliance_data_to_update.info.connected = False
|
||||
return appliance_data_to_update
|
||||
return HomeConnectApplianceData.empty(appliance)
|
||||
try:
|
||||
settings = {
|
||||
setting.key: setting
|
||||
|
@ -5,17 +5,21 @@ from __future__ import annotations
|
||||
from homeassistant.components.hardware.models import HardwareInfo, USBInfo
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .config_flow import HomeAssistantSkyConnectConfigFlow
|
||||
from .const import DOMAIN
|
||||
from .util import get_hardware_variant
|
||||
|
||||
DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/"
|
||||
EXPECTED_ENTRY_VERSION = (
|
||||
HomeAssistantSkyConnectConfigFlow.VERSION,
|
||||
HomeAssistantSkyConnectConfigFlow.MINOR_VERSION,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
"""Return board info."""
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
return [
|
||||
HardwareInfo(
|
||||
board=None,
|
||||
@ -31,4 +35,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
url=DOCUMENTATION_URL,
|
||||
)
|
||||
for entry in entries
|
||||
# Ignore unmigrated config entries in the hardware page
|
||||
if (entry.version, entry.minor_version) == EXPECTED_ENTRY_VERSION
|
||||
]
|
||||
|
@ -10,8 +10,8 @@
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP-address of the Intergas gateway.",
|
||||
"username": "The username to log into the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log into the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices."
|
||||
"username": "The username to log in to the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log in to the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices."
|
||||
}
|
||||
},
|
||||
"dhcp_auth": {
|
||||
@ -22,8 +22,8 @@
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "The username to log into the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log into the gateway, is printed at the bottom of the Gateway or is `intergas` for some older devices."
|
||||
"username": "[%key:component::incomfort::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::incomfort::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"dhcp_confirm": {
|
||||
|
@ -151,6 +151,8 @@ async def async_setup_entry(
|
||||
assert event.object_id is not None
|
||||
if event.object_id in added_ids:
|
||||
return
|
||||
if not player.expose_to_ha:
|
||||
return
|
||||
added_ids.add(event.object_id)
|
||||
async_add_entities([MusicAssistantPlayer(mass, event.object_id)])
|
||||
|
||||
@ -159,6 +161,8 @@ async def async_setup_entry(
|
||||
mass_players = []
|
||||
# add all current players
|
||||
for player in mass.players:
|
||||
if not player.expose_to_ha:
|
||||
continue
|
||||
added_ids.add(player.player_id)
|
||||
mass_players.append(MusicAssistantPlayer(mass, player.player_id))
|
||||
|
||||
|
@ -139,14 +139,13 @@ def query_circular_mean(table: type[StatisticsBase]) -> tuple[Label, Label]:
|
||||
# in Python.
|
||||
# https://en.wikipedia.org/wiki/Circular_mean
|
||||
radians = func.radians(table.mean)
|
||||
weighted_sum_sin = func.sum(func.sin(radians) * table.mean_weight)
|
||||
weighted_sum_cos = func.sum(func.cos(radians) * table.mean_weight)
|
||||
weight = func.sqrt(
|
||||
func.power(func.sum(func.sin(radians) * table.mean_weight), 2)
|
||||
+ func.power(func.sum(func.cos(radians) * table.mean_weight), 2)
|
||||
func.power(weighted_sum_sin, 2) + func.power(weighted_sum_cos, 2)
|
||||
)
|
||||
return (
|
||||
func.degrees(
|
||||
func.atan2(func.sum(func.sin(radians)), func.sum(func.cos(radians)))
|
||||
).label("mean"),
|
||||
func.degrees(func.atan2(weighted_sum_sin, weighted_sum_cos)).label("mean"),
|
||||
weight.label("mean_weight"),
|
||||
)
|
||||
|
||||
@ -240,18 +239,20 @@ DEG_TO_RAD = math.pi / 180
|
||||
RAD_TO_DEG = 180 / math.pi
|
||||
|
||||
|
||||
def weighted_circular_mean(values: Iterable[tuple[float, float]]) -> float:
|
||||
"""Return the weighted circular mean of the values."""
|
||||
sin_sum = sum(math.sin(x * DEG_TO_RAD) * weight for x, weight in values)
|
||||
cos_sum = sum(math.cos(x * DEG_TO_RAD) * weight for x, weight in values)
|
||||
return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360
|
||||
def weighted_circular_mean(
|
||||
values: Iterable[tuple[float, float]],
|
||||
) -> tuple[float, float]:
|
||||
"""Return the weighted circular mean and the weight of the values."""
|
||||
weighted_sin_sum, weighted_cos_sum = 0.0, 0.0
|
||||
for x, weight in values:
|
||||
rad_x = x * DEG_TO_RAD
|
||||
weighted_sin_sum += math.sin(rad_x) * weight
|
||||
weighted_cos_sum += math.cos(rad_x) * weight
|
||||
|
||||
|
||||
def circular_mean(values: list[float]) -> float:
|
||||
"""Return the circular mean of the values."""
|
||||
sin_sum = sum(math.sin(x * DEG_TO_RAD) for x in values)
|
||||
cos_sum = sum(math.cos(x * DEG_TO_RAD) for x in values)
|
||||
return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360
|
||||
return (
|
||||
(RAD_TO_DEG * math.atan2(weighted_sin_sum, weighted_cos_sum)) % 360,
|
||||
math.sqrt(weighted_sin_sum**2 + weighted_cos_sum**2),
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -300,6 +301,7 @@ class StatisticsRow(BaseStatisticsRow, total=False):
|
||||
min: float | None
|
||||
max: float | None
|
||||
mean: float | None
|
||||
mean_weight: float | None
|
||||
change: float | None
|
||||
|
||||
|
||||
@ -1023,7 +1025,7 @@ def _reduce_statistics(
|
||||
_want_sum = "sum" in types
|
||||
for statistic_id, stat_list in stats.items():
|
||||
max_values: list[float] = []
|
||||
mean_values: list[float] = []
|
||||
mean_values: list[tuple[float, float]] = []
|
||||
min_values: list[float] = []
|
||||
prev_stat: StatisticsRow = stat_list[0]
|
||||
fake_entry: StatisticsRow = {"start": stat_list[-1]["start"] + period_seconds}
|
||||
@ -1039,12 +1041,15 @@ def _reduce_statistics(
|
||||
}
|
||||
if _want_mean:
|
||||
row["mean"] = None
|
||||
row["mean_weight"] = None
|
||||
if mean_values:
|
||||
match metadata[statistic_id][1]["mean_type"]:
|
||||
case StatisticMeanType.ARITHMETIC:
|
||||
row["mean"] = mean(mean_values)
|
||||
row["mean"] = mean([x[0] for x in mean_values])
|
||||
case StatisticMeanType.CIRCULAR:
|
||||
row["mean"] = circular_mean(mean_values)
|
||||
row["mean"], row["mean_weight"] = (
|
||||
weighted_circular_mean(mean_values)
|
||||
)
|
||||
mean_values.clear()
|
||||
if _want_min:
|
||||
row["min"] = min(min_values) if min_values else None
|
||||
@ -1063,7 +1068,8 @@ def _reduce_statistics(
|
||||
max_values.append(_max)
|
||||
if _want_mean:
|
||||
if (_mean := statistic.get("mean")) is not None:
|
||||
mean_values.append(_mean)
|
||||
_mean_weight = statistic.get("mean_weight") or 0.0
|
||||
mean_values.append((_mean, _mean_weight))
|
||||
if _want_min and (_min := statistic.get("min")) is not None:
|
||||
min_values.append(_min)
|
||||
prev_stat = statistic
|
||||
@ -1385,7 +1391,7 @@ def _get_max_mean_min_statistic(
|
||||
match metadata[1]["mean_type"]:
|
||||
case StatisticMeanType.CIRCULAR:
|
||||
if circular_means := max_mean_min["circular_means"]:
|
||||
mean_value = weighted_circular_mean(circular_means)
|
||||
mean_value = weighted_circular_mean(circular_means)[0]
|
||||
case StatisticMeanType.ARITHMETIC:
|
||||
if (mean_value := max_mean_min.get("mean_acc")) is not None and (
|
||||
duration := max_mean_min.get("duration")
|
||||
@ -1739,12 +1745,12 @@ def statistic_during_period(
|
||||
|
||||
|
||||
_type_column_mapping = {
|
||||
"last_reset": "last_reset_ts",
|
||||
"max": "max",
|
||||
"mean": "mean",
|
||||
"min": "min",
|
||||
"state": "state",
|
||||
"sum": "sum",
|
||||
"last_reset": ("last_reset_ts",),
|
||||
"max": ("max",),
|
||||
"mean": ("mean", "mean_weight"),
|
||||
"min": ("min",),
|
||||
"state": ("state",),
|
||||
"sum": ("sum",),
|
||||
}
|
||||
|
||||
|
||||
@ -1756,12 +1762,13 @@ def _generate_select_columns_for_types_stmt(
|
||||
track_on: list[str | None] = [
|
||||
table.__tablename__, # type: ignore[attr-defined]
|
||||
]
|
||||
for key, column in _type_column_mapping.items():
|
||||
if key in types:
|
||||
columns = columns.add_columns(getattr(table, column))
|
||||
track_on.append(column)
|
||||
else:
|
||||
track_on.append(None)
|
||||
for key, type_columns in _type_column_mapping.items():
|
||||
for column in type_columns:
|
||||
if key in types:
|
||||
columns = columns.add_columns(getattr(table, column))
|
||||
track_on.append(column)
|
||||
else:
|
||||
track_on.append(None)
|
||||
return lambda_stmt(lambda: columns, track_on=track_on)
|
||||
|
||||
|
||||
@ -1944,6 +1951,12 @@ def _statistics_during_period_with_session(
|
||||
hass, session, start_time, units, _types, table, metadata, result
|
||||
)
|
||||
|
||||
# filter out mean_weight as it is only needed to reduce statistics
|
||||
# and not needed in the result
|
||||
for stats_rows in result.values():
|
||||
for row in stats_rows:
|
||||
row.pop("mean_weight", None)
|
||||
|
||||
# Return statistics combined with metadata
|
||||
return result
|
||||
|
||||
@ -2391,7 +2404,12 @@ def _sorted_statistics_to_dict(
|
||||
field_map["last_reset"] = field_map.pop("last_reset_ts")
|
||||
sum_idx = field_map["sum"] if "sum" in types else None
|
||||
sum_only = len(types) == 1 and sum_idx is not None
|
||||
row_mapping = tuple((key, field_map[key]) for key in types if key in field_map)
|
||||
row_mapping = tuple(
|
||||
(column, field_map[column])
|
||||
for key in types
|
||||
for column in ({key, *_type_column_mapping.get(key, ())})
|
||||
if column in field_map
|
||||
)
|
||||
# Append all statistic entries, and optionally do unit conversion
|
||||
table_duration_seconds = table.duration.total_seconds()
|
||||
for meta_id, db_rows in stats_by_meta_id.items():
|
||||
|
@ -160,7 +160,7 @@ def _time_weighted_arithmetic_mean(
|
||||
|
||||
def _time_weighted_circular_mean(
|
||||
fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime
|
||||
) -> float:
|
||||
) -> tuple[float, float]:
|
||||
"""Calculate a time weighted circular mean.
|
||||
|
||||
The circular mean is calculated by weighting the states by duration in seconds between
|
||||
@ -623,7 +623,7 @@ def compile_statistics( # noqa: C901
|
||||
valid_float_states, start, end
|
||||
)
|
||||
case StatisticMeanType.CIRCULAR:
|
||||
stat["mean"] = _time_weighted_circular_mean(
|
||||
stat["mean"], stat["mean_weight"] = _time_weighted_circular_mean(
|
||||
valid_float_states, start, end
|
||||
)
|
||||
|
||||
|
@ -333,7 +333,6 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
"""Define a SmartThings Air Conditioner."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_preset_mode = None
|
||||
|
||||
def __init__(self, client: SmartThings, device: FullDevice) -> None:
|
||||
"""Init the class."""
|
||||
@ -545,6 +544,18 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
SWING_OFF,
|
||||
)
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the preset mode."""
|
||||
if self.supports_capability(Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE):
|
||||
mode = self.get_attribute_value(
|
||||
Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE,
|
||||
Attribute.AC_OPTIONAL_MODE,
|
||||
)
|
||||
if mode == WINDFREE:
|
||||
return WINDFREE
|
||||
return None
|
||||
|
||||
def _determine_preset_modes(self) -> list[str] | None:
|
||||
"""Return a list of available preset modes."""
|
||||
if self.supports_capability(Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE):
|
||||
|
@ -30,5 +30,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.0.1"]
|
||||
"requirements": ["pysmartthings==3.0.2"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smhi",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pysmhi"],
|
||||
"requirements": ["pysmhi==1.0.0"]
|
||||
"requirements": ["pysmhi==1.0.1"]
|
||||
}
|
||||
|
@ -14,5 +14,5 @@
|
||||
},
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["PyTado"],
|
||||
"requirements": ["python-tado==0.18.9"]
|
||||
"requirements": ["python-tado==0.18.11"]
|
||||
}
|
||||
|
@ -214,7 +214,8 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore
|
||||
),
|
||||
(CONF_TRIGGER_ACTION, AlarmControlPanelEntityFeature.TRIGGER),
|
||||
):
|
||||
if action_config := config.get(action_id):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (action_config := config.get(action_id)) is not None:
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
|
@ -120,7 +120,8 @@ class TemplateButtonEntity(TemplateEntity, ButtonEntity):
|
||||
"""Initialize the button."""
|
||||
super().__init__(hass, config=config, unique_id=unique_id)
|
||||
assert self._attr_name is not None
|
||||
if action := config.get(CONF_PRESS):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (action := config.get(CONF_PRESS)) is not None:
|
||||
self.add_script(CONF_PRESS, action, self._attr_name, DOMAIN)
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._attr_state = None
|
||||
|
@ -172,7 +172,8 @@ class CoverTemplate(TemplateEntity, CoverEntity):
|
||||
(POSITION_ACTION, CoverEntityFeature.SET_POSITION),
|
||||
(TILT_ACTION, TILT_FEATURES),
|
||||
):
|
||||
if action_config := config.get(action_id):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (action_config := config.get(action_id)) is not None:
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
|
@ -157,7 +157,8 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
CONF_SET_OSCILLATING_ACTION,
|
||||
CONF_SET_DIRECTION_ACTION,
|
||||
):
|
||||
if action_config := config.get(action_id):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (action_config := config.get(action_id)) is not None:
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
|
||||
self._state: bool | None = False
|
||||
|
@ -296,7 +296,8 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
self._supports_transition_template = config.get(CONF_SUPPORTS_TRANSITION)
|
||||
|
||||
for action_id in (CONF_ON_ACTION, CONF_OFF_ACTION, CONF_EFFECT_ACTION):
|
||||
if action_config := config.get(action_id):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (action_config := config.get(action_id)) is not None:
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
|
||||
self._state = False
|
||||
@ -323,7 +324,8 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
(CONF_RGBW_ACTION, ColorMode.RGBW),
|
||||
(CONF_RGBWW_ACTION, ColorMode.RGBWW),
|
||||
):
|
||||
if action_config := config.get(action_id):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (action_config := config.get(action_id)) is not None:
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
color_modes.add(color_mode)
|
||||
self._supported_color_modes = filter_supported_color_modes(color_modes)
|
||||
@ -333,7 +335,7 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
self._color_mode = next(iter(self._supported_color_modes))
|
||||
|
||||
self._attr_supported_features = LightEntityFeature(0)
|
||||
if self._action_scripts.get(CONF_EFFECT_ACTION):
|
||||
if (self._action_scripts.get(CONF_EFFECT_ACTION)) is not None:
|
||||
self._attr_supported_features |= LightEntityFeature.EFFECT
|
||||
if self._supports_transition is True:
|
||||
self._attr_supported_features |= LightEntityFeature.TRANSITION
|
||||
|
@ -98,7 +98,8 @@ class TemplateLock(TemplateEntity, LockEntity):
|
||||
(CONF_UNLOCK, 0),
|
||||
(CONF_OPEN, LockEntityFeature.OPEN),
|
||||
):
|
||||
if action_config := config.get(action_id):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (action_config := config.get(action_id)) is not None:
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
self._code_format_template = config.get(CONF_CODE_FORMAT_TEMPLATE)
|
||||
|
@ -141,7 +141,8 @@ class TemplateSelect(TemplateEntity, SelectEntity):
|
||||
super().__init__(hass, config=config, unique_id=unique_id)
|
||||
assert self._attr_name is not None
|
||||
self._value_template = config[CONF_STATE]
|
||||
if select_option := config.get(CONF_SELECT_OPTION):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (select_option := config.get(CONF_SELECT_OPTION)) is not None:
|
||||
self.add_script(CONF_SELECT_OPTION, select_option, self._attr_name, DOMAIN)
|
||||
self._options_template = config[ATTR_OPTIONS]
|
||||
self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC, False)
|
||||
@ -197,7 +198,8 @@ class TriggerSelectEntity(TriggerEntity, SelectEntity):
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(hass, coordinator, config)
|
||||
if select_option := config.get(CONF_SELECT_OPTION):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (select_option := config.get(CONF_SELECT_OPTION)) is not None:
|
||||
self.add_script(
|
||||
CONF_SELECT_OPTION,
|
||||
select_option,
|
||||
|
@ -226,9 +226,10 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
|
||||
assert name is not None
|
||||
self._template = config.get(CONF_STATE)
|
||||
|
||||
if on_action := config.get(CONF_TURN_ON):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (on_action := config.get(CONF_TURN_ON)) is not None:
|
||||
self.add_script(CONF_TURN_ON, on_action, name, DOMAIN)
|
||||
if off_action := config.get(CONF_TURN_OFF):
|
||||
if (off_action := config.get(CONF_TURN_OFF)) is not None:
|
||||
self.add_script(CONF_TURN_OFF, off_action, name, DOMAIN)
|
||||
|
||||
self._state: bool | None = False
|
||||
|
@ -158,7 +158,8 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity):
|
||||
(SERVICE_LOCATE, VacuumEntityFeature.LOCATE),
|
||||
(SERVICE_SET_FAN_SPEED, VacuumEntityFeature.FAN_SPEED),
|
||||
):
|
||||
if action_config := config.get(action_id):
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (action_config := config.get(action_id)) is not None:
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
|
@ -27,7 +27,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from .const import ENERGY_HISTORY_FIELDS, LOGGER, TeslaFleetState
|
||||
|
||||
VEHICLE_INTERVAL_SECONDS = 300
|
||||
VEHICLE_INTERVAL_SECONDS = 600
|
||||
VEHICLE_INTERVAL = timedelta(seconds=VEHICLE_INTERVAL_SECONDS)
|
||||
VEHICLE_WAIT = timedelta(minutes=15)
|
||||
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.0.16"]
|
||||
"requirements": ["tesla-fleet-api==1.0.17"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.0.16", "teslemetry-stream==0.6.12"]
|
||||
"requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.6.12"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie", "tesla-fleet-api"],
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.0.16"]
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.0.17"]
|
||||
}
|
||||
|
@ -1626,7 +1626,11 @@ class ConfigEntriesFlowManager(
|
||||
result["handler"], flow.unique_id
|
||||
)
|
||||
|
||||
if existing_entry is not None and flow.handler != "mobile_app":
|
||||
if (
|
||||
existing_entry is not None
|
||||
and flow.handler != "mobile_app"
|
||||
and existing_entry.source != SOURCE_IGNORE
|
||||
):
|
||||
# This causes the old entry to be removed and replaced, when the flow
|
||||
# should instead be aborted.
|
||||
# In case of manual flows, integrations should implement options, reauth,
|
||||
|
@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 4
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "1"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)
|
||||
|
@ -38,7 +38,7 @@ habluetooth==3.37.0
|
||||
hass-nabucasa==0.94.0
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250401.0
|
||||
home-assistant-frontend==20250404.0
|
||||
home-assistant-intents==2025.3.28
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.4.0"
|
||||
version = "2025.4.1"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
18
requirements_all.txt
generated
18
requirements_all.txt
generated
@ -901,7 +901,7 @@ eufylife-ble-client==0.1.8
|
||||
# evdev==1.6.1
|
||||
|
||||
# homeassistant.components.evohome
|
||||
evohome-async==1.0.4
|
||||
evohome-async==1.0.5
|
||||
|
||||
# homeassistant.components.bryant_evolution
|
||||
evolutionhttp==0.0.18
|
||||
@ -954,7 +954,7 @@ fnv-hash-fast==1.4.0
|
||||
foobot_async==1.0.0
|
||||
|
||||
# homeassistant.components.forecast_solar
|
||||
forecast-solar==4.0.0
|
||||
forecast-solar==4.1.0
|
||||
|
||||
# homeassistant.components.fortios
|
||||
fortiosapi==1.0.5
|
||||
@ -1157,7 +1157,7 @@ hole==0.8.0
|
||||
holidays==0.69
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250401.0
|
||||
home-assistant-frontend==20250404.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.3.28
|
||||
@ -1891,7 +1891,7 @@ pycsspeechtts==1.0.8
|
||||
# pycups==2.0.4
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.14.1
|
||||
pydaikin==2.15.0
|
||||
|
||||
# homeassistant.components.danfoss_air
|
||||
pydanfossair==0.1.0
|
||||
@ -1948,7 +1948,7 @@ pyeiscp==0.0.7
|
||||
pyemoncms==0.1.1
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==1.25.1
|
||||
pyenphase==1.25.5
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
pyenvisalink==4.7
|
||||
@ -2319,13 +2319,13 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.0.1
|
||||
pysmartthings==3.0.2
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
|
||||
# homeassistant.components.smhi
|
||||
pysmhi==1.0.0
|
||||
pysmhi==1.0.1
|
||||
|
||||
# homeassistant.components.edl21
|
||||
pysml==0.0.12
|
||||
@ -2482,7 +2482,7 @@ python-snoo==0.6.5
|
||||
python-songpal==0.16.2
|
||||
|
||||
# homeassistant.components.tado
|
||||
python-tado==0.18.9
|
||||
python-tado==0.18.11
|
||||
|
||||
# homeassistant.components.technove
|
||||
python-technove==2.0.0
|
||||
@ -2878,7 +2878,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.0.16
|
||||
tesla-fleet-api==1.0.17
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
|
18
requirements_test_all.txt
generated
18
requirements_test_all.txt
generated
@ -768,7 +768,7 @@ eternalegypt==0.0.16
|
||||
eufylife-ble-client==0.1.8
|
||||
|
||||
# homeassistant.components.evohome
|
||||
evohome-async==1.0.4
|
||||
evohome-async==1.0.5
|
||||
|
||||
# homeassistant.components.bryant_evolution
|
||||
evolutionhttp==0.0.18
|
||||
@ -814,7 +814,7 @@ fnv-hash-fast==1.4.0
|
||||
foobot_async==1.0.0
|
||||
|
||||
# homeassistant.components.forecast_solar
|
||||
forecast-solar==4.0.0
|
||||
forecast-solar==4.1.0
|
||||
|
||||
# homeassistant.components.freebox
|
||||
freebox-api==1.2.2
|
||||
@ -984,7 +984,7 @@ hole==0.8.0
|
||||
holidays==0.69
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250401.0
|
||||
home-assistant-frontend==20250404.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.3.28
|
||||
@ -1548,7 +1548,7 @@ pycountry==24.6.1
|
||||
pycsspeechtts==1.0.8
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.14.1
|
||||
pydaikin==2.15.0
|
||||
|
||||
# homeassistant.components.deako
|
||||
pydeako==0.6.0
|
||||
@ -1590,7 +1590,7 @@ pyeiscp==0.0.7
|
||||
pyemoncms==0.1.1
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==1.25.1
|
||||
pyenphase==1.25.5
|
||||
|
||||
# homeassistant.components.everlights
|
||||
pyeverlights==0.1.0
|
||||
@ -1889,13 +1889,13 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.0.1
|
||||
pysmartthings==3.0.2
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
|
||||
# homeassistant.components.smhi
|
||||
pysmhi==1.0.0
|
||||
pysmhi==1.0.1
|
||||
|
||||
# homeassistant.components.edl21
|
||||
pysml==0.0.12
|
||||
@ -2013,7 +2013,7 @@ python-snoo==0.6.5
|
||||
python-songpal==0.16.2
|
||||
|
||||
# homeassistant.components.tado
|
||||
python-tado==0.18.9
|
||||
python-tado==0.18.11
|
||||
|
||||
# homeassistant.components.technove
|
||||
python-technove==2.0.0
|
||||
@ -2316,7 +2316,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.0.16
|
||||
tesla-fleet-api==1.0.17
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
|
@ -59,12 +59,12 @@ def use_mocked_zeroconf(mock_async_zeroconf: MagicMock) -> None:
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_setup_entry() -> Generator[None]:
|
||||
def mock_setup_entry() -> Generator[Mock]:
|
||||
"""Mock setting up a config entry."""
|
||||
with patch(
|
||||
"homeassistant.components.apple_tv.async_setup_entry", return_value=True
|
||||
):
|
||||
yield
|
||||
) as setup_entry:
|
||||
yield setup_entry
|
||||
|
||||
|
||||
# User Flows
|
||||
@ -1183,7 +1183,9 @@ async def test_zeroconf_mismatch(hass: HomeAssistant, mock_scan: AsyncMock) -> N
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mrp_device", "pairing")
|
||||
async def test_reconfigure_update_credentials(hass: HomeAssistant) -> None:
|
||||
async def test_reconfigure_update_credentials(
|
||||
hass: HomeAssistant, mock_setup_entry: Mock
|
||||
) -> None:
|
||||
"""Test that reconfigure flow updates config entry."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain="apple_tv", unique_id="mrpid", data={"identifiers": ["mrpid"]}
|
||||
@ -1215,6 +1217,9 @@ async def test_reconfigure_update_credentials(hass: HomeAssistant) -> None:
|
||||
"identifiers": ["mrpid"],
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
# Options
|
||||
|
||||
|
@ -39,9 +39,8 @@ from . import CLIENT_ERROR_500, CLIENT_ERROR_API_KEY_INVALID
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_models():
|
||||
"""Mock the model list API."""
|
||||
def get_models_pager():
|
||||
"""Return a generator that yields the models."""
|
||||
model_20_flash = Mock(
|
||||
display_name="Gemini 2.0 Flash",
|
||||
supported_actions=["generateContent"],
|
||||
@ -72,11 +71,7 @@ def mock_models():
|
||||
yield model_15_pro
|
||||
yield model_10_pro
|
||||
|
||||
with patch(
|
||||
"google.genai.models.AsyncModels.list",
|
||||
return_value=models_pager(),
|
||||
):
|
||||
yield
|
||||
return models_pager()
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
@ -119,8 +114,13 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
def will_options_be_rendered_again(current_options, new_options) -> bool:
|
||||
"""Determine if options will be rendered again."""
|
||||
return current_options.get(CONF_RECOMMENDED) != new_options.get(CONF_RECOMMENDED)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current_options", "new_options", "expected_options"),
|
||||
("current_options", "new_options", "expected_options", "errors"),
|
||||
[
|
||||
(
|
||||
{
|
||||
@ -147,6 +147,7 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_USE_GOOGLE_SEARCH_TOOL: RECOMMENDED_USE_GOOGLE_SEARCH_TOOL,
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
{
|
||||
@ -157,6 +158,7 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_TOP_K: RECOMMENDED_TOP_K,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_USE_GOOGLE_SEARCH_TOOL: True,
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
@ -168,6 +170,98 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_PROMPT: "",
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_TEMPERATURE: 0.3,
|
||||
CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_TOP_K: RECOMMENDED_TOP_K,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_USE_GOOGLE_SEARCH_TOOL: RECOMMENDED_USE_GOOGLE_SEARCH_TOOL,
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_TEMPERATURE: 0.3,
|
||||
CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_TOP_K: RECOMMENDED_TOP_K,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_USE_GOOGLE_SEARCH_TOOL: True,
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_TEMPERATURE: 0.3,
|
||||
CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_TOP_K: RECOMMENDED_TOP_K,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_USE_GOOGLE_SEARCH_TOOL: True,
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_TEMPERATURE: 0.3,
|
||||
CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_TOP_K: RECOMMENDED_TOP_K,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_USE_GOOGLE_SEARCH_TOOL: True,
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_TEMPERATURE: 0.3,
|
||||
CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_TOP_K: RECOMMENDED_TOP_K,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_USE_GOOGLE_SEARCH_TOOL: True,
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_TEMPERATURE: 0.3,
|
||||
CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_TOP_K: RECOMMENDED_TOP_K,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_HATE_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD: RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
CONF_USE_GOOGLE_SEARCH_TOOL: True,
|
||||
},
|
||||
{CONF_USE_GOOGLE_SEARCH_TOOL: "invalid_google_search_option"},
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -175,10 +269,10 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
async def test_options_switching(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_models,
|
||||
current_options,
|
||||
new_options,
|
||||
expected_options,
|
||||
errors,
|
||||
) -> None:
|
||||
"""Test the options form."""
|
||||
with patch("google.genai.models.AsyncModels.get"):
|
||||
@ -186,24 +280,42 @@ async def test_options_switching(
|
||||
mock_config_entry, options=current_options
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
options_flow = await hass.config_entries.options.async_init(
|
||||
mock_config_entry.entry_id
|
||||
)
|
||||
if current_options.get(CONF_RECOMMENDED) != new_options.get(CONF_RECOMMENDED):
|
||||
options_flow = await hass.config_entries.options.async_configure(
|
||||
options_flow["flow_id"],
|
||||
{
|
||||
**current_options,
|
||||
CONF_RECOMMENDED: new_options[CONF_RECOMMENDED],
|
||||
},
|
||||
with patch(
|
||||
"google.genai.models.AsyncModels.list",
|
||||
return_value=get_models_pager(),
|
||||
):
|
||||
options_flow = await hass.config_entries.options.async_init(
|
||||
mock_config_entry.entry_id
|
||||
)
|
||||
if will_options_be_rendered_again(current_options, new_options):
|
||||
retry_options = {
|
||||
**current_options,
|
||||
CONF_RECOMMENDED: new_options[CONF_RECOMMENDED],
|
||||
}
|
||||
with patch(
|
||||
"google.genai.models.AsyncModels.list",
|
||||
return_value=get_models_pager(),
|
||||
):
|
||||
options_flow = await hass.config_entries.options.async_configure(
|
||||
options_flow["flow_id"],
|
||||
retry_options,
|
||||
)
|
||||
with patch(
|
||||
"google.genai.models.AsyncModels.list",
|
||||
return_value=get_models_pager(),
|
||||
):
|
||||
options = await hass.config_entries.options.async_configure(
|
||||
options_flow["flow_id"],
|
||||
new_options,
|
||||
)
|
||||
options = await hass.config_entries.options.async_configure(
|
||||
options_flow["flow_id"],
|
||||
new_options,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert options["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert options["data"] == expected_options
|
||||
if errors is None:
|
||||
assert options["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert options["data"] == expected_options
|
||||
|
||||
else:
|
||||
assert options["type"] is FlowResultType.FORM
|
||||
assert options.get("errors", None) == errors
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -54,6 +54,14 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
INITIAL_FETCH_CLIENT_METHODS = [
|
||||
"get_settings",
|
||||
"get_status",
|
||||
"get_all_programs",
|
||||
"get_available_commands",
|
||||
"get_available_program",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[str]:
|
||||
@ -214,15 +222,32 @@ async def test_coordinator_failure_refresh_and_stream(
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"appliance",
|
||||
["Dishwasher"],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_coordinator_not_fetching_on_disconnected_appliance(
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
appliance: HomeAppliance,
|
||||
) -> None:
|
||||
"""Test that the coordinator does not fetch anything on disconnected appliance."""
|
||||
appliance.connected = False
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
for method in INITIAL_FETCH_CLIENT_METHODS:
|
||||
assert getattr(client, method).call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_method",
|
||||
[
|
||||
"get_settings",
|
||||
"get_status",
|
||||
"get_all_programs",
|
||||
"get_available_commands",
|
||||
"get_available_program",
|
||||
],
|
||||
INITIAL_FETCH_CLIENT_METHODS,
|
||||
)
|
||||
async def test_coordinator_update_failing(
|
||||
mock_method: str,
|
||||
@ -551,3 +576,35 @@ async def test_devices_updated_on_refresh(
|
||||
assert not device_registry.async_get_device({(DOMAIN, appliances[0].ha_id)})
|
||||
for appliance in appliances[2:3]:
|
||||
assert device_registry.async_get_device({(DOMAIN, appliance.ha_id)})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
async def test_paired_disconnected_devices_not_fetching(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
appliance: HomeAppliance,
|
||||
) -> None:
|
||||
"""Test that Home Connect API is not fetched after pairing a disconnected device."""
|
||||
client.get_home_appliances = AsyncMock(return_value=ArrayOfHomeAppliances([]))
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
appliance.connected = False
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance.ha_id,
|
||||
EventType.PAIRED,
|
||||
data=ArrayOfEvents([]),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client.get_specific_appliance.assert_awaited_once_with(appliance.ha_id)
|
||||
for method in INITIAL_FETCH_CLIENT_METHODS:
|
||||
assert getattr(client, method).call_count == 0
|
||||
|
@ -28,6 +28,10 @@ CONFIG_ENTRY_DATA_2 = {
|
||||
"firmware": "ezsp",
|
||||
}
|
||||
|
||||
CONFIG_ENTRY_DATA_BAD = {
|
||||
"device": "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_a87b7d75b18beb119fe564a0f320645d-if00-port0",
|
||||
}
|
||||
|
||||
|
||||
async def test_hardware_info(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, addon_store_info
|
||||
@ -59,9 +63,20 @@ async def test_hardware_info(
|
||||
minor_version=2,
|
||||
)
|
||||
config_entry_2.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry_2.entry_id)
|
||||
|
||||
config_entry_bad = MockConfigEntry(
|
||||
data=CONFIG_ENTRY_DATA_BAD,
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Connect ZBT-1",
|
||||
unique_id="unique_3",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
config_entry_bad.add_to_hass(hass)
|
||||
assert not await hass.config_entries.async_setup(config_entry_bad.entry_id)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "hardware/info"})
|
||||
@ -97,5 +112,6 @@ async def test_hardware_info(
|
||||
"name": "Home Assistant Connect ZBT-1",
|
||||
"url": "https://skyconnect.home-assistant.io/documentation/",
|
||||
},
|
||||
# Bad entry is skipped
|
||||
]
|
||||
}
|
||||
|
@ -4508,23 +4508,19 @@ async def test_compile_statistics_hourly_daily_monthly_summary(
|
||||
duration += dur
|
||||
return total / duration
|
||||
|
||||
def _time_weighted_circular_mean(values: list[tuple[float, int]]):
|
||||
def _weighted_circular_mean(
|
||||
values: Iterable[tuple[float, float]],
|
||||
) -> tuple[float, float]:
|
||||
sin_sum = 0
|
||||
cos_sum = 0
|
||||
for x, dur in values:
|
||||
sin_sum += math.sin(x * DEG_TO_RAD) * dur
|
||||
cos_sum += math.cos(x * DEG_TO_RAD) * dur
|
||||
for x, weight in values:
|
||||
sin_sum += math.sin(x * DEG_TO_RAD) * weight
|
||||
cos_sum += math.cos(x * DEG_TO_RAD) * weight
|
||||
|
||||
return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360
|
||||
|
||||
def _circular_mean(values: list[float]) -> float:
|
||||
sin_sum = 0
|
||||
cos_sum = 0
|
||||
for x in values:
|
||||
sin_sum += math.sin(x * DEG_TO_RAD)
|
||||
cos_sum += math.cos(x * DEG_TO_RAD)
|
||||
|
||||
return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360
|
||||
return (
|
||||
(RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360,
|
||||
math.sqrt(sin_sum**2 + cos_sum**2),
|
||||
)
|
||||
|
||||
def _min(seq, last_state):
|
||||
if last_state is None:
|
||||
@ -4631,7 +4627,7 @@ async def test_compile_statistics_hourly_daily_monthly_summary(
|
||||
values = [(seq, durations[j]) for j, seq in enumerate(seq)]
|
||||
if (state := last_states["sensor.test5"]) is not None:
|
||||
values.append((state, 5))
|
||||
expected_means["sensor.test5"].append(_time_weighted_circular_mean(values))
|
||||
expected_means["sensor.test5"].append(_weighted_circular_mean(values))
|
||||
last_states["sensor.test5"] = seq[-1]
|
||||
|
||||
start += timedelta(minutes=5)
|
||||
@ -4733,15 +4729,17 @@ async def test_compile_statistics_hourly_daily_monthly_summary(
|
||||
start = zero
|
||||
end = zero + timedelta(minutes=5)
|
||||
for i in range(24):
|
||||
for entity_id in (
|
||||
"sensor.test1",
|
||||
"sensor.test2",
|
||||
"sensor.test3",
|
||||
"sensor.test4",
|
||||
"sensor.test5",
|
||||
for entity_id, mean_extractor in (
|
||||
("sensor.test1", lambda x: x),
|
||||
("sensor.test2", lambda x: x),
|
||||
("sensor.test3", lambda x: x),
|
||||
("sensor.test4", lambda x: x),
|
||||
("sensor.test5", lambda x: x[0]),
|
||||
):
|
||||
expected_average = (
|
||||
expected_means[entity_id][i] if entity_id in expected_means else None
|
||||
mean_extractor(expected_means[entity_id][i])
|
||||
if entity_id in expected_means
|
||||
else None
|
||||
)
|
||||
expected_minimum = (
|
||||
expected_minima[entity_id][i] if entity_id in expected_minima else None
|
||||
@ -4772,7 +4770,7 @@ async def test_compile_statistics_hourly_daily_monthly_summary(
|
||||
assert stats == expected_stats
|
||||
|
||||
def verify_stats(
|
||||
period: Literal["5minute", "day", "hour", "week", "month"],
|
||||
period: Literal["hour", "day", "week", "month"],
|
||||
start: datetime,
|
||||
next_datetime: Callable[[datetime], datetime],
|
||||
) -> None:
|
||||
@ -4791,7 +4789,7 @@ async def test_compile_statistics_hourly_daily_monthly_summary(
|
||||
("sensor.test2", mean),
|
||||
("sensor.test3", mean),
|
||||
("sensor.test4", mean),
|
||||
("sensor.test5", _circular_mean),
|
||||
("sensor.test5", lambda x: _weighted_circular_mean(x)[0]),
|
||||
):
|
||||
expected_average = (
|
||||
mean_fn(expected_means[entity_id][i * 12 : (i + 1) * 12])
|
||||
|
@ -473,7 +473,7 @@
|
||||
"timestamp": "2024-09-10T10:26:28.781Z"
|
||||
},
|
||||
"acOptionalMode": {
|
||||
"value": "off",
|
||||
"value": "windFree",
|
||||
"timestamp": "2025-02-09T09:14:39.642Z"
|
||||
}
|
||||
},
|
||||
|
@ -211,7 +211,7 @@
|
||||
]),
|
||||
'max_temp': 35,
|
||||
'min_temp': 7,
|
||||
'preset_mode': None,
|
||||
'preset_mode': 'windFree',
|
||||
'preset_modes': list([
|
||||
'windFree',
|
||||
]),
|
||||
|
@ -1065,7 +1065,7 @@
|
||||
'custom.airConditionerOptionalMode': dict({
|
||||
'acOptionalMode': dict({
|
||||
'timestamp': '2025-02-09T09:14:39.642Z',
|
||||
'value': 'off',
|
||||
'value': 'windFree',
|
||||
}),
|
||||
'supportedAcOptionalMode': dict({
|
||||
'timestamp': '2024-09-10T10:26:28.781Z',
|
||||
|
@ -82,6 +82,15 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = {
|
||||
"data": {"code": "{{ this.entity_id }}"},
|
||||
},
|
||||
}
|
||||
EMPTY_ACTIONS = {
|
||||
"arm_away": [],
|
||||
"arm_home": [],
|
||||
"arm_night": [],
|
||||
"arm_vacation": [],
|
||||
"arm_custom_bypass": [],
|
||||
"disarm": [],
|
||||
"trigger": [],
|
||||
}
|
||||
|
||||
|
||||
TEMPLATE_ALARM_CONFIG = {
|
||||
@ -173,6 +182,12 @@ async def test_setup_config_entry(
|
||||
"panels": {"test_template_panel": OPTIMISTIC_TEMPLATE_ALARM_CONFIG},
|
||||
}
|
||||
},
|
||||
{
|
||||
"alarm_control_panel": {
|
||||
"platform": "template",
|
||||
"panels": {"test_template_panel": EMPTY_ACTIONS},
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("start_ha")
|
||||
|
@ -93,6 +93,45 @@ async def test_missing_optional_config(hass: HomeAssistant) -> None:
|
||||
_verify(hass, STATE_UNKNOWN)
|
||||
|
||||
|
||||
async def test_missing_emtpy_press_action_config(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test: missing optional template is ok."""
|
||||
with assert_setup_component(1, "template"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": {
|
||||
"button": {
|
||||
"press": [],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
_verify(hass, STATE_UNKNOWN)
|
||||
|
||||
now = dt.datetime.now(dt.UTC)
|
||||
freezer.move_to(now)
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{CONF_ENTITY_ID: _TEST_BUTTON},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
_verify(
|
||||
hass,
|
||||
now.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
async def test_missing_required_keys(hass: HomeAssistant) -> None:
|
||||
"""Test: missing required fields will fail."""
|
||||
with assert_setup_component(0, "template"):
|
||||
|
@ -9,6 +9,7 @@ from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
ATTR_TILT_POSITION,
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
CoverEntityFeature,
|
||||
CoverState,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@ -28,6 +29,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import assert_setup_component
|
||||
|
||||
@ -1123,3 +1125,50 @@ async def test_self_referencing_icon_with_no_template_is_not_a_loop(
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
assert "Template loop detected" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("script", "supported_feature"),
|
||||
[
|
||||
("stop_cover", CoverEntityFeature.STOP),
|
||||
("set_cover_position", CoverEntityFeature.SET_POSITION),
|
||||
(
|
||||
"set_cover_tilt_position",
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.STOP_TILT
|
||||
| CoverEntityFeature.SET_TILT_POSITION,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_emtpy_action_config(
|
||||
hass: HomeAssistant, script: str, supported_feature: CoverEntityFeature
|
||||
) -> None:
|
||||
"""Test configuration with empty script."""
|
||||
with assert_setup_component(1, COVER_DOMAIN):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
COVER_DOMAIN,
|
||||
{
|
||||
COVER_DOMAIN: {
|
||||
"platform": "template",
|
||||
"covers": {
|
||||
"test_template_cover": {
|
||||
"open_cover": [],
|
||||
"close_cover": [],
|
||||
script: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("cover.test_template_cover")
|
||||
assert (
|
||||
state.attributes["supported_features"]
|
||||
== CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | supported_feature
|
||||
)
|
||||
|
@ -556,6 +556,42 @@ async def setup_single_action_light(
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_empty_action_light(
|
||||
hass: HomeAssistant,
|
||||
count: int,
|
||||
style: ConfigurationStyle,
|
||||
action: str,
|
||||
extra_config: dict,
|
||||
) -> None:
|
||||
"""Do setup of light integration."""
|
||||
if style == ConfigurationStyle.LEGACY:
|
||||
await async_setup_legacy_format(
|
||||
hass,
|
||||
count,
|
||||
{
|
||||
"test_template_light": {
|
||||
"turn_on": [],
|
||||
"turn_off": [],
|
||||
action: [],
|
||||
**extra_config,
|
||||
}
|
||||
},
|
||||
)
|
||||
elif style == ConfigurationStyle.MODERN:
|
||||
await async_setup_new_format(
|
||||
hass,
|
||||
count,
|
||||
{
|
||||
"name": "test_template_light",
|
||||
"turn_on": [],
|
||||
"turn_off": [],
|
||||
action: [],
|
||||
**extra_config,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_light_with_effects(
|
||||
hass: HomeAssistant,
|
||||
@ -2404,3 +2440,82 @@ async def test_nested_unique_id(
|
||||
entry = entity_registry.async_get("light.test_b")
|
||||
assert entry
|
||||
assert entry.unique_id == "x-b"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "extra_config"), [(1, {})])
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[
|
||||
ConfigurationStyle.LEGACY,
|
||||
ConfigurationStyle.MODERN,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("action", "color_mode"),
|
||||
[
|
||||
("set_level", ColorMode.BRIGHTNESS),
|
||||
("set_temperature", ColorMode.COLOR_TEMP),
|
||||
("set_hs", ColorMode.HS),
|
||||
("set_rgb", ColorMode.RGB),
|
||||
("set_rgbw", ColorMode.RGBW),
|
||||
("set_rgbww", ColorMode.RGBWW),
|
||||
],
|
||||
)
|
||||
async def test_empty_color_mode_action_config(
|
||||
hass: HomeAssistant,
|
||||
color_mode: ColorMode,
|
||||
setup_empty_action_light,
|
||||
) -> None:
|
||||
"""Test empty actions for color mode actions."""
|
||||
state = hass.states.get("light.test_template_light")
|
||||
assert state.attributes["supported_color_modes"] == [color_mode]
|
||||
|
||||
await hass.services.async_call(
|
||||
light.DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "light.test_template_light"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get("light.test_template_light")
|
||||
assert state.state == STATE_ON
|
||||
|
||||
await hass.services.async_call(
|
||||
light.DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "light.test_template_light"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get("light.test_template_light")
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count"), [1])
|
||||
@pytest.mark.parametrize(
|
||||
("style", "extra_config"),
|
||||
[
|
||||
(
|
||||
ConfigurationStyle.LEGACY,
|
||||
{
|
||||
"effect_list_template": "{{ ['a'] }}",
|
||||
"effect_template": "{{ 'a' }}",
|
||||
},
|
||||
),
|
||||
(
|
||||
ConfigurationStyle.MODERN,
|
||||
{
|
||||
"effect_list": "{{ ['a'] }}",
|
||||
"effect": "{{ 'a' }}",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("action", ["set_effect"])
|
||||
async def test_effect_with_empty_action(
|
||||
hass: HomeAssistant,
|
||||
setup_empty_action_light,
|
||||
) -> None:
|
||||
"""Test empty set_effect action."""
|
||||
state = hass.states.get("light.test_template_light")
|
||||
assert state.attributes["supported_features"] == LightEntityFeature.EFFECT
|
||||
|
@ -4,7 +4,7 @@ import pytest
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components import lock
|
||||
from homeassistant.components.lock import LockState
|
||||
from homeassistant.components.lock import LockEntityFeature, LockState
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE,
|
||||
ATTR_ENTITY_ID,
|
||||
@ -15,6 +15,8 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from tests.common import assert_setup_component
|
||||
|
||||
OPTIMISTIC_LOCK_CONFIG = {
|
||||
"platform": "template",
|
||||
"lock": {
|
||||
@ -718,3 +720,50 @@ async def test_unique_id(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all("lock")) == 1
|
||||
|
||||
|
||||
async def test_emtpy_action_config(hass: HomeAssistant) -> None:
|
||||
"""Test configuration with empty script."""
|
||||
with assert_setup_component(1, lock.DOMAIN):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
lock.DOMAIN,
|
||||
{
|
||||
lock.DOMAIN: {
|
||||
"platform": "template",
|
||||
"value_template": "{{ 0 == 1 }}",
|
||||
"lock": [],
|
||||
"unlock": [],
|
||||
"open": [],
|
||||
"name": "test_template_lock",
|
||||
"optimistic": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("lock.test_template_lock")
|
||||
assert state.attributes["supported_features"] == LockEntityFeature.OPEN
|
||||
|
||||
await hass.services.async_call(
|
||||
lock.DOMAIN,
|
||||
lock.SERVICE_UNLOCK,
|
||||
{ATTR_ENTITY_ID: "lock.test_template_lock"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("lock.test_template_lock")
|
||||
assert state.state == LockState.UNLOCKED
|
||||
|
||||
await hass.services.async_call(
|
||||
lock.DOMAIN,
|
||||
lock.SERVICE_LOCK,
|
||||
{ATTR_ENTITY_ID: "lock.test_template_lock"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("lock.test_template_lock")
|
||||
assert state.state == LockState.LOCKED
|
||||
|
@ -1,8 +1,12 @@
|
||||
"""The tests for the Template number platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components import number, template
|
||||
from homeassistant.components.input_number import (
|
||||
ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE,
|
||||
DOMAIN as INPUT_NUMBER_DOMAIN,
|
||||
@ -18,6 +22,7 @@ from homeassistant.components.number import (
|
||||
)
|
||||
from homeassistant.components.template import DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_ICON,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
@ -25,10 +30,14 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import ConfigurationStyle
|
||||
|
||||
from tests.common import MockConfigEntry, assert_setup_component, async_capture_events
|
||||
|
||||
_TEST_NUMBER = "number.template_number"
|
||||
_TEST_OBJECT_ID = "template_number"
|
||||
_TEST_NUMBER = f"number.{_TEST_OBJECT_ID}"
|
||||
# Represent for number's value
|
||||
_VALUE_INPUT_NUMBER = "input_number.value"
|
||||
# Represent for number's minimum
|
||||
@ -50,6 +59,38 @@ _VALUE_INPUT_NUMBER_CONFIG = {
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_modern_format(
|
||||
hass: HomeAssistant, count: int, number_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Do setup of number integration via new format."""
|
||||
config = {"template": {"number": number_config}}
|
||||
|
||||
with assert_setup_component(count, template.DOMAIN):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
template.DOMAIN,
|
||||
config,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_number(
|
||||
hass: HomeAssistant,
|
||||
count: int,
|
||||
style: ConfigurationStyle,
|
||||
number_config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Do setup of number integration."""
|
||||
if style == ConfigurationStyle.MODERN:
|
||||
await async_setup_modern_format(
|
||||
hass, count, {"name": _TEST_OBJECT_ID, **number_config}
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_config_entry(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
@ -565,3 +606,36 @@ async def test_device_id(
|
||||
template_entity = entity_registry.async_get("number.my_template")
|
||||
assert template_entity is not None
|
||||
assert template_entity.device_id == device_entry.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "number_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"state": "{{ 1 }}",
|
||||
"set_value": [],
|
||||
"step": "{{ 1 }}",
|
||||
"optimistic": True,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[
|
||||
ConfigurationStyle.MODERN,
|
||||
],
|
||||
)
|
||||
async def test_empty_action_config(hass: HomeAssistant, setup_number) -> None:
|
||||
"""Test configuration with empty script."""
|
||||
await hass.services.async_call(
|
||||
number.DOMAIN,
|
||||
number.SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: _TEST_NUMBER, "value": 4},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(_TEST_NUMBER)
|
||||
assert float(state.state) == 4
|
||||
|
@ -1,8 +1,12 @@
|
||||
"""The tests for the Template select platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components import select, template
|
||||
from homeassistant.components.input_select import (
|
||||
ATTR_OPTION as INPUT_SELECT_ATTR_OPTION,
|
||||
ATTR_OPTIONS as INPUT_SELECT_ATTR_OPTIONS,
|
||||
@ -17,17 +21,53 @@ from homeassistant.components.select import (
|
||||
SERVICE_SELECT_OPTION as SELECT_SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.components.template import DOMAIN
|
||||
from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import ConfigurationStyle
|
||||
|
||||
from tests.common import MockConfigEntry, assert_setup_component, async_capture_events
|
||||
|
||||
_TEST_SELECT = "select.template_select"
|
||||
_TEST_OBJECT_ID = "template_select"
|
||||
_TEST_SELECT = f"select.{_TEST_OBJECT_ID}"
|
||||
# Represent for select's current_option
|
||||
_OPTION_INPUT_SELECT = "input_select.option"
|
||||
|
||||
|
||||
async def async_setup_modern_format(
|
||||
hass: HomeAssistant, count: int, select_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Do setup of select integration via new format."""
|
||||
config = {"template": {"select": select_config}}
|
||||
|
||||
with assert_setup_component(count, template.DOMAIN):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
template.DOMAIN,
|
||||
config,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_select(
|
||||
hass: HomeAssistant,
|
||||
count: int,
|
||||
style: ConfigurationStyle,
|
||||
select_config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Do setup of select integration."""
|
||||
if style == ConfigurationStyle.MODERN:
|
||||
await async_setup_modern_format(
|
||||
hass, count, {"name": _TEST_OBJECT_ID, **select_config}
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_config_entry(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
@ -527,3 +567,36 @@ async def test_device_id(
|
||||
template_entity = entity_registry.async_get("select.my_template")
|
||||
assert template_entity is not None
|
||||
assert template_entity.device_id == device_entry.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "select_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"state": "{{ 'b' }}",
|
||||
"select_option": [],
|
||||
"options": "{{ ['a', 'b'] }}",
|
||||
"optimistic": True,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[
|
||||
ConfigurationStyle.MODERN,
|
||||
],
|
||||
)
|
||||
async def test_empty_action_config(hass: HomeAssistant, setup_select) -> None:
|
||||
"""Test configuration with empty script."""
|
||||
await hass.services.async_call(
|
||||
select.DOMAIN,
|
||||
select.SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: _TEST_SELECT, "option": "a"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(_TEST_SELECT)
|
||||
assert state.state == "a"
|
||||
|
@ -981,3 +981,49 @@ async def test_device_id(
|
||||
template_entity = entity_registry.async_get("switch.my_template")
|
||||
assert template_entity is not None
|
||||
assert template_entity.device_id == device_entry.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize("count", [1])
|
||||
@pytest.mark.parametrize(
|
||||
("style", "switch_config"),
|
||||
[
|
||||
(
|
||||
ConfigurationStyle.LEGACY,
|
||||
{
|
||||
TEST_OBJECT_ID: {
|
||||
"turn_on": [],
|
||||
"turn_off": [],
|
||||
},
|
||||
},
|
||||
),
|
||||
(
|
||||
ConfigurationStyle.MODERN,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
"turn_on": [],
|
||||
"turn_off": [],
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_empty_action_config(hass: HomeAssistant, setup_switch) -> None:
|
||||
"""Test configuration with empty script."""
|
||||
await hass.services.async_call(
|
||||
switch.DOMAIN,
|
||||
switch.SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
await hass.services.async_call(
|
||||
switch.DOMAIN,
|
||||
switch.SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
|
@ -1,18 +1,29 @@
|
||||
"""The tests for the Template vacuum platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components.vacuum import ATTR_BATTERY_LEVEL, VacuumActivity
|
||||
from homeassistant.components import vacuum
|
||||
from homeassistant.components.vacuum import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
VacuumActivity,
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import ConfigurationStyle
|
||||
|
||||
from tests.common import assert_setup_component
|
||||
from tests.components.vacuum import common
|
||||
|
||||
_TEST_VACUUM = "vacuum.test_vacuum"
|
||||
_TEST_OBJECT_ID = "test_vacuum"
|
||||
_TEST_VACUUM = f"vacuum.{_TEST_OBJECT_ID}"
|
||||
_STATE_INPUT_SELECT = "input_select.state"
|
||||
_SPOT_CLEANING_INPUT_BOOLEAN = "input_boolean.spot_cleaning"
|
||||
_LOCATING_INPUT_BOOLEAN = "input_boolean.locating"
|
||||
@ -20,6 +31,50 @@ _FAN_SPEED_INPUT_SELECT = "input_select.fan_speed"
|
||||
_BATTERY_LEVEL_INPUT_NUMBER = "input_number.battery_level"
|
||||
|
||||
|
||||
async def async_setup_legacy_format(
|
||||
hass: HomeAssistant, count: int, vacuum_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Do setup of number integration via new format."""
|
||||
config = {"vacuum": {"platform": "template", "vacuums": vacuum_config}}
|
||||
|
||||
with assert_setup_component(count, vacuum.DOMAIN):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
vacuum.DOMAIN,
|
||||
config,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_vacuum(
|
||||
hass: HomeAssistant,
|
||||
count: int,
|
||||
style: ConfigurationStyle,
|
||||
vacuum_config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Do setup of number integration."""
|
||||
if style == ConfigurationStyle.LEGACY:
|
||||
await async_setup_legacy_format(hass, count, vacuum_config)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_test_vacuum_with_extra_config(
|
||||
hass: HomeAssistant,
|
||||
count: int,
|
||||
style: ConfigurationStyle,
|
||||
vacuum_config: dict[str, Any],
|
||||
extra_config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Do setup of number integration."""
|
||||
config = {_TEST_OBJECT_ID: {**vacuum_config, **extra_config}}
|
||||
if style == ConfigurationStyle.LEGACY:
|
||||
await async_setup_legacy_format(hass, count, config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, "vacuum")])
|
||||
@pytest.mark.parametrize(
|
||||
("parm1", "parm2", "config"),
|
||||
@ -697,3 +752,71 @@ async def _register_components(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("count", [1])
|
||||
@pytest.mark.parametrize(
|
||||
("style", "vacuum_config"),
|
||||
[
|
||||
(
|
||||
ConfigurationStyle.LEGACY,
|
||||
{
|
||||
"start": [],
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("extra_config", "supported_features"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"pause": [],
|
||||
},
|
||||
VacuumEntityFeature.PAUSE,
|
||||
),
|
||||
(
|
||||
{
|
||||
"stop": [],
|
||||
},
|
||||
VacuumEntityFeature.STOP,
|
||||
),
|
||||
(
|
||||
{
|
||||
"return_to_base": [],
|
||||
},
|
||||
VacuumEntityFeature.RETURN_HOME,
|
||||
),
|
||||
(
|
||||
{
|
||||
"clean_spot": [],
|
||||
},
|
||||
VacuumEntityFeature.CLEAN_SPOT,
|
||||
),
|
||||
(
|
||||
{
|
||||
"locate": [],
|
||||
},
|
||||
VacuumEntityFeature.LOCATE,
|
||||
),
|
||||
(
|
||||
{
|
||||
"set_fan_speed": [],
|
||||
},
|
||||
VacuumEntityFeature.FAN_SPEED,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_empty_action_config(
|
||||
hass: HomeAssistant,
|
||||
supported_features: VacuumEntityFeature,
|
||||
setup_test_vacuum_with_extra_config,
|
||||
) -> None:
|
||||
"""Test configuration with empty script."""
|
||||
await common.async_start(hass, _TEST_VACUUM)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_TEST_VACUUM)
|
||||
assert state.attributes["supported_features"] == (
|
||||
VacuumEntityFeature.STATE | VacuumEntityFeature.START | supported_features
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Test the Tesla Fleet init."""
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiohttp import RequestInfo
|
||||
@ -231,57 +232,58 @@ async def test_vehicle_sleep(
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test coordinator refresh with an error."""
|
||||
await setup_platform(hass, normal_config_entry)
|
||||
assert mock_vehicle_data.call_count == 1
|
||||
|
||||
freezer.tick(VEHICLE_WAIT + VEHICLE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# Let vehicle sleep, no updates for 15 minutes
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 2
|
||||
TEST_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
freezer.tick(VEHICLE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# No polling, call_count should not increase
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 2
|
||||
with patch(
|
||||
"homeassistant.components.tesla_fleet.coordinator.VEHICLE_INTERVAL",
|
||||
TEST_INTERVAL,
|
||||
):
|
||||
await setup_platform(hass, normal_config_entry)
|
||||
assert mock_vehicle_data.call_count == 1
|
||||
|
||||
freezer.tick(VEHICLE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# No polling, call_count should not increase
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 2
|
||||
freezer.tick(VEHICLE_WAIT + TEST_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# Let vehicle sleep, no updates for 15 minutes
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 2
|
||||
|
||||
freezer.tick(VEHICLE_WAIT)
|
||||
async_fire_time_changed(hass)
|
||||
# Vehicle didn't sleep, go back to normal
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 3
|
||||
freezer.tick(TEST_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# No polling, call_count should not increase
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 2
|
||||
|
||||
freezer.tick(VEHICLE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# Regular polling
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 4
|
||||
freezer.tick(VEHICLE_WAIT)
|
||||
async_fire_time_changed(hass)
|
||||
# Vehicle didn't sleep, go back to normal
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 3
|
||||
|
||||
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
|
||||
freezer.tick(VEHICLE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# Vehicle active
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 5
|
||||
freezer.tick(TEST_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# Regular polling
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 4
|
||||
|
||||
freezer.tick(VEHICLE_WAIT)
|
||||
async_fire_time_changed(hass)
|
||||
# Dont let sleep when active
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 6
|
||||
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
|
||||
freezer.tick(TEST_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# Vehicle active
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 5
|
||||
|
||||
freezer.tick(VEHICLE_WAIT)
|
||||
async_fire_time_changed(hass)
|
||||
# Dont let sleep when active
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 7
|
||||
freezer.tick(TEST_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# Dont let sleep when active
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 6
|
||||
|
||||
freezer.tick(TEST_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
# Dont let sleep when active
|
||||
await hass.async_block_till_done()
|
||||
assert mock_vehicle_data.call_count == 7
|
||||
|
||||
|
||||
# Test Energy Live Coordinator
|
||||
|
@ -49,7 +49,8 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None:
|
||||
"product": "SkyConnect v1.0",
|
||||
"firmware": "ezsp",
|
||||
},
|
||||
version=2,
|
||||
version=1,
|
||||
minor_version=4,
|
||||
domain=SKYCONNECT_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant SkyConnect",
|
||||
@ -66,7 +67,8 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None:
|
||||
"product": "Home Assistant Connect ZBT-1",
|
||||
"firmware": "ezsp",
|
||||
},
|
||||
version=2,
|
||||
version=1,
|
||||
minor_version=4,
|
||||
domain=SKYCONNECT_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Connect ZBT-1",
|
||||
|
@ -8883,15 +8883,17 @@ async def test_add_description_placeholder_automatically_not_overwrites(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "expected_log"),
|
||||
("domain", "source", "expected_log"),
|
||||
[
|
||||
("some_integration", True),
|
||||
("mobile_app", False),
|
||||
("some_integration", config_entries.SOURCE_USER, True),
|
||||
("some_integration", config_entries.SOURCE_IGNORE, False),
|
||||
("mobile_app", config_entries.SOURCE_USER, False),
|
||||
],
|
||||
)
|
||||
async def test_create_entry_existing_unique_id(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
source: str,
|
||||
expected_log: bool,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
@ -8902,6 +8904,7 @@ async def test_create_entry_existing_unique_id(
|
||||
entry_id="01J915Q6T9F6G5V0QJX6HBC94T",
|
||||
data={"host": "any", "port": 123},
|
||||
unique_id="mock-unique-id",
|
||||
source=source,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user