Compare commits

..

61 Commits

Author SHA1 Message Date
Franck Nijhof a5eb816dcf 2024.12.4 (#133422) 2024-12-17 15:28:28 +01:00
Franck Nijhof 517f3faa0a Bump version to 2024.12.4 2024-12-17 12:14:26 +00:00
G Johansson b4015805f7 Bump holidays to 0.63 (#133391) 2024-12-17 12:14:04 +00:00
Jonas Fors Lellky a56ad0273b Fix fan setpoints for flexit_bacnet (#133388) 2024-12-17 12:14:00 +00:00
Maciej Bieniek 2bc917c842 Bump imgw-pib to version 1.0.7 (#133364) 2024-12-17 12:13:57 +00:00
Michael 97f22b3a3d Allow load_verify_locations with only cadata passed (#133299) 2024-12-17 12:13:53 +00:00
J. Nick Koston a48a5adc81 Set code_arm_required to False for homekit_controller (#133284) 2024-12-17 12:13:50 +00:00
J. Nick Koston eb86b00dd4 Bump yalexs-ble to 2.5.5 (#133229)
changelog: https://github.com/bdraco/yalexs-ble/compare/v2.5.4...v2.5.5
2024-12-17 12:13:47 +00:00
Jan Bouwhuis e93256951e Bump incomfort-client to v0.6.4 (#133205) 2024-12-17 12:13:43 +00:00
Erik Montnemery 3b0ab421b0 Revert "Improve recorder history queries (#131702)" (#133203) 2024-12-17 12:11:13 +00:00
Erik Montnemery ca47253d81 Revert "Simplify recorder RecorderRunsManager" (#133201)
Revert "Simplify recorder RecorderRunsManager (#131785)"

This reverts commit cf0ee63507.
2024-12-17 12:08:41 +00:00
Avi Miller 9b0a489753 Bump aiolifx to 1.1.2 and add new HomeKit product prefixes (#133191)
Signed-off-by: Avi Miller <me@dje.li>
2024-12-17 11:44:54 +00:00
Conor Eager 9b02db008e Bump starlink-grpc-core to 1.2.1 to fix missing ping (#133183) 2024-12-17 11:44:50 +00:00
J. Nick Koston 223817a7fb Bump yalexs-ble to 2.5.4 (#133172) 2024-12-17 11:44:47 +00:00
G Johansson cdea9b5d3a Fix strptime in python_script (#133159)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2024-12-17 11:44:41 +00:00
Joost Lekkerkerker 8286ec9e60 Bump yt-dlp to 2024.12.13 (#133129) 2024-12-17 11:44:38 +00:00
rappenze cce7b9ac34 Fix fibaro climate hvac mode (#132508) 2024-12-17 11:44:34 +00:00
Erik Montnemery a42c0230c9 Simplify recorder RecorderRunsManager (#131785) 2024-12-17 11:44:30 +00:00
Franck Nijhof 46db3964f3 2024.12.3 (#133123) 2024-12-13 12:16:14 +01:00
Franck Nijhof 9b83a00285 Bump version to 2024.12.3 2024-12-13 11:04:47 +01:00
Joost Lekkerkerker 9a7fda5b25 Bump aiowithings to 3.1.4 (#133117) 2024-12-13 11:04:34 +01:00
Robert Resch f9bdc29546 Bump deebot-client to 9.4.0 (#133114) 2024-12-13 11:04:31 +01:00
Brandon Rothweiler d9bb1f6035 Bump py-aosmith to 1.0.12 (#133100) 2024-12-13 11:04:28 +01:00
David Bonnes 01359b32c4 Bugfix to use evohome's new hostname (#133085) 2024-12-13 11:04:25 +01:00
jb101010-2 d0c00aaa67 Bump pysuezV2 to 1.3.5 (#133076) 2024-12-13 11:04:22 +01:00
Bram Kragten 73465a7aa8 Update frontend to 20241127.8 (#133066) 2024-12-13 11:04:19 +01:00
Franck Nijhof ed03c0a294 Fix LaMetric config flow for cloud import path (#133039) 2024-12-13 11:04:16 +01:00
Michael Hansen b38a7186d2 Change warning to debug for VAD timeout (#132987) 2024-12-13 11:04:13 +01:00
J. Nick Koston 31348930cc Bump led-ble to 1.1.1 (#132977)
changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v1.0.2...v1.1.1
2024-12-13 11:04:09 +01:00
Simone Chemelli 83e1353c01 Guard Vodafone Station updates against bad data (#132921)
guard Vodafone Station updates against bad data
2024-12-13 11:04:07 +01:00
Simone Chemelli ede9c3ecd2 fix AndroidTV logging when disconnected (#132919) 2024-12-13 11:04:04 +01:00
Michael Hansen c08ffcff9b Fix pipeline conversation language (#132896) 2024-12-13 11:04:01 +01:00
Stefano Angeleri 038115fea2 Bump pydaikin to 2.13.8 (#132759) 2024-12-13 11:03:58 +01:00
Simon Lamon 4e5ceb3aa4 Bump python-linkplay to v0.1.1 (#132091) 2024-12-13 11:03:53 +01:00
Franck Nijhof 3fe2c14a79 2024.12.2 (#132846) 2024-12-10 21:45:06 +01:00
Franck Nijhof 238cf691a4 Bump version to 2024.12.2 2024-12-10 15:07:18 +01:00
Josef Zweck 5a5bb139fa Bump aioacaia to 0.1.11 (#132838) 2024-12-10 14:59:48 +01:00
Robert Resch 01a9a58327 Bump deebot-client to 9.3.0 (#132834) 2024-12-10 14:59:45 +01:00
David Knowles fc34c6181c Pass an application identifier to the Hydrawise API (#132779) 2024-12-10 14:59:42 +01:00
David Knowles 60e8a38ba3 Catch Hydrawise authorization errors in the correct place (#132727) 2024-12-10 14:59:37 +01:00
starkillerOG e4765c40fe Bump reolink-aio to 0.11.5 (#132757) 2024-12-10 14:55:48 +01:00
Bram Kragten e239871566 Update frontend to 20241127.7 (#132729)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2024-12-10 14:55:43 +01:00
Michael Hansen c8e5a6df5d Bump intents to 2024.12.9 (#132726) 2024-12-10 14:54:48 +01:00
Simone Rescio cac4eef795 Revert "Bump pyezviz to 0.2.2.3" (#132715) 2024-12-10 14:51:04 +01:00
Joost Lekkerkerker 8fc50c776e Bump yt-dlp to 2024.12.06 (#132684) 2024-12-10 14:51:00 +01:00
Bouwe Westerdijk da344a44e5 Bump plugwise to v1.6.3 (#132673) 2024-12-10 14:50:57 +01:00
puddly 1993142e44 Bump ZHA dependencies (#132630) 2024-12-10 14:50:54 +01:00
Thomas55555 382d32c7a7 Fix config flow in Husqvarna Automower (#132615) 2024-12-10 14:50:50 +01:00
Bouwe Westerdijk ef89563bad Bump plugwise to v1.6.2 and adapt (#132608) 2024-12-10 14:50:46 +01:00
Bouwe Westerdijk 26012ac922 Bump plugwise to v1.6.1 (#131950) 2024-12-10 14:50:42 +01:00
J. Nick Koston a33c69a2a2 Bump yalexs-ble to 2.5.2 (#132560) 2024-12-10 14:45:34 +01:00
Franck Nijhof 0096ffb659 Update twentemilieu to 2.2.0 (#132554) 2024-12-10 14:45:31 +01:00
Robert Svensson db141ce449 Bump aiounifi to v81 to fix partitioned cookies on python 3.13 (#132540) 2024-12-10 14:45:26 +01:00
Austin Mroczek af5f718a71 bump total_connect_client to 2023.12 (#132531) 2024-12-10 14:43:52 +01:00
Franck Nijhof f1284178ed Update debugpy to 1.8.8 (#132519) 2024-12-10 14:41:15 +01:00
J. Nick Koston b0005cedff Bump pycups to 2.0.4 (#132514) 2024-12-10 14:41:11 +01:00
Erwin Douna 5d01f7db85 Fix PyTado dependency (#132510) 2024-12-10 14:41:08 +01:00
Alex d6a4a7f052 Update pyrisco to 0.6.5 (#132493) 2024-12-10 14:41:02 +01:00
Ravaka Razafimanantsoa 1f6c5b4d8b Fix API change for AC not supporting floats in SwitchBot Cloud (#132231) 2024-12-10 14:36:09 +01:00
David Knowles 4e56f9c014 Bump pydrawise to 2024.12.0 (#132015) 2024-12-10 14:36:06 +01:00
Åke Strandberg f343dce418 Enable additional entities on myUplink model SMO20 (#131688)
* Add a couple of entities to SMO 20

* Enable additional entities on SMO20
2024-12-10 14:35:58 +01:00
90 changed files with 776 additions and 338 deletions
+7 -1
View File
@@ -50,6 +50,12 @@ def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
return False
def _check_load_verify_locations_call_allowed(mapped_args: dict[str, Any]) -> bool:
# If only cadata is passed, we can ignore it
kwargs = mapped_args.get("kwargs")
return bool(kwargs and len(kwargs) == 1 and "cadata" in kwargs)
@dataclass(slots=True, frozen=True)
class BlockingCall:
"""Class to hold information about a blocking call."""
@@ -158,7 +164,7 @@ _BLOCKING_CALLS: tuple[BlockingCall, ...] = (
original_func=SSLContext.load_verify_locations,
object=SSLContext,
function="load_verify_locations",
check_allowed=None,
check_allowed=_check_load_verify_locations_call_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
+1 -1
View File
@@ -25,5 +25,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aioacaia"],
"requirements": ["aioacaia==0.1.10"]
"requirements": ["aioacaia==0.1.11"]
}
+10 -9
View File
@@ -135,15 +135,16 @@ async def async_connect_androidtv(
)
aftv = await async_androidtv_setup(
config[CONF_HOST],
config[CONF_PORT],
adbkey,
config.get(CONF_ADB_SERVER_IP),
config.get(CONF_ADB_SERVER_PORT, DEFAULT_ADB_SERVER_PORT),
state_detection_rules,
config[CONF_DEVICE_CLASS],
timeout,
signer,
host=config[CONF_HOST],
port=config[CONF_PORT],
adbkey=adbkey,
adb_server_ip=config.get(CONF_ADB_SERVER_IP),
adb_server_port=config.get(CONF_ADB_SERVER_PORT, DEFAULT_ADB_SERVER_PORT),
state_detection_rules=state_detection_rules,
device_class=config[CONF_DEVICE_CLASS],
auth_timeout_s=timeout,
signer=signer,
log_errors=False,
)
if not aftv.available:
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aosmith",
"iot_class": "cloud_polling",
"requirements": ["py-aosmith==1.0.11"]
"requirements": ["py-aosmith==1.0.12"]
}
@@ -29,6 +29,7 @@ from homeassistant.components import (
from homeassistant.components.tts import (
generate_media_source_id as tts_generate_media_source_id,
)
from homeassistant.const import MATCH_ALL
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent
@@ -1009,12 +1010,19 @@ class PipelineRun:
if self.intent_agent is None:
raise RuntimeError("Recognize intent was not prepared")
if self.pipeline.conversation_language == MATCH_ALL:
# LLMs support all languages ('*') so use pipeline language for
# intent fallback.
input_language = self.pipeline.language
else:
input_language = self.pipeline.conversation_language
self.process_event(
PipelineEvent(
PipelineEventType.INTENT_START,
{
"engine": self.intent_agent,
"language": self.pipeline.conversation_language,
"language": input_language,
"intent_input": intent_input,
"conversation_id": conversation_id,
"device_id": device_id,
@@ -1029,7 +1037,7 @@ class PipelineRun:
context=self.context,
conversation_id=conversation_id,
device_id=device_id,
language=self.pipeline.language,
language=input_language,
agent_id=self.intent_agent,
)
processed_locally = self.intent_agent == conversation.HOME_ASSISTANT_AGENT
@@ -140,7 +140,7 @@ class VoiceCommandSegmenter:
self._timeout_seconds_left -= chunk_seconds
if self._timeout_seconds_left <= 0:
_LOGGER.warning(
_LOGGER.debug(
"VAD end of speech detection timed out after %s seconds",
self.timeout_seconds,
)
@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.1"]
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.5"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.4"]
"requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.9"]
}
+1 -1
View File
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/cups",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["pycups==1.9.73"]
"requirements": ["pycups==2.0.4"]
}
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.13.7"],
"requirements": ["pydaikin==2.13.8"],
"zeroconf": ["_dkapi._tcp.local."]
}
@@ -6,5 +6,5 @@
"integration_type": "service",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["debugpy==1.8.6"]
"requirements": ["debugpy==1.8.8"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==9.2.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==9.4.0"]
}
@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==0.4.20"]
"requirements": ["evohome-async==0.4.21"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/ezviz",
"iot_class": "cloud_polling",
"loggers": ["paho_mqtt", "pyezviz"],
"requirements": ["pyezviz==0.2.2.3"]
"requirements": ["pyezviz==0.2.1.2"]
}
+3 -3
View File
@@ -274,7 +274,9 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
if isinstance(fibaro_operation_mode, str):
with suppress(ValueError):
return HVACMode(fibaro_operation_mode.lower())
elif fibaro_operation_mode in OPMODES_HVAC:
# when the mode cannot be instantiated a preset_mode is selected
return HVACMode.AUTO
if fibaro_operation_mode in OPMODES_HVAC:
return OPMODES_HVAC[fibaro_operation_mode]
return None
@@ -282,8 +284,6 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
"""Set new target operation mode."""
if not self._op_mode_device:
return
if self.preset_mode:
return
if "setOperatingMode" in self._op_mode_device.fibaro_device.actions:
self._op_mode_device.action("setOperatingMode", HA_OPMODES_HVAC[hvac_mode])
@@ -29,6 +29,8 @@ class FlexitNumberEntityDescription(NumberEntityDescription):
"""Describes a Flexit number entity."""
native_value_fn: Callable[[FlexitBACnet], float]
native_max_value_fn: Callable[[FlexitBACnet], int]
native_min_value_fn: Callable[[FlexitBACnet], int]
set_native_value_fn: Callable[[FlexitBACnet], Callable[[int], Awaitable[None]]]
@@ -37,121 +39,121 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
key="away_extract_fan_setpoint",
translation_key="away_extract_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_extract_air_away,
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_away,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda device: int(device.fan_setpoint_extract_air_home),
native_min_value_fn=lambda _: 30,
),
FlexitNumberEntityDescription(
key="away_supply_fan_setpoint",
translation_key="away_supply_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_supply_air_away,
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_away,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda device: int(device.fan_setpoint_supply_air_home),
native_min_value_fn=lambda _: 30,
),
FlexitNumberEntityDescription(
key="cooker_hood_extract_fan_setpoint",
translation_key="cooker_hood_extract_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_extract_air_cooker,
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_cooker,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda _: 100,
native_min_value_fn=lambda _: 30,
),
FlexitNumberEntityDescription(
key="cooker_hood_supply_fan_setpoint",
translation_key="cooker_hood_supply_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_supply_air_cooker,
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_cooker,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda _: 100,
native_min_value_fn=lambda _: 30,
),
FlexitNumberEntityDescription(
key="fireplace_extract_fan_setpoint",
translation_key="fireplace_extract_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_extract_air_fire,
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_fire,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda _: 100,
native_min_value_fn=lambda _: 30,
),
FlexitNumberEntityDescription(
key="fireplace_supply_fan_setpoint",
translation_key="fireplace_supply_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_supply_air_fire,
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_fire,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda _: 100,
native_min_value_fn=lambda _: 30,
),
FlexitNumberEntityDescription(
key="high_extract_fan_setpoint",
translation_key="high_extract_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_extract_air_high,
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_high,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda _: 100,
native_min_value_fn=lambda device: int(device.fan_setpoint_extract_air_home),
),
FlexitNumberEntityDescription(
key="high_supply_fan_setpoint",
translation_key="high_supply_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_supply_air_high,
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_high,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda _: 100,
native_min_value_fn=lambda device: int(device.fan_setpoint_supply_air_home),
),
FlexitNumberEntityDescription(
key="home_extract_fan_setpoint",
translation_key="home_extract_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_extract_air_home,
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_home,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda _: 100,
native_min_value_fn=lambda device: int(device.fan_setpoint_extract_air_away),
),
FlexitNumberEntityDescription(
key="home_supply_fan_setpoint",
translation_key="home_supply_fan_setpoint",
device_class=NumberDeviceClass.POWER_FACTOR,
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_value_fn=lambda device: device.fan_setpoint_supply_air_home,
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_home,
native_unit_of_measurement=PERCENTAGE,
native_max_value_fn=lambda _: 100,
native_min_value_fn=lambda device: int(device.fan_setpoint_supply_air_away),
),
)
@@ -192,6 +194,16 @@ class FlexitNumber(FlexitEntity, NumberEntity):
"""Return the state of the number."""
return self.entity_description.native_value_fn(self.coordinator.device)
@property
def native_max_value(self) -> float:
"""Return the native max value of the number."""
return self.entity_description.native_max_value_fn(self.coordinator.device)
@property
def native_min_value(self) -> float:
"""Return the native min value of the number."""
return self.entity_description.native_min_value_fn(self.coordinator.device)
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
set_native_value_fn = self.entity_description.set_native_value_fn(
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20241127.6"]
"requirements": ["home-assistant-frontend==20241127.8"]
}
+2 -5
View File
@@ -22,7 +22,7 @@ import homeassistant.util.dt as dt_util
from . import websocket_api
from .const import DOMAIN
from .helpers import entities_may_have_state_changes_after, has_states_before
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
CONF_ORDER = "use_include_order"
@@ -107,10 +107,7 @@ class HistoryPeriodView(HomeAssistantView):
no_attributes = "no_attributes" in request.query
if (
# has_states_before will return True if there are states older than
# end_time. If it's false, we know there are no states in the
# database up until end_time.
(end_time and not has_states_before(hass, end_time))
(end_time and not has_recorder_run_after(hass, end_time))
or not include_start_time_state
and entity_ids
and not entities_may_have_state_changes_after(
+6 -7
View File
@@ -6,6 +6,7 @@ from collections.abc import Iterable
from datetime import datetime as dt
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import process_timestamp
from homeassistant.core import HomeAssistant
@@ -25,10 +26,8 @@ def entities_may_have_state_changes_after(
return False
def has_states_before(hass: HomeAssistant, run_time: dt) -> bool:
"""Check if the recorder has states as old or older than run_time.
Returns True if there may be such states.
"""
oldest_ts = get_instance(hass).states_manager.oldest_ts
return oldest_ts is not None and run_time.timestamp() >= oldest_ts
def has_recorder_run_after(hass: HomeAssistant, run_time: dt) -> bool:
"""Check if the recorder has any runs after a specific time."""
return run_time >= process_timestamp(
get_instance(hass).recorder_runs_manager.first.start
)
@@ -39,7 +39,7 @@ from homeassistant.util.async_ import create_eager_task
import homeassistant.util.dt as dt_util
from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES
from .helpers import entities_may_have_state_changes_after, has_states_before
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
_LOGGER = logging.getLogger(__name__)
@@ -142,10 +142,7 @@ async def ws_get_history_during_period(
no_attributes = msg["no_attributes"]
if (
# has_states_before will return True if there are states older than
# end_time. If it's false, we know there are no states in the
# database up until end_time.
(end_time and not has_states_before(hass, end_time))
(end_time and not has_recorder_run_after(hass, end_time))
or not include_start_time_state
and entity_ids
and not entities_may_have_state_changes_after(
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.62", "babel==2.15.0"]
"requirements": ["holidays==0.63", "babel==2.15.0"]
}
@@ -69,6 +69,7 @@ class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT
)
_attr_code_arm_required = False
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
@@ -53,10 +53,10 @@ class HusqvarnaConfigFlowHandler(
tz = await dt_util.async_get_time_zone(str(dt_util.DEFAULT_TIME_ZONE))
automower_api = AutomowerSession(AsyncConfigFlowAuth(websession, token), tz)
try:
data = await automower_api.get_status()
status_data = await automower_api.get_status()
except Exception: # noqa: BLE001
return self.async_abort(reason="unknown")
if data == {}:
if status_data == {}:
return self.async_abort(reason="no_mower_connected")
structured_token = structure_token(token[CONF_ACCESS_TOKEN])
@@ -7,7 +7,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import DOMAIN
from .const import APP_ID, DOMAIN
from .coordinator import (
HydrawiseMainDataUpdateCoordinator,
HydrawiseUpdateCoordinators,
@@ -30,7 +30,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
raise ConfigEntryAuthFailed
hydrawise = client.Hydrawise(
auth.Auth(config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD])
auth.Auth(config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD]),
app_id=APP_ID,
)
main_coordinator = HydrawiseMainDataUpdateCoordinator(hass, hydrawise)
@@ -6,14 +6,14 @@ from collections.abc import Callable, Mapping
from typing import Any
from aiohttp import ClientError
from pydrawise import auth, client
from pydrawise import auth as pydrawise_auth, client
from pydrawise.exceptions import NotAuthorizedError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN, LOGGER
from .const import APP_ID, DOMAIN, LOGGER
class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -29,16 +29,21 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN):
on_failure: Callable[[str], ConfigFlowResult],
) -> ConfigFlowResult:
"""Create the config entry."""
# Verify that the provided credentials work."""
api = client.Hydrawise(auth.Auth(username, password))
auth = pydrawise_auth.Auth(username, password)
try:
# Don't fetch zones because we don't need them yet.
user = await api.get_user(fetch_zones=False)
await auth.token()
except NotAuthorizedError:
return on_failure("invalid_auth")
except TimeoutError:
return on_failure("timeout_connect")
try:
api = client.Hydrawise(auth, app_id=APP_ID)
# Don't fetch zones because we don't need them yet.
user = await api.get_user(fetch_zones=False)
except TimeoutError:
return on_failure("timeout_connect")
except ClientError as ex:
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", ex)
return on_failure("cannot_connect")
@@ -3,8 +3,12 @@
from datetime import timedelta
import logging
from homeassistant.const import __version__ as HA_VERSION
LOGGER = logging.getLogger(__package__)
APP_ID = f"homeassistant-{HA_VERSION}"
DOMAIN = "hydrawise"
DEFAULT_WATERING_TIME = timedelta(minutes=15)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2024.9.0"]
"requirements": ["pydrawise==2024.12.0"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"requirements": ["imgw_pib==1.0.6"]
"requirements": ["imgw_pib==1.0.7"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/incomfort",
"iot_class": "local_polling",
"loggers": ["incomfortclient"],
"requirements": ["incomfort-client==0.6.3-1"]
"requirements": ["incomfort-client==0.6.4"]
}
@@ -249,7 +249,10 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
device = await lametric.device()
if self.source != SOURCE_REAUTH:
await self.async_set_unique_id(device.serial_number)
await self.async_set_unique_id(
device.serial_number,
raise_on_progress=False,
)
self._abort_if_unique_id_configured(
updates={CONF_HOST: lametric.host, CONF_API_KEY: lametric.api_key}
)
@@ -21,8 +21,11 @@
"api_key": "You can find this API key in [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices)."
}
},
"user_cloud_select_device": {
"cloud_select_device": {
"data": {
"device": "Device"
},
"data_description": {
"device": "Select the LaMetric device to add"
}
}
@@ -35,5 +35,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.20.0", "led-ble==1.0.2"]
"requirements": ["bluetooth-data-tools==1.20.0", "led-ble==1.1.1"]
}
+4 -1
View File
@@ -23,6 +23,7 @@
"LIFX Ceiling",
"LIFX Clean",
"LIFX Color",
"LIFX Colour",
"LIFX DLCOL",
"LIFX Dlight",
"LIFX DLWW",
@@ -35,12 +36,14 @@
"LIFX Neon",
"LIFX Nightvision",
"LIFX PAR38",
"LIFX Permanent Outdoor",
"LIFX Pls",
"LIFX Plus",
"LIFX Round",
"LIFX Square",
"LIFX String",
"LIFX Tile",
"LIFX Tube",
"LIFX White",
"LIFX Z"
]
@@ -48,7 +51,7 @@
"iot_class": "local_polling",
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
"requirements": [
"aiolifx==1.1.1",
"aiolifx==1.1.2",
"aiolifx-effects==0.3.2",
"aiolifx-themes==0.5.5"
]
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["linkplay"],
"requirements": ["python-linkplay==0.0.20"],
"requirements": ["python-linkplay==0.1.1"],
"zeroconf": ["_linkplay._tcp.local."]
}
@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp[default]==2024.12.03"],
"requirements": ["yt-dlp[default]==2024.12.13"],
"single_config_entry": true
}
@@ -95,11 +95,17 @@ PARAMETER_ID_TO_EXCLUDE_F730 = (
)
PARAMETER_ID_TO_INCLUDE_SMO20 = (
"40013",
"40033",
"40940",
"44069",
"44071",
"44073",
"47011",
"47015",
"47028",
"47032",
"47398",
"50004",
)
+2 -11
View File
@@ -191,17 +191,8 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
self._previous_action_mode(self.coordinator)
# Adam provides the hvac_action for each thermostat
if self._gateway["smile_name"] == "Adam":
if (control_state := self.device.get("control_state")) == "cooling":
return HVACAction.COOLING
if control_state == "heating":
return HVACAction.HEATING
if control_state == "preheating":
return HVACAction.PREHEATING
if control_state == "off":
return HVACAction.IDLE
return HVACAction.IDLE
if (action := self.device.get("control_state")) is not None:
return HVACAction(action)
# Anna
heater: str = self._gateway["heater_id"]
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["plugwise"],
"requirements": ["plugwise==1.6.0"],
"requirements": ["plugwise==1.6.3"],
"zeroconf": ["_plugwise._tcp.local."]
}
@@ -1,5 +1,6 @@
"""Component to allow running Python scripts."""
from collections.abc import Mapping, Sequence
import datetime
import glob
import logging
@@ -7,6 +8,7 @@ from numbers import Number
import operator
import os
import time
import types
from typing import Any
from RestrictedPython import (
@@ -167,6 +169,20 @@ IOPERATOR_TO_OPERATOR = {
}
def guarded_import(
name: str,
globals: Mapping[str, object] | None = None,
locals: Mapping[str, object] | None = None,
fromlist: Sequence[str] = (),
level: int = 0,
) -> types.ModuleType:
"""Guard imports."""
# Allow import of _strptime needed by datetime.datetime.strptime
if name == "_strptime":
return __import__(name, globals, locals, fromlist, level)
raise ScriptError(f"Not allowed to import {name}")
def guarded_inplacevar(op: str, target: Any, operand: Any) -> Any:
"""Implement augmented-assign (+=, -=, etc.) operators for restricted code.
@@ -232,6 +248,7 @@ def execute(hass, filename, source, data=None, return_response=False):
return getattr(obj, name, default)
extra_builtins = {
"__import__": guarded_import,
"datetime": datetime,
"sorted": sorted,
"time": TimeWrapper(),
@@ -1431,7 +1431,6 @@ class Recorder(threading.Thread):
with session_scope(session=self.get_session()) as session:
end_incomplete_runs(session, self.recorder_runs_manager.recording_start)
self.recorder_runs_manager.start(session)
self.states_manager.load_from_db(session)
self._open_event_session()
@@ -22,9 +22,9 @@ from homeassistant.core import HomeAssistant, State, split_entity_id
from homeassistant.helpers.recorder import get_instance
import homeassistant.util.dt as dt_util
from ..db_schema import StateAttributes, States
from ..db_schema import RecorderRuns, StateAttributes, States
from ..filters import Filters
from ..models import process_timestamp_to_utc_isoformat
from ..models import process_timestamp, process_timestamp_to_utc_isoformat
from ..models.legacy import LegacyLazyState, legacy_row_to_compressed_state
from ..util import execute_stmt_lambda_element, session_scope
from .const import (
@@ -436,7 +436,7 @@ def get_last_state_changes(
def _get_states_for_entities_stmt(
run_start_ts: float,
run_start: datetime,
utc_point_in_time: datetime,
entity_ids: list[str],
no_attributes: bool,
@@ -447,6 +447,7 @@ def _get_states_for_entities_stmt(
)
# We got an include-list of entities, accelerate the query by filtering already
# in the inner query.
run_start_ts = process_timestamp(run_start).timestamp()
utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time)
stmt += lambda q: q.join(
(
@@ -482,7 +483,7 @@ def _get_rows_with_session(
session: Session,
utc_point_in_time: datetime,
entity_ids: list[str],
*,
run: RecorderRuns | None = None,
no_attributes: bool = False,
) -> Iterable[Row]:
"""Return the states at a specific point in time."""
@@ -494,16 +495,17 @@ def _get_rows_with_session(
),
)
oldest_ts = get_instance(hass).states_manager.oldest_ts
if run is None:
run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time)
if oldest_ts is None or oldest_ts > utc_point_in_time.timestamp():
# We don't have any states for the requested time
if run is None or process_timestamp(run.start) > utc_point_in_time:
# History did not run before utc_point_in_time
return []
# We have more than one entity to look at so we need to do a query on states
# since the last recorder run started.
stmt = _get_states_for_entities_stmt(
oldest_ts, utc_point_in_time, entity_ids, no_attributes
run.start, utc_point_in_time, entity_ids, no_attributes
)
return execute_stmt_lambda_element(session, stmt)
@@ -34,6 +34,7 @@ from ..models import (
LazyState,
datetime_to_timestamp_or_none,
extract_metadata_ids,
process_timestamp,
row_to_compressed_state,
)
from ..util import execute_stmt_lambda_element, session_scope
@@ -245,9 +246,9 @@ def get_significant_states_with_session(
if metadata_id is not None
and split_entity_id(entity_id)[0] in SIGNIFICANT_DOMAINS
]
oldest_ts: float | None = None
run_start_ts: float | None = None
if include_start_time_state and not (
oldest_ts := _get_oldest_possible_ts(hass, start_time)
run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time)
):
include_start_time_state = False
start_time_ts = dt_util.utc_to_timestamp(start_time)
@@ -263,7 +264,7 @@ def get_significant_states_with_session(
significant_changes_only,
no_attributes,
include_start_time_state,
oldest_ts,
run_start_ts,
),
track_on=[
bool(single_metadata_id),
@@ -410,9 +411,9 @@ def state_changes_during_period(
entity_id_to_metadata_id: dict[str, int | None] = {
entity_id: single_metadata_id
}
oldest_ts: float | None = None
run_start_ts: float | None = None
if include_start_time_state and not (
oldest_ts := _get_oldest_possible_ts(hass, start_time)
run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time)
):
include_start_time_state = False
start_time_ts = dt_util.utc_to_timestamp(start_time)
@@ -425,7 +426,7 @@ def state_changes_during_period(
no_attributes,
limit,
include_start_time_state,
oldest_ts,
run_start_ts,
has_last_reported,
),
track_on=[
@@ -599,17 +600,17 @@ def _get_start_time_state_for_entities_stmt(
)
def _get_oldest_possible_ts(
def _get_run_start_ts_for_utc_point_in_time(
hass: HomeAssistant, utc_point_in_time: datetime
) -> float | None:
"""Return the oldest possible timestamp.
Returns None if there are no states as old as utc_point_in_time.
"""
oldest_ts = get_instance(hass).states_manager.oldest_ts
if oldest_ts is not None and oldest_ts < utc_point_in_time.timestamp():
return oldest_ts
"""Return the start time of a run."""
run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time)
if (
run is not None
and (run_start := process_timestamp(run.start)) < utc_point_in_time
):
return run_start.timestamp()
# History did not run before utc_point_in_time but we still
return None
@@ -123,9 +123,6 @@ def purge_old_data(
_purge_old_entity_ids(instance, session)
_purge_old_recorder_runs(instance, session, purge_before)
with session_scope(session=instance.get_session(), read_only=True) as session:
instance.recorder_runs_manager.load_from_db(session)
instance.states_manager.load_from_db(session)
if repack:
repack_database(instance)
return True
@@ -637,15 +637,6 @@ def find_states_to_purge(
)
def find_oldest_state() -> StatementLambdaElement:
"""Find the last_updated_ts of the oldest state."""
return lambda_stmt(
lambda: select(States.last_updated_ts).where(
States.state_id.in_(select(func.min(States.state_id)))
)
)
def find_short_term_statistics_to_purge(
purge_before: datetime, max_bind_vars: int
) -> StatementLambdaElement:
@@ -2,15 +2,7 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, cast
from sqlalchemy.engine.row import Row
from sqlalchemy.orm.session import Session
from ..db_schema import States
from ..queries import find_oldest_state
from ..util import execute_stmt_lambda_element
class StatesManager:
@@ -21,12 +13,6 @@ class StatesManager:
self._pending: dict[str, States] = {}
self._last_committed_id: dict[str, int] = {}
self._last_reported: dict[int, float] = {}
self._oldest_ts: float | None = None
@property
def oldest_ts(self) -> float | None:
"""Return the oldest timestamp."""
return self._oldest_ts
def pop_pending(self, entity_id: str) -> States | None:
"""Pop a pending state.
@@ -58,8 +44,6 @@ class StatesManager:
recorder thread.
"""
self._pending[entity_id] = state
if self._oldest_ts is None:
self._oldest_ts = state.last_updated_ts
def update_pending_last_reported(
self, state_id: int, last_reported_timestamp: float
@@ -90,22 +74,6 @@ class StatesManager:
"""
self._last_committed_id.clear()
self._pending.clear()
self._oldest_ts = None
def load_from_db(self, session: Session) -> None:
"""Update the cache.
Must run in the recorder thread.
"""
result = cast(
Sequence[Row[Any]],
execute_stmt_lambda_element(session, find_oldest_state()),
)
if not result:
ts = None
else:
ts = result[0].last_updated_ts
self._oldest_ts = ts
def evict_purged_state_ids(self, purged_state_ids: set[int]) -> None:
"""Evict purged states from the committed states.
@@ -120,6 +120,8 @@ class PurgeTask(RecorderTask):
if purge.purge_old_data(
instance, self.purge_before, self.repack, self.apply_filter
):
with instance.get_session() as session:
instance.recorder_runs_manager.load_from_db(session)
# We always need to do the db cleanups after a purge
# is finished to ensure the WAL checkpoint and other
# tasks happen after a vacuum.
@@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.11.4"]
"requirements": ["reolink-aio==0.11.5"]
}
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/risco",
"iot_class": "local_push",
"loggers": ["pyrisco"],
"requirements": ["pyrisco==0.6.4"]
"requirements": ["pyrisco==0.6.5"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/starlink",
"iot_class": "local_polling",
"requirements": ["starlink-grpc-core==1.2.0"]
"requirements": ["starlink-grpc-core==1.2.2"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/suez_water",
"iot_class": "cloud_polling",
"loggers": ["pysuez", "regex"],
"requirements": ["pysuezV2==1.3.2"]
"requirements": ["pysuezV2==1.3.5"]
}
@@ -79,6 +79,8 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity):
_attr_hvac_mode = HVACMode.FAN_ONLY
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature = 21
_attr_target_temperature_step = 1
_attr_precision = 1
_attr_name = None
_enable_turn_on_off_backwards_compatibility = False
@@ -97,7 +99,7 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity):
)
await self.send_api_command(
AirConditionerCommands.SET_ALL,
parameters=f"{new_temperature},{new_mode},{new_fan_speed},on",
parameters=f"{int(new_temperature)},{new_mode},{new_fan_speed},on",
)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
+1 -1
View File
@@ -14,5 +14,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["PyTado"],
"requirements": ["python-tado==0.17.7"]
"requirements": ["python-tado==0.17.6"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
"iot_class": "cloud_polling",
"loggers": ["total_connect_client"],
"requirements": ["total-connect-client==2024.5"]
"requirements": ["total-connect-client==2024.12"]
}
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["twentemilieu"],
"requirements": ["twentemilieu==2.1.0"]
"requirements": ["twentemilieu==2.2.0"]
}
+1 -1
View File
@@ -7,7 +7,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["aiounifi"],
"requirements": ["aiounifi==80"],
"requirements": ["aiounifi==81"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
@@ -2,6 +2,7 @@
from dataclasses import dataclass
from datetime import datetime, timedelta
from json.decoder import JSONDecodeError
from typing import Any
from aiovodafone import VodafoneStationDevice, VodafoneStationSercommApi, exceptions
@@ -107,6 +108,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
exceptions.CannotConnect,
exceptions.AlreadyLogged,
exceptions.GenericLoginError,
JSONDecodeError,
) as err:
raise UpdateFailed(f"Error fetching data: {err!r}") from err
except (ConfigEntryAuthFailed, UpdateFailed):
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/withings",
"iot_class": "cloud_push",
"loggers": ["aiowithings"],
"requirements": ["aiowithings==3.1.3"]
"requirements": ["aiowithings==3.1.4"]
}
@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.62"]
"requirements": ["holidays==0.63"]
}
+1 -1
View File
@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/yale",
"iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.1"]
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.5"]
}
@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"iot_class": "local_push",
"requirements": ["yalexs-ble==2.5.1"]
"requirements": ["yalexs-ble==2.5.5"]
}
+1 -1
View File
@@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["universal-silabs-flasher==0.0.25", "zha==0.0.41"],
"requirements": ["universal-silabs-flasher==0.0.25", "zha==0.0.42"],
"usb": [
{
"vid": "10C4",
+1 -1
View File
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "4"
__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)
+12
View File
@@ -92,6 +92,10 @@ HOMEKIT = {
"always_discover": True,
"domain": "lifx",
},
"LIFX Colour": {
"always_discover": True,
"domain": "lifx",
},
"LIFX DLCOL": {
"always_discover": True,
"domain": "lifx",
@@ -140,6 +144,10 @@ HOMEKIT = {
"always_discover": True,
"domain": "lifx",
},
"LIFX Permanent Outdoor": {
"always_discover": True,
"domain": "lifx",
},
"LIFX Pls": {
"always_discover": True,
"domain": "lifx",
@@ -164,6 +172,10 @@ HOMEKIT = {
"always_discover": True,
"domain": "lifx",
},
"LIFX Tube": {
"always_discover": True,
"domain": "lifx",
},
"LIFX White": {
"always_discover": True,
"domain": "lifx",
+2 -2
View File
@@ -34,8 +34,8 @@ habluetooth==3.6.0
hass-nabucasa==0.86.0
hassil==2.0.5
home-assistant-bluetooth==1.13.0
home-assistant-frontend==20241127.6
home-assistant-intents==2024.12.4
home-assistant-frontend==20241127.8
home-assistant-intents==2024.12.9
httpx==0.27.2
ifaddr==0.2.0
Jinja2==3.1.4
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.12.1"
version = "2024.12.4"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
+30 -30
View File
@@ -173,7 +173,7 @@ aio-geojson-usgs-earthquakes==0.3
aio-georss-gdacs==0.10
# homeassistant.components.acaia
aioacaia==0.1.10
aioacaia==0.1.11
# homeassistant.components.airq
aioairq==0.4.3
@@ -286,7 +286,7 @@ aiolifx-effects==0.3.2
aiolifx-themes==0.5.5
# homeassistant.components.lifx
aiolifx==1.1.1
aiolifx==1.1.2
# homeassistant.components.livisi
aiolivisi==0.0.19
@@ -402,7 +402,7 @@ aiotedee==0.2.20
aiotractive==0.6.0
# homeassistant.components.unifi
aiounifi==80
aiounifi==81
# homeassistant.components.vlc_telnet
aiovlc==0.5.1
@@ -420,7 +420,7 @@ aiowatttime==0.1.1
aiowebostv==0.4.2
# homeassistant.components.withings
aiowithings==3.1.3
aiowithings==3.1.4
# homeassistant.components.yandex_transport
aioymaps==1.2.5
@@ -729,7 +729,7 @@ datapoint==0.9.9
dbus-fast==2.24.3
# homeassistant.components.debugpy
debugpy==1.8.6
debugpy==1.8.8
# homeassistant.components.decora_wifi
# decora-wifi==1.4
@@ -738,7 +738,7 @@ debugpy==1.8.6
# decora==0.6
# homeassistant.components.ecovacs
deebot-client==9.2.0
deebot-client==9.4.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -878,7 +878,7 @@ eufylife-ble-client==0.1.8
# evdev==1.6.1
# homeassistant.components.evohome
evohome-async==0.4.20
evohome-async==0.4.21
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -1127,13 +1127,13 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.62
holidays==0.63
# homeassistant.components.frontend
home-assistant-frontend==20241127.6
home-assistant-frontend==20241127.8
# homeassistant.components.conversation
home-assistant-intents==2024.12.4
home-assistant-intents==2024.12.9
# homeassistant.components.home_connect
homeconnect==0.8.0
@@ -1189,10 +1189,10 @@ iglo==1.2.7
ihcsdk==2.8.5
# homeassistant.components.imgw_pib
imgw_pib==1.0.6
imgw_pib==1.0.7
# homeassistant.components.incomfort
incomfort-client==0.6.3-1
incomfort-client==0.6.4
# homeassistant.components.influxdb
influxdb-client==1.24.0
@@ -1280,7 +1280,7 @@ ld2410-ble==0.1.1
leaone-ble==0.1.0
# homeassistant.components.led_ble
led-ble==1.0.2
led-ble==1.1.1
# homeassistant.components.lektrico
lektricowifi==0.0.43
@@ -1622,7 +1622,7 @@ plexauth==0.0.6
plexwebsocket==0.0.14
# homeassistant.components.plugwise
plugwise==1.6.0
plugwise==1.6.3
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -1672,7 +1672,7 @@ pushover_complete==1.1.1
pvo==2.1.1
# homeassistant.components.aosmith
py-aosmith==1.0.11
py-aosmith==1.0.12
# homeassistant.components.canary
py-canary==0.5.4
@@ -1832,10 +1832,10 @@ pycountry==24.6.1
pycsspeechtts==1.0.8
# homeassistant.components.cups
# pycups==1.9.73
# pycups==2.0.4
# homeassistant.components.daikin
pydaikin==2.13.7
pydaikin==2.13.8
# homeassistant.components.danfoss_air
pydanfossair==0.1.0
@@ -1859,7 +1859,7 @@ pydiscovergy==3.0.2
pydoods==1.0.2
# homeassistant.components.hydrawise
pydrawise==2024.9.0
pydrawise==2024.12.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@@ -1907,7 +1907,7 @@ pyeverlights==0.1.0
pyevilgenius==2.0.0
# homeassistant.components.ezviz
pyezviz==0.2.2.3
pyezviz==0.2.1.2
# homeassistant.components.fibaro
pyfibaro==0.8.0
@@ -2203,7 +2203,7 @@ pyrecswitch==1.0.2
pyrepetierng==0.1.0
# homeassistant.components.risco
pyrisco==0.6.4
pyrisco==0.6.5
# homeassistant.components.rituals_perfume_genie
pyrituals==0.0.6
@@ -2293,7 +2293,7 @@ pysqueezebox==0.10.0
pystiebeleltron==0.0.1.dev2
# homeassistant.components.suez_water
pysuezV2==1.3.2
pysuezV2==1.3.5
# homeassistant.components.switchbee
pyswitchbee==1.8.3
@@ -2365,7 +2365,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.8.1
# homeassistant.components.linkplay
python-linkplay==0.0.20
python-linkplay==0.1.1
# homeassistant.components.lirc
# python-lirc==1.2.3
@@ -2411,7 +2411,7 @@ python-smarttub==0.0.38
python-songpal==0.16.2
# homeassistant.components.tado
python-tado==0.17.7
python-tado==0.17.6
# homeassistant.components.technove
python-technove==1.3.1
@@ -2556,7 +2556,7 @@ renault-api==0.2.7
renson-endura-delta==1.7.1
# homeassistant.components.reolink
reolink-aio==0.11.4
reolink-aio==0.11.5
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@@ -2734,7 +2734,7 @@ starline==0.1.5
starlingbank==3.2
# homeassistant.components.starlink
starlink-grpc-core==1.2.0
starlink-grpc-core==1.2.2
# homeassistant.components.statsd
statsd==3.2.1
@@ -2858,7 +2858,7 @@ tololib==1.1.0
toonapi==0.3.0
# homeassistant.components.totalconnect
total-connect-client==2024.5
total-connect-client==2024.12
# homeassistant.components.tplink_lte
tp-connected==0.0.4
@@ -2882,7 +2882,7 @@ ttn_client==1.2.0
tuya-device-sharing-sdk==0.2.1
# homeassistant.components.twentemilieu
twentemilieu==2.1.0
twentemilieu==2.2.0
# homeassistant.components.twilio
twilio==6.32.0
@@ -3044,7 +3044,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==2.5.1
yalexs-ble==2.5.5
# homeassistant.components.august
# homeassistant.components.yale
@@ -3066,7 +3066,7 @@ youless-api==2.1.2
youtubeaio==1.1.5
# homeassistant.components.media_extractor
yt-dlp[default]==2024.12.03
yt-dlp[default]==2024.12.13
# homeassistant.components.zamg
zamg==0.3.6
@@ -3081,7 +3081,7 @@ zeroconf==0.136.2
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.41
zha==0.0.42
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13
+29 -29
View File
@@ -161,7 +161,7 @@ aio-geojson-usgs-earthquakes==0.3
aio-georss-gdacs==0.10
# homeassistant.components.acaia
aioacaia==0.1.10
aioacaia==0.1.11
# homeassistant.components.airq
aioairq==0.4.3
@@ -268,7 +268,7 @@ aiolifx-effects==0.3.2
aiolifx-themes==0.5.5
# homeassistant.components.lifx
aiolifx==1.1.1
aiolifx==1.1.2
# homeassistant.components.livisi
aiolivisi==0.0.19
@@ -384,7 +384,7 @@ aiotedee==0.2.20
aiotractive==0.6.0
# homeassistant.components.unifi
aiounifi==80
aiounifi==81
# homeassistant.components.vlc_telnet
aiovlc==0.5.1
@@ -402,7 +402,7 @@ aiowatttime==0.1.1
aiowebostv==0.4.2
# homeassistant.components.withings
aiowithings==3.1.3
aiowithings==3.1.4
# homeassistant.components.yandex_transport
aioymaps==1.2.5
@@ -625,10 +625,10 @@ datapoint==0.9.9
dbus-fast==2.24.3
# homeassistant.components.debugpy
debugpy==1.8.6
debugpy==1.8.8
# homeassistant.components.ecovacs
deebot-client==9.2.0
deebot-client==9.4.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -744,7 +744,7 @@ eternalegypt==0.0.16
eufylife-ble-client==0.1.8
# homeassistant.components.evohome
evohome-async==0.4.20
evohome-async==0.4.21
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -953,13 +953,13 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.62
holidays==0.63
# homeassistant.components.frontend
home-assistant-frontend==20241127.6
home-assistant-frontend==20241127.8
# homeassistant.components.conversation
home-assistant-intents==2024.12.4
home-assistant-intents==2024.12.9
# homeassistant.components.home_connect
homeconnect==0.8.0
@@ -1000,10 +1000,10 @@ idasen-ha==2.6.2
ifaddr==0.2.0
# homeassistant.components.imgw_pib
imgw_pib==1.0.6
imgw_pib==1.0.7
# homeassistant.components.incomfort
incomfort-client==0.6.3-1
incomfort-client==0.6.4
# homeassistant.components.influxdb
influxdb-client==1.24.0
@@ -1076,7 +1076,7 @@ ld2410-ble==0.1.1
leaone-ble==0.1.0
# homeassistant.components.led_ble
led-ble==1.0.2
led-ble==1.1.1
# homeassistant.components.lektrico
lektricowifi==0.0.43
@@ -1329,7 +1329,7 @@ plexauth==0.0.6
plexwebsocket==0.0.14
# homeassistant.components.plugwise
plugwise==1.6.0
plugwise==1.6.3
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -1367,7 +1367,7 @@ pushover_complete==1.1.1
pvo==2.1.1
# homeassistant.components.aosmith
py-aosmith==1.0.11
py-aosmith==1.0.12
# homeassistant.components.canary
py-canary==0.5.4
@@ -1485,7 +1485,7 @@ pycountry==24.6.1
pycsspeechtts==1.0.8
# homeassistant.components.daikin
pydaikin==2.13.7
pydaikin==2.13.8
# homeassistant.components.deako
pydeako==0.6.0
@@ -1500,7 +1500,7 @@ pydexcom==0.2.3
pydiscovergy==3.0.2
# homeassistant.components.hydrawise
pydrawise==2024.9.0
pydrawise==2024.12.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@@ -1536,7 +1536,7 @@ pyeverlights==0.1.0
pyevilgenius==2.0.0
# homeassistant.components.ezviz
pyezviz==0.2.2.3
pyezviz==0.2.1.2
# homeassistant.components.fibaro
pyfibaro==0.8.0
@@ -1775,7 +1775,7 @@ pyqwikswitch==0.93
pyrainbird==6.0.1
# homeassistant.components.risco
pyrisco==0.6.4
pyrisco==0.6.5
# homeassistant.components.rituals_perfume_genie
pyrituals==0.0.6
@@ -1850,7 +1850,7 @@ pyspeex-noise==1.0.2
pysqueezebox==0.10.0
# homeassistant.components.suez_water
pysuezV2==1.3.2
pysuezV2==1.3.5
# homeassistant.components.switchbee
pyswitchbee==1.8.3
@@ -1892,7 +1892,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.8.1
# homeassistant.components.linkplay
python-linkplay==0.0.20
python-linkplay==0.1.1
# homeassistant.components.matter
python-matter-server==6.6.0
@@ -1932,7 +1932,7 @@ python-smarttub==0.0.38
python-songpal==0.16.2
# homeassistant.components.tado
python-tado==0.17.7
python-tado==0.17.6
# homeassistant.components.technove
python-technove==1.3.1
@@ -2047,7 +2047,7 @@ renault-api==0.2.7
renson-endura-delta==1.7.1
# homeassistant.components.reolink
reolink-aio==0.11.4
reolink-aio==0.11.5
# homeassistant.components.rflink
rflink==0.0.66
@@ -2183,7 +2183,7 @@ srpenergy==1.3.6
starline==0.1.5
# homeassistant.components.starlink
starlink-grpc-core==1.2.0
starlink-grpc-core==1.2.2
# homeassistant.components.statsd
statsd==3.2.1
@@ -2274,7 +2274,7 @@ tololib==1.1.0
toonapi==0.3.0
# homeassistant.components.totalconnect
total-connect-client==2024.5
total-connect-client==2024.12
# homeassistant.components.tplink_omada
tplink-omada-client==1.4.3
@@ -2295,7 +2295,7 @@ ttn_client==1.2.0
tuya-device-sharing-sdk==0.2.1
# homeassistant.components.twentemilieu
twentemilieu==2.1.0
twentemilieu==2.2.0
# homeassistant.components.twilio
twilio==6.32.0
@@ -2433,7 +2433,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==2.5.1
yalexs-ble==2.5.5
# homeassistant.components.august
# homeassistant.components.yale
@@ -2452,7 +2452,7 @@ youless-api==2.1.2
youtubeaio==1.1.5
# homeassistant.components.media_extractor
yt-dlp[default]==2024.12.03
yt-dlp[default]==2024.12.13
# homeassistant.components.zamg
zamg==0.3.6
@@ -2464,7 +2464,7 @@ zeroconf==0.136.2
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.41
zha==0.0.42
# homeassistant.components.zwave_js
zwave-js-server-python==0.59.1
+1 -1
View File
@@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.4,source=/uv,target=/bin/uv \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.8.0 \
PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.5 home-assistant-intents==2024.12.4 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.5 home-assistant-intents==2024.12.9 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
LABEL "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"
@@ -142,7 +142,7 @@
'data': dict({
'code': 'no_intent_match',
}),
'language': 'en',
'language': 'en-US',
'response_type': 'error',
'speech': dict({
'plain': dict({
@@ -233,7 +233,7 @@
'data': dict({
'code': 'no_intent_match',
}),
'language': 'en',
'language': 'en-US',
'response_type': 'error',
'speech': dict({
'plain': dict({
@@ -387,6 +387,57 @@
}),
])
# ---
# name: test_pipeline_language_used_instead_of_conversation_language
list([
dict({
'data': dict({
'language': 'en',
'pipeline': <ANY>,
}),
'type': <PipelineEventType.RUN_START: 'run-start'>,
}),
dict({
'data': dict({
'conversation_id': None,
'device_id': None,
'engine': 'conversation.home_assistant',
'intent_input': 'test input',
'language': 'en',
'prefer_local_intents': False,
}),
'type': <PipelineEventType.INTENT_START: 'intent-start'>,
}),
dict({
'data': dict({
'intent_output': dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
}),
}),
}),
'processed_locally': True,
}),
'type': <PipelineEventType.INTENT_END: 'intent-end'>,
}),
dict({
'data': None,
'type': <PipelineEventType.RUN_END: 'run-end'>,
}),
])
# ---
# name: test_wake_word_detection_aborted
list([
dict({
@@ -23,6 +23,7 @@ from homeassistant.components.assist_pipeline.const import (
CONF_DEBUG_RECORDING_DIR,
DOMAIN,
)
from homeassistant.const import MATCH_ALL
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import intent
from homeassistant.setup import async_setup_component
@@ -1098,3 +1099,77 @@ async def test_prefer_local_intents(
]
== "Order confirmed"
)
async def test_pipeline_language_used_instead_of_conversation_language(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
init_components,
snapshot: SnapshotAssertion,
) -> None:
"""Test that the pipeline language is used when the conversation language is '*' (all languages)."""
client = await hass_ws_client(hass)
events: list[assist_pipeline.PipelineEvent] = []
await client.send_json_auto_id(
{
"type": "assist_pipeline/pipeline/create",
"conversation_engine": "homeassistant",
"conversation_language": MATCH_ALL,
"language": "en",
"name": "test_name",
"stt_engine": "test",
"stt_language": "en-US",
"tts_engine": "test",
"tts_language": "en-US",
"tts_voice": "Arnold Schwarzenegger",
"wake_word_entity": None,
"wake_word_id": None,
}
)
msg = await client.receive_json()
assert msg["success"]
pipeline_id = msg["result"]["id"]
pipeline = assist_pipeline.async_get_pipeline(hass, pipeline_id)
pipeline_input = assist_pipeline.pipeline.PipelineInput(
intent_input="test input",
run=assist_pipeline.pipeline.PipelineRun(
hass,
context=Context(),
pipeline=pipeline,
start_stage=assist_pipeline.PipelineStage.INTENT,
end_stage=assist_pipeline.PipelineStage.INTENT,
event_callback=events.append,
),
)
await pipeline_input.validate()
with patch(
"homeassistant.components.assist_pipeline.pipeline.conversation.async_converse",
return_value=conversation.ConversationResult(
intent.IntentResponse(pipeline.language)
),
) as mock_async_converse:
await pipeline_input.execute()
# Check intent start event
assert process_events(events) == snapshot
intent_start: assist_pipeline.PipelineEvent | None = None
for event in events:
if event.type == assist_pipeline.PipelineEventType.INTENT_START:
intent_start = event
break
assert intent_start is not None
# Pipeline language (en) should be used instead of '*'
assert intent_start.data.get("language") == pipeline.language
# Check input to async_converse
mock_async_converse.assert_called_once()
assert (
mock_async_converse.call_args_list[0].kwargs.get("language")
== pipeline.language
)
@@ -571,7 +571,7 @@
'name': 'HassGetState',
}),
'match': True,
'sentence_template': '[tell me] how many {on_off_domains:domain} (is|are) {on_off_states:state} [in <area>]',
'sentence_template': '[tell me] how many {on_off_domains:domain} (is|are) {on_off_states:state} [<in_area_floor>]',
'slots': dict({
'area': 'kitchen',
'domain': 'lights',
@@ -30,6 +30,7 @@ from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME,
STATE_CLOSED,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
EntityCategory,
@@ -3049,3 +3050,49 @@ async def test_entities_names_are_not_templates(hass: HomeAssistant) -> None:
assert result is not None
assert result.response.response_type == intent.IntentResponseType.ERROR
@pytest.mark.parametrize(
("language", "light_name", "on_sentence", "off_sentence"),
[
("en", "test light", "turn on test light", "turn off test light"),
("zh-cn", "卧室灯", "打开卧室灯", "关闭卧室灯"),
("zh-hk", "睡房燈", "打開睡房燈", "關閉睡房燈"),
("zh-tw", "臥室檯燈", "打開臥室檯燈", "關臥室檯燈"),
],
)
@pytest.mark.usefixtures("init_components")
async def test_turn_on_off(
hass: HomeAssistant,
language: str,
light_name: str,
on_sentence: str,
off_sentence: str,
) -> None:
"""Test turn on/off in multiple languages."""
entity_id = "light.light1234"
hass.states.async_set(
entity_id, STATE_OFF, attributes={ATTR_FRIENDLY_NAME: light_name}
)
on_calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
await conversation.async_converse(
hass,
on_sentence,
None,
Context(),
language=language,
)
assert len(on_calls) == 1
assert on_calls[0].data.get("entity_id") == [entity_id]
off_calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_off")
await conversation.async_converse(
hass,
off_sentence,
None,
Context(),
language=language,
)
assert len(off_calls) == 1
assert off_calls[0].data.get("entity_id") == [entity_id]
+56
View File
@@ -129,6 +129,62 @@ def mock_light() -> Mock:
return light
@pytest.fixture
def mock_thermostat() -> Mock:
"""Fixture for a thermostat."""
climate = Mock()
climate.fibaro_id = 4
climate.parent_fibaro_id = 0
climate.name = "Test climate"
climate.room_id = 1
climate.dead = False
climate.visible = True
climate.enabled = True
climate.type = "com.fibaro.thermostatDanfoss"
climate.base_type = "com.fibaro.device"
climate.properties = {"manufacturer": ""}
climate.actions = {"setThermostatMode": 1}
climate.supported_features = {}
climate.has_supported_thermostat_modes = True
climate.supported_thermostat_modes = ["Off", "Heat", "CustomerSpecific"]
climate.has_operating_mode = False
climate.has_thermostat_mode = True
climate.thermostat_mode = "CustomerSpecific"
value_mock = Mock()
value_mock.has_value = True
value_mock.int_value.return_value = 20
climate.value = value_mock
return climate
@pytest.fixture
def mock_thermostat_with_operating_mode() -> Mock:
"""Fixture for a thermostat."""
climate = Mock()
climate.fibaro_id = 4
climate.parent_fibaro_id = 0
climate.name = "Test climate"
climate.room_id = 1
climate.dead = False
climate.visible = True
climate.enabled = True
climate.type = "com.fibaro.thermostatDanfoss"
climate.base_type = "com.fibaro.device"
climate.properties = {"manufacturer": ""}
climate.actions = {"setOperationMode": 1}
climate.supported_features = {}
climate.has_supported_operating_modes = True
climate.supported_operating_modes = [0, 1, 15]
climate.has_operating_mode = True
climate.operating_mode = 15
climate.has_thermostat_mode = False
value_mock = Mock()
value_mock.has_value = True
value_mock.int_value.return_value = 20
climate.value = value_mock
return climate
@pytest.fixture
def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Return the default mocked config entry."""
+134
View File
@@ -0,0 +1,134 @@
"""Test the Fibaro climate platform."""
from unittest.mock import Mock, patch
from homeassistant.components.climate import ClimateEntityFeature, HVACMode
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import init_integration
from tests.common import MockConfigEntry
async def test_climate_setup(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_fibaro_client: Mock,
mock_config_entry: MockConfigEntry,
mock_thermostat: Mock,
mock_room: Mock,
) -> None:
"""Test that the climate creates an entity."""
# Arrange
mock_fibaro_client.read_rooms.return_value = [mock_room]
mock_fibaro_client.read_devices.return_value = [mock_thermostat]
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]):
# Act
await init_integration(hass, mock_config_entry)
# Assert
entry = entity_registry.async_get("climate.room_1_test_climate_4")
assert entry
assert entry.unique_id == "hc2_111111.4"
assert entry.original_name == "Room 1 Test climate"
assert entry.supported_features == (
ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.PRESET_MODE
)
async def test_hvac_mode_preset(
hass: HomeAssistant,
mock_fibaro_client: Mock,
mock_config_entry: MockConfigEntry,
mock_thermostat: Mock,
mock_room: Mock,
) -> None:
"""Test that the climate state is auto when a preset is selected."""
# Arrange
mock_fibaro_client.read_rooms.return_value = [mock_room]
mock_fibaro_client.read_devices.return_value = [mock_thermostat]
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]):
# Act
await init_integration(hass, mock_config_entry)
# Assert
state = hass.states.get("climate.room_1_test_climate_4")
assert state.state == HVACMode.AUTO
assert state.attributes["preset_mode"] == "CustomerSpecific"
async def test_hvac_mode_heat(
hass: HomeAssistant,
mock_fibaro_client: Mock,
mock_config_entry: MockConfigEntry,
mock_thermostat: Mock,
mock_room: Mock,
) -> None:
"""Test that the preset mode is None if a hvac mode is active."""
# Arrange
mock_thermostat.thermostat_mode = "Heat"
mock_fibaro_client.read_rooms.return_value = [mock_room]
mock_fibaro_client.read_devices.return_value = [mock_thermostat]
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]):
# Act
await init_integration(hass, mock_config_entry)
# Assert
state = hass.states.get("climate.room_1_test_climate_4")
assert state.state == HVACMode.HEAT
assert state.attributes["preset_mode"] is None
async def test_set_hvac_mode(
hass: HomeAssistant,
mock_fibaro_client: Mock,
mock_config_entry: MockConfigEntry,
mock_thermostat: Mock,
mock_room: Mock,
) -> None:
"""Test that set_hvac_mode() works."""
# Arrange
mock_fibaro_client.read_rooms.return_value = [mock_room]
mock_fibaro_client.read_devices.return_value = [mock_thermostat]
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]):
# Act
await init_integration(hass, mock_config_entry)
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": "climate.room_1_test_climate_4", "hvac_mode": HVACMode.HEAT},
blocking=True,
)
# Assert
mock_thermostat.execute_action.assert_called_once()
async def test_hvac_mode_with_operation_mode_support(
hass: HomeAssistant,
mock_fibaro_client: Mock,
mock_config_entry: MockConfigEntry,
mock_thermostat_with_operating_mode: Mock,
mock_room: Mock,
) -> None:
"""Test that operating mode works."""
# Arrange
mock_fibaro_client.read_rooms.return_value = [mock_room]
mock_fibaro_client.read_devices.return_value = [mock_thermostat_with_operating_mode]
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]):
# Act
await init_integration(hass, mock_config_entry)
# Assert
state = hass.states.get("climate.room_1_test_climate_4")
assert state.state == HVACMode.AUTO
+10 -10
View File
@@ -68,16 +68,16 @@ def mock_flexit_bacnet() -> Generator[AsyncMock]:
flexit_bacnet.electric_heater = True
# Mock fan setpoints
flexit_bacnet.fan_setpoint_extract_air_fire = 10
flexit_bacnet.fan_setpoint_supply_air_fire = 20
flexit_bacnet.fan_setpoint_extract_air_away = 30
flexit_bacnet.fan_setpoint_supply_air_away = 40
flexit_bacnet.fan_setpoint_extract_air_home = 50
flexit_bacnet.fan_setpoint_supply_air_home = 60
flexit_bacnet.fan_setpoint_extract_air_high = 70
flexit_bacnet.fan_setpoint_supply_air_high = 80
flexit_bacnet.fan_setpoint_extract_air_cooker = 90
flexit_bacnet.fan_setpoint_supply_air_cooker = 100
flexit_bacnet.fan_setpoint_extract_air_fire = 56
flexit_bacnet.fan_setpoint_supply_air_fire = 77
flexit_bacnet.fan_setpoint_extract_air_away = 40
flexit_bacnet.fan_setpoint_supply_air_away = 42
flexit_bacnet.fan_setpoint_extract_air_home = 70
flexit_bacnet.fan_setpoint_supply_air_home = 74
flexit_bacnet.fan_setpoint_extract_air_high = 100
flexit_bacnet.fan_setpoint_supply_air_high = 100
flexit_bacnet.fan_setpoint_extract_air_cooker = 50
flexit_bacnet.fan_setpoint_supply_air_cooker = 70
yield flexit_bacnet
@@ -5,8 +5,8 @@
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'max': 70,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -42,8 +42,8 @@
'attributes': ReadOnlyDict({
'device_class': 'power_factor',
'friendly_name': 'Device Name Away extract fan setpoint',
'max': 100,
'min': 0,
'max': 70,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -53,7 +53,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30',
'state': '40',
})
# ---
# name: test_numbers[number.device_name_away_supply_fan_setpoint-entry]
@@ -62,8 +62,8 @@
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'max': 74,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -99,8 +99,8 @@
'attributes': ReadOnlyDict({
'device_class': 'power_factor',
'friendly_name': 'Device Name Away supply fan setpoint',
'max': 100,
'min': 0,
'max': 74,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -110,7 +110,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '40',
'state': '42',
})
# ---
# name: test_numbers[number.device_name_cooker_hood_extract_fan_setpoint-entry]
@@ -120,7 +120,7 @@
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -157,7 +157,7 @@
'device_class': 'power_factor',
'friendly_name': 'Device Name Cooker hood extract fan setpoint',
'max': 100,
'min': 0,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -167,7 +167,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '90',
'state': '50',
})
# ---
# name: test_numbers[number.device_name_cooker_hood_supply_fan_setpoint-entry]
@@ -177,7 +177,7 @@
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -214,7 +214,7 @@
'device_class': 'power_factor',
'friendly_name': 'Device Name Cooker hood supply fan setpoint',
'max': 100,
'min': 0,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -224,7 +224,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
'state': '70',
})
# ---
# name: test_numbers[number.device_name_fireplace_extract_fan_setpoint-entry]
@@ -234,7 +234,7 @@
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -271,7 +271,7 @@
'device_class': 'power_factor',
'friendly_name': 'Device Name Fireplace extract fan setpoint',
'max': 100,
'min': 0,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -281,7 +281,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '10',
'state': '56',
})
# ---
# name: test_numbers[number.device_name_fireplace_supply_fan_setpoint-entry]
@@ -291,7 +291,7 @@
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -328,7 +328,7 @@
'device_class': 'power_factor',
'friendly_name': 'Device Name Fireplace supply fan setpoint',
'max': 100,
'min': 0,
'min': 30,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -338,7 +338,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '20',
'state': '77',
})
# ---
# name: test_numbers[number.device_name_high_extract_fan_setpoint-entry]
@@ -348,7 +348,7 @@
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'min': 70,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -385,7 +385,7 @@
'device_class': 'power_factor',
'friendly_name': 'Device Name High extract fan setpoint',
'max': 100,
'min': 0,
'min': 70,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -395,7 +395,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '70',
'state': '100',
})
# ---
# name: test_numbers[number.device_name_high_supply_fan_setpoint-entry]
@@ -405,7 +405,7 @@
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'min': 74,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -442,7 +442,7 @@
'device_class': 'power_factor',
'friendly_name': 'Device Name High supply fan setpoint',
'max': 100,
'min': 0,
'min': 74,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -452,7 +452,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '80',
'state': '100',
})
# ---
# name: test_numbers[number.device_name_home_extract_fan_setpoint-entry]
@@ -462,7 +462,7 @@
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'min': 40,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -499,7 +499,7 @@
'device_class': 'power_factor',
'friendly_name': 'Device Name Home extract fan setpoint',
'max': 100,
'min': 0,
'min': 40,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -509,7 +509,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '50',
'state': '70',
})
# ---
# name: test_numbers[number.device_name_home_supply_fan_setpoint-entry]
@@ -519,7 +519,7 @@
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'min': 42,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
@@ -556,7 +556,7 @@
'device_class': 'power_factor',
'friendly_name': 'Device Name Home supply fan setpoint',
'max': 100,
'min': 0,
'min': 42,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
@@ -566,6 +566,6 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '60',
'state': '74',
})
# ---
@@ -64,21 +64,21 @@ async def test_numbers_implementation(
assert len(mocked_method.mock_calls) == 1
assert hass.states.get(ENTITY_ID).state == "60"
mock_flexit_bacnet.fan_setpoint_supply_air_fire = 10
mock_flexit_bacnet.fan_setpoint_supply_air_fire = 40
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_VALUE: 10,
ATTR_VALUE: 40,
},
blocking=True,
)
mocked_method = getattr(mock_flexit_bacnet, "set_fan_setpoint_supply_air_fire")
assert len(mocked_method.mock_calls) == 2
assert hass.states.get(ENTITY_ID).state == "10"
assert hass.states.get(ENTITY_ID).state == "40"
# Error recovery, when setting the value
mock_flexit_bacnet.set_fan_setpoint_supply_air_fire.side_effect = DecodingError
@@ -89,7 +89,7 @@ async def test_numbers_implementation(
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_VALUE: 10,
ATTR_VALUE: 40,
},
blocking=True,
)
@@ -1474,7 +1474,7 @@
'state': dict({
'attributes': dict({
'changed_by': None,
'code_arm_required': True,
'code_arm_required': False,
'code_format': None,
'friendly_name': 'Aqara-Hub-E1-00A0 Security System',
'supported_features': <AlarmControlPanelEntityFeature: 7>,
@@ -1848,7 +1848,7 @@
'state': dict({
'attributes': dict({
'changed_by': None,
'code_arm_required': True,
'code_arm_required': False,
'code_format': None,
'friendly_name': 'Aqara Hub-1563 Security System',
'supported_features': <AlarmControlPanelEntityFeature: 7>,
@@ -6,6 +6,7 @@ from aiohomekit.model import Accessory
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from homeassistant.components.alarm_control_panel import ATTR_CODE_ARM_REQUIRED
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -106,6 +107,7 @@ async def test_switch_read_alarm_state(
state = await helper.poll_and_get_state()
assert state.state == "armed_home"
assert state.attributes["battery_level"] == 50
assert state.attributes[ATTR_CODE_ARM_REQUIRED] is False
await helper.async_update(
ServicesTypes.SECURITY_SYSTEM,
-1
View File
@@ -56,7 +56,6 @@ def mock_legacy_pydrawise(
@pytest.fixture
def mock_pydrawise(
mock_auth: AsyncMock,
user: User,
controller: Controller,
zones: list[Zone],
+31 -8
View File
@@ -21,6 +21,7 @@ pytestmark = pytest.mark.usefixtures("mock_setup_entry")
async def test_form(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_auth: AsyncMock,
mock_pydrawise: AsyncMock,
user: User,
) -> None:
@@ -46,11 +47,12 @@ async def test_form(
CONF_PASSWORD: "__password__",
}
assert len(mock_setup_entry.mock_calls) == 1
mock_pydrawise.get_user.assert_called_once_with(fetch_zones=False)
mock_auth.token.assert_awaited_once_with()
mock_pydrawise.get_user.assert_awaited_once_with(fetch_zones=False)
async def test_form_api_error(
hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User
hass: HomeAssistant, mock_auth: AsyncMock, mock_pydrawise: AsyncMock, user: User
) -> None:
"""Test we handle API errors."""
mock_pydrawise.get_user.side_effect = ClientError("XXX")
@@ -71,8 +73,29 @@ async def test_form_api_error(
assert result2["type"] is FlowResultType.CREATE_ENTRY
async def test_form_connect_timeout(
hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User
async def test_form_auth_connect_timeout(
hass: HomeAssistant, mock_auth: AsyncMock, mock_pydrawise: AsyncMock
) -> None:
"""Test we handle API errors."""
mock_auth.token.side_effect = TimeoutError
init_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}
result = await hass.config_entries.flow.async_configure(
init_result["flow_id"], data
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "timeout_connect"}
mock_auth.token.reset_mock(side_effect=True)
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data)
assert result2["type"] is FlowResultType.CREATE_ENTRY
async def test_form_client_connect_timeout(
hass: HomeAssistant, mock_auth: AsyncMock, mock_pydrawise: AsyncMock, user: User
) -> None:
"""Test we handle API errors."""
mock_pydrawise.get_user.side_effect = TimeoutError
@@ -94,10 +117,10 @@ async def test_form_connect_timeout(
async def test_form_not_authorized_error(
hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User
hass: HomeAssistant, mock_auth: AsyncMock, mock_pydrawise: AsyncMock
) -> None:
"""Test we handle API errors."""
mock_pydrawise.get_user.side_effect = NotAuthorizedError
mock_auth.token.side_effect = NotAuthorizedError
init_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -109,8 +132,7 @@ async def test_form_not_authorized_error(
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "invalid_auth"}
mock_pydrawise.get_user.reset_mock(side_effect=True)
mock_pydrawise.get_user.return_value = user
mock_auth.token.reset_mock(side_effect=True)
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data)
assert result2["type"] is FlowResultType.CREATE_ENTRY
@@ -118,6 +140,7 @@ async def test_form_not_authorized_error(
async def test_reauth(
hass: HomeAssistant,
user: User,
mock_auth: AsyncMock,
mock_pydrawise: AsyncMock,
) -> None:
"""Test that re-authorization works."""
@@ -31,8 +31,10 @@ async def test_diagnostics(
patch.object(LinkPlayMultiroom, "update_status", return_value=None),
):
endpoints = [
LinkPlayApiEndpoint(protocol="https", endpoint=HOST, session=None),
LinkPlayApiEndpoint(protocol="http", endpoint=HOST, session=None),
LinkPlayApiEndpoint(
protocol="https", port=443, endpoint=HOST, session=None
),
LinkPlayApiEndpoint(protocol="http", port=80, endpoint=HOST, session=None),
]
for endpoint in endpoints:
mock_session.get(
@@ -176,7 +176,7 @@
"off"
],
"climate_mode": "auto",
"control_state": "off",
"control_state": "idle",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Bathroom",
@@ -3,7 +3,7 @@
"06aecb3d00354375924f50c47af36bd2": {
"active_preset": "no_frost",
"climate_mode": "off",
"control_state": "off",
"control_state": "idle",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Slaapkamer",
@@ -26,7 +26,7 @@
"13228dab8ce04617af318a2888b3c548": {
"active_preset": "home",
"climate_mode": "heat",
"control_state": "off",
"control_state": "idle",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Woonkamer",
@@ -238,7 +238,7 @@
"d27aede973b54be484f6842d1b2802ad": {
"active_preset": "home",
"climate_mode": "heat",
"control_state": "off",
"control_state": "idle",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Kinderkamer",
@@ -285,7 +285,7 @@
"d58fec52899f4f1c92e4f8fad6d8c48c": {
"active_preset": "home",
"climate_mode": "heat",
"control_state": "off",
"control_state": "idle",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Logeerkamer",
@@ -32,6 +32,7 @@
"off"
],
"climate_mode": "auto",
"control_state": "idle",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Badkamer",
@@ -66,6 +67,7 @@
"off"
],
"climate_mode": "heat",
"control_state": "idle",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Bios",
@@ -112,6 +114,7 @@
"446ac08dd04d4eff8ac57489757b7314": {
"active_preset": "no_frost",
"climate_mode": "heat",
"control_state": "idle",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Garage",
@@ -258,6 +261,7 @@
"off"
],
"climate_mode": "auto",
"control_state": "idle",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Jessie",
@@ -402,6 +406,7 @@
"off"
],
"climate_mode": "auto",
"control_state": "heating",
"dev_class": "climate",
"model": "ThermoZone",
"name": "Woonkamer",
@@ -577,7 +582,7 @@
"cooling_present": false,
"gateway_id": "fe799307f1624099878210aa0b9f1475",
"heater_id": "90986d591dcd426cae3ec3e8111ff730",
"item_count": 364,
"item_count": 369,
"notifications": {
"af82e4ccf9c548528166d38e560662a4": {
"warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device."
@@ -34,6 +34,7 @@
'off',
]),
'climate_mode': 'auto',
'control_state': 'idle',
'dev_class': 'climate',
'model': 'ThermoZone',
'name': 'Badkamer',
@@ -75,6 +76,7 @@
'off',
]),
'climate_mode': 'heat',
'control_state': 'idle',
'dev_class': 'climate',
'model': 'ThermoZone',
'name': 'Bios',
@@ -131,6 +133,7 @@
'446ac08dd04d4eff8ac57489757b7314': dict({
'active_preset': 'no_frost',
'climate_mode': 'heat',
'control_state': 'idle',
'dev_class': 'climate',
'model': 'ThermoZone',
'name': 'Garage',
@@ -286,6 +289,7 @@
'off',
]),
'climate_mode': 'auto',
'control_state': 'idle',
'dev_class': 'climate',
'model': 'ThermoZone',
'name': 'Jessie',
@@ -440,6 +444,7 @@
'off',
]),
'climate_mode': 'auto',
'control_state': 'heating',
'dev_class': 'climate',
'model': 'ThermoZone',
'name': 'Woonkamer',
@@ -625,7 +630,7 @@
'cooling_present': False,
'gateway_id': 'fe799307f1624099878210aa0b9f1475',
'heater_id': '90986d591dcd426cae3ec3e8111ff730',
'item_count': 364,
'item_count': 369,
'notifications': dict({
'af82e4ccf9c548528166d38e560662a4': dict({
'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.",
+4 -8
View File
@@ -31,15 +31,13 @@ async def test_adam_climate_entity_attributes(
state = hass.states.get("climate.woonkamer")
assert state
assert state.state == HVACMode.AUTO
assert state.attributes["hvac_action"] == "heating"
assert state.attributes["hvac_modes"] == [HVACMode.AUTO, HVACMode.HEAT]
# hvac_action is not asserted as the fixture is not in line with recent firmware functionality
assert "preset_modes" in state.attributes
assert "no_frost" in state.attributes["preset_modes"]
assert "home" in state.attributes["preset_modes"]
assert state.attributes["current_temperature"] == 20.9
assert state.attributes["preset_mode"] == "home"
assert state.attributes["current_temperature"] == 20.9
assert state.attributes["supported_features"] == 17
assert state.attributes["temperature"] == 21.5
assert state.attributes["min_temp"] == 0.0
@@ -49,15 +47,13 @@ async def test_adam_climate_entity_attributes(
state = hass.states.get("climate.jessie")
assert state
assert state.state == HVACMode.AUTO
assert state.attributes["hvac_action"] == "idle"
assert state.attributes["hvac_modes"] == [HVACMode.AUTO, HVACMode.HEAT]
# hvac_action is not asserted as the fixture is not in line with recent firmware functionality
assert "preset_modes" in state.attributes
assert "no_frost" in state.attributes["preset_modes"]
assert "home" in state.attributes["preset_modes"]
assert state.attributes["current_temperature"] == 17.2
assert state.attributes["preset_mode"] == "asleep"
assert state.attributes["current_temperature"] == 17.2
assert state.attributes["temperature"] == 15.0
assert state.attributes["min_temp"] == 0.0
assert state.attributes["max_temp"] == 35.0
@@ -688,3 +688,27 @@ async def test_prohibited_augmented_assignment_operations(
hass.async_add_executor_job(execute, hass, "aug_assign_prohibited.py", case, {})
await hass.async_block_till_done(wait_background_tasks=True)
assert error in caplog.text
async def test_import_allow_strptime(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test calling datetime.datetime.strptime works."""
source = """
test_date = datetime.datetime.strptime('2024-04-01', '%Y-%m-%d')
logger.info(f'Date {test_date}')
"""
hass.async_add_executor_job(execute, hass, "test.py", source, {})
await hass.async_block_till_done(wait_background_tasks=True)
assert "Error executing script: Not allowed to import _strptime" not in caplog.text
assert "Date 2024-04-01 00:00:00" in caplog.text
async def test_no_other_imports_allowed(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test imports are not allowed."""
source = "import sys"
hass.async_add_executor_job(execute, hass, "test.py", source, {})
await hass.async_block_till_done(wait_background_tasks=True)
assert "Error executing script: Not allowed to import sys" in caplog.text
-17
View File
@@ -112,9 +112,6 @@ async def test_purge_big_database(hass: HomeAssistant, recorder_mock: Recorder)
async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> None:
"""Test deleting old states."""
assert recorder_mock.states_manager.oldest_ts is None
oldest_ts = recorder_mock.states_manager.oldest_ts
await _add_test_states(hass)
# make sure we start with 6 states
@@ -130,10 +127,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) ->
events = session.query(Events).filter(Events.event_type == "state_changed")
assert events.count() == 0
assert recorder_mock.states_manager.oldest_ts != oldest_ts
assert recorder_mock.states_manager.oldest_ts == states[0].last_updated_ts
oldest_ts = recorder_mock.states_manager.oldest_ts
assert "test.recorder2" in recorder_mock.states_manager._last_committed_id
purge_before = dt_util.utcnow() - timedelta(days=4)
@@ -147,8 +140,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) ->
repack=False,
)
assert not finished
# states_manager.oldest_ts is not updated until after the purge is complete
assert recorder_mock.states_manager.oldest_ts == oldest_ts
with session_scope(hass=hass) as session:
states = session.query(States)
@@ -171,8 +162,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) ->
finished = purge_old_data(recorder_mock, purge_before, repack=False)
assert finished
# states_manager.oldest_ts should now be updated
assert recorder_mock.states_manager.oldest_ts != oldest_ts
with session_scope(hass=hass) as session:
states = session.query(States)
@@ -180,10 +169,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) ->
assert states.count() == 2
assert state_attributes.count() == 1
assert recorder_mock.states_manager.oldest_ts != oldest_ts
assert recorder_mock.states_manager.oldest_ts == states[0].last_updated_ts
oldest_ts = recorder_mock.states_manager.oldest_ts
assert "test.recorder2" in recorder_mock.states_manager._last_committed_id
# run purge_old_data again
@@ -196,8 +181,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) ->
repack=False,
)
assert not finished
# states_manager.oldest_ts is not updated until after the purge is complete
assert recorder_mock.states_manager.oldest_ts == oldest_ts
with session_scope(hass=hass) as session:
assert states.count() == 0
+6
View File
@@ -429,6 +429,12 @@ async def test_protect_loop_load_verify_locations(
context.load_verify_locations("/dev/null")
assert "Detected blocking call to load_verify_locations" in caplog.text
# ignore with only cadata
caplog.clear()
with pytest.raises(ssl.SSLError):
context.load_verify_locations(cadata="xxx")
assert "Detected blocking call to load_verify_locations" not in caplog.text
async def test_protect_loop_load_cert_chain(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture