diff --git a/.strict-typing b/.strict-typing index 313dda48649..86fbf3c3563 100644 --- a/.strict-typing +++ b/.strict-typing @@ -163,7 +163,6 @@ homeassistant.components.easyenergy.* homeassistant.components.ecovacs.* homeassistant.components.ecowitt.* homeassistant.components.efergy.* -homeassistant.components.electrasmart.* homeassistant.components.electric_kiwi.* homeassistant.components.elgato.* homeassistant.components.elkm1.* diff --git a/CODEOWNERS b/CODEOWNERS index 32f885f6015..97765fd5553 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1486,8 +1486,6 @@ build.json @home-assistant/supervisor /tests/components/unifi/ @Kane610 /homeassistant/components/unifi_direct/ @tofuSCHNITZEL /homeassistant/components/unifiled/ @florisvdk -/homeassistant/components/unifiprotect/ @bdraco -/tests/components/unifiprotect/ @bdraco /homeassistant/components/upb/ @gwww /tests/components/upb/ @gwww /homeassistant/components/upc_connect/ @pvizeli @fabaff diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index 0fe60724154..a87147c8f09 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -2,5 +2,5 @@ DOMAIN = "aladdin_connect" -OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html" +OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html" OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token" diff --git a/homeassistant/components/azure_data_explorer/__init__.py b/homeassistant/components/azure_data_explorer/__init__.py index 62718d6938e..319f7e4389b 100644 --- a/homeassistant/components/azure_data_explorer/__init__.py +++ b/homeassistant/components/azure_data_explorer/__init__.py @@ -62,13 +62,12 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: Adds an empty filter to hass data. Tries to get a filter from yaml, if present set to hass data. - If config is empty after getting the filter, return, otherwise emit - deprecated warning and pass the rest to the config flow. """ - hass.data.setdefault(DOMAIN, {DATA_FILTER: {}}) + hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})}) if DOMAIN in yaml_config: - hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN][CONF_FILTER] + hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER) + return True @@ -207,6 +206,6 @@ class AzureDataExplorer: if "\n" in state.state: return None, dropped + 1 - json_event = str(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8")) + json_event = json.dumps(obj=state, cls=JSONEncoder) return (json_event, dropped) diff --git a/homeassistant/components/azure_data_explorer/client.py b/homeassistant/components/azure_data_explorer/client.py index 40528bc6a6f..88609ff8e10 100644 --- a/homeassistant/components/azure_data_explorer/client.py +++ b/homeassistant/components/azure_data_explorer/client.py @@ -23,7 +23,7 @@ from .const import ( CONF_APP_REG_ID, CONF_APP_REG_SECRET, CONF_AUTHORITY_ID, - CONF_USE_FREE, + CONF_USE_QUEUED_CLIENT, ) _LOGGER = logging.getLogger(__name__) @@ -35,7 +35,6 @@ class AzureDataExplorerClient: def __init__(self, data: Mapping[str, Any]) -> None: """Create the right class.""" - self._cluster_ingest_uri = data[CONF_ADX_CLUSTER_INGEST_URI] self._database = data[CONF_ADX_DATABASE_NAME] self._table = data[CONF_ADX_TABLE_NAME] self._ingestion_properties = IngestionProperties( @@ -45,24 +44,36 @@ class AzureDataExplorerClient: ingestion_mapping_reference="ha_json_mapping", ) - # Create cLient for ingesting and querying data - kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication( - self._cluster_ingest_uri, - data[CONF_APP_REG_ID], - data[CONF_APP_REG_SECRET], - data[CONF_AUTHORITY_ID], + # Create client for ingesting data + kcsb_ingest = ( + KustoConnectionStringBuilder.with_aad_application_key_authentication( + data[CONF_ADX_CLUSTER_INGEST_URI], + data[CONF_APP_REG_ID], + data[CONF_APP_REG_SECRET], + data[CONF_AUTHORITY_ID], + ) ) - if data[CONF_USE_FREE] is True: - # Queded is the only option supported on free tear of ADX - self.write_client = QueuedIngestClient(kcsb) - else: - self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb) + # Create client for querying data + kcsb_query = ( + KustoConnectionStringBuilder.with_aad_application_key_authentication( + data[CONF_ADX_CLUSTER_INGEST_URI].replace("ingest-", ""), + data[CONF_APP_REG_ID], + data[CONF_APP_REG_SECRET], + data[CONF_AUTHORITY_ID], + ) + ) - self.query_client = KustoClient(kcsb) + if data[CONF_USE_QUEUED_CLIENT] is True: + # Queded is the only option supported on free tear of ADX + self.write_client = QueuedIngestClient(kcsb_ingest) + else: + self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest) + + self.query_client = KustoClient(kcsb_query) def test_connection(self) -> None: - """Test connection, will throw Exception when it cannot connect.""" + """Test connection, will throw Exception if it cannot connect.""" query = f"{self._table} | take 1" diff --git a/homeassistant/components/azure_data_explorer/config_flow.py b/homeassistant/components/azure_data_explorer/config_flow.py index d8390246b41..4ffb5ea7cf7 100644 --- a/homeassistant/components/azure_data_explorer/config_flow.py +++ b/homeassistant/components/azure_data_explorer/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigFlowResult +from homeassistant.helpers.selector import BooleanSelector from . import AzureDataExplorerClient from .const import ( @@ -19,7 +20,7 @@ from .const import ( CONF_APP_REG_ID, CONF_APP_REG_SECRET, CONF_AUTHORITY_ID, - CONF_USE_FREE, + CONF_USE_QUEUED_CLIENT, DEFAULT_OPTIONS, DOMAIN, ) @@ -34,7 +35,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( vol.Required(CONF_APP_REG_ID): str, vol.Required(CONF_APP_REG_SECRET): str, vol.Required(CONF_AUTHORITY_ID): str, - vol.Optional(CONF_USE_FREE, default=False): bool, + vol.Required(CONF_USE_QUEUED_CLIENT, default=False): BooleanSelector(), } ) diff --git a/homeassistant/components/azure_data_explorer/const.py b/homeassistant/components/azure_data_explorer/const.py index ca98110597a..a88a6b8b94f 100644 --- a/homeassistant/components/azure_data_explorer/const.py +++ b/homeassistant/components/azure_data_explorer/const.py @@ -17,7 +17,7 @@ CONF_AUTHORITY_ID = "authority_id" CONF_SEND_INTERVAL = "send_interval" CONF_MAX_DELAY = "max_delay" CONF_FILTER = DATA_FILTER = "filter" -CONF_USE_FREE = "use_queued_ingestion" +CONF_USE_QUEUED_CLIENT = "use_queued_ingestion" DATA_HUB = "hub" STEP_USER = "user" diff --git a/homeassistant/components/azure_data_explorer/strings.json b/homeassistant/components/azure_data_explorer/strings.json index 64005872579..c8ec158a844 100644 --- a/homeassistant/components/azure_data_explorer/strings.json +++ b/homeassistant/components/azure_data_explorer/strings.json @@ -3,15 +3,19 @@ "step": { "user": { "title": "Setup your Azure Data Explorer integration", - "description": "Enter connection details.", + "description": "Enter connection details", "data": { - "cluster_ingest_uri": "Cluster ingest URI", - "database": "Database name", - "table": "Table name", + "cluster_ingest_uri": "Cluster Ingest URI", + "authority_id": "Authority ID", "client_id": "Client ID", "client_secret": "Client secret", - "authority_id": "Authority ID", + "database": "Database name", + "table": "Table name", "use_queued_ingestion": "Use queued ingestion" + }, + "data_description": { + "cluster_ingest_uri": "Ingest-URI of the cluster", + "use_queued_ingestion": "Must be enabled when using ADX free cluster" } } }, diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 86a13de1ac8..c9a6eab5c62 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -120,7 +120,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: director_all_items = json.loads(director_all_items) entry_data[CONF_DIRECTOR_ALL_ITEMS] = director_all_items - entry_data[CONF_UI_CONFIGURATION] = json.loads(await director.getUiConfiguration()) + # Check if OS version is 3 or higher to get UI configuration + entry_data[CONF_UI_CONFIGURATION] = None + if int(entry_data[CONF_DIRECTOR_SW_VERSION].split(".")[0]) >= 3: + entry_data[CONF_UI_CONFIGURATION] = json.loads( + await director.getUiConfiguration() + ) # Load options from config entry entry_data[CONF_SCAN_INTERVAL] = entry.options.get( diff --git a/homeassistant/components/control4/media_player.py b/homeassistant/components/control4/media_player.py index 99d8c27face..72aa44faaed 100644 --- a/homeassistant/components/control4/media_player.py +++ b/homeassistant/components/control4/media_player.py @@ -81,11 +81,18 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Control4 rooms from a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + ui_config = entry_data[CONF_UI_CONFIGURATION] + + # OS 2 will not have a ui_configuration + if not ui_config: + _LOGGER.debug("No UI Configuration found for Control4") + return + all_rooms = await get_rooms(hass, entry) if not all_rooms: return - entry_data = hass.data[DOMAIN][entry.entry_id] scan_interval = entry_data[CONF_SCAN_INTERVAL] _LOGGER.debug("Scan interval = %s", scan_interval) @@ -119,8 +126,6 @@ async def async_setup_entry( if "parentId" in item and k > 1 } - ui_config = entry_data[CONF_UI_CONFIGURATION] - entity_list = [] for room in all_rooms: room_id = room["id"] diff --git a/homeassistant/components/electrasmart/manifest.json b/homeassistant/components/electrasmart/manifest.json index 405d9ee688a..e00b818e2a6 100644 --- a/homeassistant/components/electrasmart/manifest.json +++ b/homeassistant/components/electrasmart/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/electrasmart", "iot_class": "cloud_polling", - "requirements": ["pyElectra==1.2.0"] + "requirements": ["pyElectra==1.2.1"] } diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 2cd3d611bf5..339bed97f6f 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -59,7 +59,15 @@ class ElgatoLight(ElgatoEntity, LightEntity): self._attr_unique_id = coordinator.data.info.serial_number # Elgato Light supporting color, have a different temperature range - if self.coordinator.data.settings.power_on_hue is not None: + if ( + self.coordinator.data.info.product_name + in ( + "Elgato Light Strip", + "Elgato Light Strip Pro", + ) + or self.coordinator.data.settings.power_on_hue + or self.coordinator.data.state.hue is not None + ): self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} self._attr_min_mireds = 153 self._attr_max_mireds = 285 diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 037d82cd6c0..81f4f830833 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -141,10 +141,10 @@ class Enigma2Device(MediaPlayerEntity): self._device: OpenWebIfDevice = device self._entry = entry - self._attr_unique_id = device.mac_address + self._attr_unique_id = device.mac_address or entry.entry_id self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.mac_address)}, + identifiers={(DOMAIN, self._attr_unique_id)}, manufacturer=about["info"]["brand"], model=about["info"]["model"], configuration_url=device.base, diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 119608bbb2a..b962621edea 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -116,8 +116,9 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): ): """Initialize the alarm panel.""" self._partition_number = partition_number - self._code = code self._panic_type = panic_type + self._alarm_control_panel_option_default_code = code + self._attr_code_format = CodeFormat.NUMBER _LOGGER.debug("Setting up alarm: %s", alarm_name) super().__init__(alarm_name, info, controller) @@ -141,13 +142,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): if partition is None or int(partition) == self._partition_number: self.async_write_ha_state() - @property - def code_format(self) -> CodeFormat | None: - """Regex for code format or None if no code is required.""" - if self._code: - return None - return CodeFormat.NUMBER - @property def state(self) -> str: """Return the state of the device.""" @@ -169,34 +163,15 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" - if code: - self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number) - else: - self.hass.data[DATA_EVL].disarm_partition( - str(self._code), self._partition_number - ) + self.hass.data[DATA_EVL].disarm_partition(code, self._partition_number) async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" - if code: - self.hass.data[DATA_EVL].arm_stay_partition( - str(code), self._partition_number - ) - else: - self.hass.data[DATA_EVL].arm_stay_partition( - str(self._code), self._partition_number - ) + self.hass.data[DATA_EVL].arm_stay_partition(code, self._partition_number) async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" - if code: - self.hass.data[DATA_EVL].arm_away_partition( - str(code), self._partition_number - ) - else: - self.hass.data[DATA_EVL].arm_away_partition( - str(self._code), self._partition_number - ) + self.hass.data[DATA_EVL].arm_away_partition(code, self._partition_number) async def async_alarm_trigger(self, code: str | None = None) -> None: """Alarm trigger command. Will be used to trigger a panic alarm.""" @@ -204,9 +179,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" - self.hass.data[DATA_EVL].arm_night_partition( - str(code) if code else str(self._code), self._partition_number - ) + self.hass.data[DATA_EVL].arm_night_partition(code, self._partition_number) @callback def async_alarm_keypress(self, keypress=None): diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 27322b423d0..d3d19375105 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240605.0"] + "requirements": ["home-assistant-frontend==20240610.0"] } diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index 6598aeaafd8..1e3ef156d72 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -13,5 +13,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "iot_class": "local_polling", - "requirements": ["gardena-bluetooth==1.4.1"] + "requirements": ["gardena-bluetooth==1.4.2"] } diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 68101583b48..e129a375df2 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/glances", "iot_class": "local_polling", "loggers": ["glances_api"], - "requirements": ["glances-api==0.7.0"] + "requirements": ["glances-api==0.8.0"] } diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index e39634a5dd6..3d1daea9810 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1586,6 +1586,17 @@ class ArmDisArmTrait(_Trait): if features & required_feature != 0 ] + def _default_arm_state(self): + states = self._supported_states() + + if STATE_ALARM_TRIGGERED in states: + states.remove(STATE_ALARM_TRIGGERED) + + if len(states) != 1: + raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing") + + return states[0] + def sync_attributes(self): """Return ArmDisarm attributes for a sync request.""" response = {} @@ -1609,10 +1620,13 @@ class ArmDisArmTrait(_Trait): def query_attributes(self): """Return ArmDisarm query attributes.""" armed_state = self.state.attributes.get("next_state", self.state.state) - response = {"isArmed": armed_state in self.state_to_service} - if response["isArmed"]: - response.update({"currentArmLevel": armed_state}) - return response + + if armed_state in self.state_to_service: + return {"isArmed": True, "currentArmLevel": armed_state} + return { + "isArmed": False, + "currentArmLevel": self._default_arm_state(), + } async def execute(self, command, data, params, challenge): """Execute an ArmDisarm command.""" @@ -1620,15 +1634,7 @@ class ArmDisArmTrait(_Trait): # If no arm level given, we can only arm it if there is # only one supported arm type. We never default to triggered. if not (arm_level := params.get("armLevel")): - states = self._supported_states() - - if STATE_ALARM_TRIGGERED in states: - states.remove(STATE_ALARM_TRIGGERED) - - if len(states) != 1: - raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing") - - arm_level = states[0] + arm_level = self._default_arm_state() if self.state.state == arm_level: raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed") diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index 523198355d1..f115f3923b6 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: response = await model.generate_content_async(prompt_parts) except ( - ClientError, + GoogleAPICallError, ValueError, genai_types.BlockedPromptException, genai_types.StopCandidateException, diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 6b2f3c11dcc..65c0dc7fd93 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -2,11 +2,12 @@ from __future__ import annotations +import codecs from typing import Any, Literal -import google.ai.generativelanguage as glm from google.api_core.exceptions import GoogleAPICallError import google.generativeai as genai +from google.generativeai import protos import google.generativeai.types as genai_types from google.protobuf.json_format import MessageToDict import voluptuous as vol @@ -93,7 +94,7 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]: parameters = _format_schema(convert(tool.parameters)) - return glm.Tool( + return protos.Tool( { "function_declarations": [ { @@ -106,14 +107,14 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]: ) -def _adjust_value(value: Any) -> Any: - """Reverse unnecessary single quotes escaping.""" +def _escape_decode(value: Any) -> Any: + """Recursively call codecs.escape_decode on all values.""" if isinstance(value, str): - return value.replace("\\'", "'") + return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") # type: ignore[attr-defined] if isinstance(value, list): - return [_adjust_value(item) for item in value] + return [_escape_decode(item) for item in value] if isinstance(value, dict): - return {k: _adjust_value(v) for k, v in value.items()} + return {k: _escape_decode(v) for k, v in value.items()} return value @@ -334,10 +335,7 @@ class GoogleGenerativeAIConversationEntity( for function_call in function_calls: tool_call = MessageToDict(function_call._pb) # noqa: SLF001 tool_name = tool_call["name"] - tool_args = { - key: _adjust_value(value) - for key, value in tool_call["args"].items() - } + tool_args = _escape_decode(tool_call["args"]) LOGGER.debug("Tool call: %s(%s)", tool_name, tool_args) tool_input = llm.ToolInput(tool_name=tool_name, tool_args=tool_args) try: @@ -349,13 +347,13 @@ class GoogleGenerativeAIConversationEntity( LOGGER.debug("Tool response: %s", function_response) tool_responses.append( - glm.Part( - function_response=glm.FunctionResponse( + protos.Part( + function_response=protos.FunctionResponse( name=tool_name, response=function_response ) ) ) - chat_request = glm.Content(parts=tool_responses) + chat_request = protos.Content(parts=tool_responses) intent_response.async_set_speech( " ".join([part.text.strip() for part in chat_response.parts if part.text]) diff --git a/homeassistant/components/google_generative_ai_conversation/manifest.json b/homeassistant/components/google_generative_ai_conversation/manifest.json index 1886b16985f..168fee105a0 100644 --- a/homeassistant/components/google_generative_ai_conversation/manifest.json +++ b/homeassistant/components/google_generative_ai_conversation/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["google-generativeai==0.5.4", "voluptuous-openapi==0.0.4"] + "requirements": ["google-generativeai==0.6.0", "voluptuous-openapi==0.0.4"] } diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 203b1b3fc8e..2e6c321be1e 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -36,7 +36,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import ( @@ -45,6 +52,7 @@ from homeassistant.helpers.entity import ( get_unit_of_measurement, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, @@ -329,6 +337,7 @@ class SensorGroup(GroupEntity, SensorEntity): self._native_unit_of_measurement = unit_of_measurement self._valid_units: set[str | None] = set() self._can_convert: bool = False + self.calculate_attributes_later: CALLBACK_TYPE | None = None self._attr_name = name if name == DEFAULT_NAME: self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize() @@ -345,13 +354,32 @@ class SensorGroup(GroupEntity, SensorEntity): async def async_added_to_hass(self) -> None: """When added to hass.""" + for entity_id in self._entity_ids: + if self.hass.states.get(entity_id) is None: + self.calculate_attributes_later = async_track_state_change_event( + self.hass, self._entity_ids, self.calculate_state_attributes + ) + break + if not self.calculate_attributes_later: + await self.calculate_state_attributes() + await super().async_added_to_hass() + + async def calculate_state_attributes( + self, event: Event[EventStateChangedData] | None = None + ) -> None: + """Calculate state attributes.""" + for entity_id in self._entity_ids: + if self.hass.states.get(entity_id) is None: + return + if self.calculate_attributes_later: + self.calculate_attributes_later() + self.calculate_attributes_later = None self._attr_state_class = self._calculate_state_class(self._state_class) self._attr_device_class = self._calculate_device_class(self._device_class) self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement( self._native_unit_of_measurement ) self._valid_units = self._get_valid_units() - await super().async_added_to_hass() @callback def async_update_group_state(self) -> None: diff --git a/homeassistant/components/idasen_desk/config_flow.py b/homeassistant/components/idasen_desk/config_flow.py index b7c14089656..782d4988a3c 100644 --- a/homeassistant/components/idasen_desk/config_flow.py +++ b/homeassistant/components/idasen_desk/config_flow.py @@ -64,7 +64,7 @@ class IdasenDeskConfigFlow(ConfigFlow, domain=DOMAIN): desk = Desk(None, monitor_height=False) try: - await desk.connect(discovery_info.device, auto_reconnect=False) + await desk.connect(discovery_info.device, retry=False) except AuthFailedError: errors["base"] = "auth_failed" except TimeoutError: diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index fe714691f13..08946a802f1 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["imgw_pib==1.0.4"] + "requirements": ["imgw_pib==1.0.5"] } diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 5a975bb19ec..d3ba571e65a 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==4.0.4"] + "requirements": ["google-nest-sdm==4.0.5"] } diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 78c0bc88ef7..7d09f710828 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -388,12 +388,12 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): async def async_turn_off(self) -> None: """Turn off the zone.""" - await self.async_set_hvac_mode(OPERATION_MODE_OFF) + await self.async_set_hvac_mode(HVACMode.OFF) self._signal_zone_update() async def async_turn_on(self) -> None: """Turn on the zone.""" - await self.async_set_hvac_mode(OPERATION_MODE_AUTO) + await self.async_set_hvac_mode(HVACMode.AUTO) self._signal_zone_update() async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: diff --git a/homeassistant/components/openweathermap/repairs.py b/homeassistant/components/openweathermap/repairs.py index 0f411a45405..c54484e1e1e 100644 --- a/homeassistant/components/openweathermap/repairs.py +++ b/homeassistant/components/openweathermap/repairs.py @@ -73,7 +73,7 @@ def async_create_issue(hass: HomeAssistant, entry_id: str) -> None: domain=DOMAIN, issue_id=_get_issue_id(entry_id), is_fixable=True, - is_persistent=True, + is_persistent=False, severity=ir.IssueSeverity.WARNING, learn_more_url="https://www.home-assistant.io/integrations/openweathermap/", translation_key="deprecated_v25", diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 7e16bacdfda..d419fdcb043 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.4.6"] + "requirements": ["opower==0.4.7"] } diff --git a/homeassistant/components/ping/helpers.py b/homeassistant/components/ping/helpers.py index f1fd8518d42..7f1696d2ed9 100644 --- a/homeassistant/components/ping/helpers.py +++ b/homeassistant/components/ping/helpers.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any from icmplib import NameLookupError, async_ping from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import UpdateFailed from .const import ICMP_TIMEOUT, PING_TIMEOUT @@ -58,9 +59,16 @@ class PingDataICMPLib(PingData): timeout=ICMP_TIMEOUT, privileged=self._privileged, ) - except NameLookupError: + except NameLookupError as err: self.is_alive = False - return + raise UpdateFailed(f"Error resolving host: {self.ip_address}") from err + + _LOGGER.debug( + "async_ping returned: reachable=%s sent=%i received=%s", + data.is_alive, + data.packets_sent, + data.packets_received, + ) self.is_alive = data.is_alive if not self.is_alive: @@ -94,6 +102,10 @@ class PingDataSubProcess(PingData): async def async_ping(self) -> dict[str, Any] | None: """Send ICMP echo request and return details if success.""" + _LOGGER.debug( + "Pinging %s with: `%s`", self.ip_address, " ".join(self._ping_cmd) + ) + pinger = await asyncio.create_subprocess_exec( *self._ping_cmd, stdin=None, @@ -140,20 +152,17 @@ class PingDataSubProcess(PingData): if TYPE_CHECKING: assert match is not None rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() - except TimeoutError: - _LOGGER.exception( - "Timed out running command: `%s`, after: %ss", - self._ping_cmd, - self._count + PING_TIMEOUT, - ) + except TimeoutError as err: if pinger: with suppress(TypeError): await pinger.kill() # type: ignore[func-returns-value] del pinger - return None - except AttributeError: - return None + raise UpdateFailed( + f"Timed out running command: `{self._ping_cmd}`, after: {self._count + PING_TIMEOUT}s" + ) from err + except AttributeError as err: + raise UpdateFailed from err return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} async def async_update(self) -> None: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 7b5c6811e29..0d76cd93724 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1245,7 +1245,7 @@ def _first_statistic( table: type[StatisticsBase], metadata_id: int, ) -> datetime | None: - """Return the data of the oldest statistic row for a given metadata id.""" + """Return the date of the oldest statistic row for a given metadata id.""" stmt = lambda_stmt( lambda: select(table.start_ts) .filter(table.metadata_id == metadata_id) @@ -1257,12 +1257,30 @@ def _first_statistic( return None +def _last_statistic( + session: Session, + table: type[StatisticsBase], + metadata_id: int, +) -> datetime | None: + """Return the date of the newest statistic row for a given metadata id.""" + stmt = lambda_stmt( + lambda: select(table.start_ts) + .filter(table.metadata_id == metadata_id) + .order_by(table.start_ts.desc()) + .limit(1) + ) + if stats := cast(Sequence[Row], execute_stmt_lambda_element(session, stmt)): + return dt_util.utc_from_timestamp(stats[0].start_ts) + return None + + def _get_oldest_sum_statistic( session: Session, head_start_time: datetime | None, main_start_time: datetime | None, tail_start_time: datetime | None, oldest_stat: datetime | None, + oldest_5_min_stat: datetime | None, tail_only: bool, metadata_id: int, ) -> float | None: @@ -1307,6 +1325,15 @@ def _get_oldest_sum_statistic( if ( head_start_time is not None + and oldest_5_min_stat is not None + and ( + # If we want stats older than the short term purge window, don't lookup + # the oldest sum in the short term table, as it would be prioritized + # over older LongTermStats. + (oldest_stat is None) + or (oldest_5_min_stat < oldest_stat) + or (oldest_5_min_stat <= head_start_time) + ) and ( oldest_sum := _get_oldest_sum_statistic_in_sub_period( session, head_start_time, StatisticsShortTerm, metadata_id @@ -1477,13 +1504,16 @@ def statistic_during_period( tail_start_time: datetime | None = None tail_end_time: datetime | None = None if end_time is None: - tail_start_time = now.replace(minute=0, second=0, microsecond=0) + tail_start_time = _last_statistic(session, Statistics, metadata_id) + if tail_start_time: + tail_start_time += Statistics.duration + else: + tail_start_time = now.replace(minute=0, second=0, microsecond=0) + elif tail_only: + tail_start_time = start_time + tail_end_time = end_time elif end_time.minute: - tail_start_time = ( - start_time - if tail_only - else end_time.replace(minute=0, second=0, microsecond=0) - ) + tail_start_time = end_time.replace(minute=0, second=0, microsecond=0) tail_end_time = end_time # Calculate the main period @@ -1518,6 +1548,7 @@ def statistic_during_period( main_start_time, tail_start_time, oldest_stat, + oldest_5_min_stat, tail_only, metadata_id, ) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 3fd6dd7d782..42c0f9ba347 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.2.3", + "python-roborock==2.3.0", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index f49ae276665..992c86d5d7e 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -297,16 +297,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if version == 2: if minor_version < 2: # Cleanup invalid MAC addresses - see #103512 - dev_reg = dr.async_get(hass) - for device in dr.async_entries_for_config_entry( - dev_reg, config_entry.entry_id - ): - new_connections = device.connections.copy() - new_connections.discard((dr.CONNECTION_NETWORK_MAC, "none")) - if new_connections != device.connections: - dev_reg.async_update_device( - device.id, new_connections=new_connections - ) + # Reverted due to device registry collisions - see #119082 / #119249 minor_version = 2 hass.config_entries.async_update_entry(config_entry, minor_version=2) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 1bcd9c7c1e4..cc1ea5e81a6 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -import contextlib from typing import Final from aioshelly.block_device import BlockDevice @@ -301,13 +300,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> b entry, platforms ): if shelly_entry_data.rpc: - with contextlib.suppress(DeviceConnectionError): - # If the device is restarting or has gone offline before - # the ping/pong timeout happens, the shutdown command - # will fail, but we don't care since we are unloading - # and if we setup again, we will fix anything that is - # in an inconsistent state at that time. - await shelly_entry_data.rpc.shutdown() + await shelly_entry_data.rpc.shutdown() return unload_ok diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index c12e1aea289..5bb05d48d62 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -625,7 +625,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.connected: # Already connected return self.connected = True - await self._async_run_connected_events() + try: + await self._async_run_connected_events() + except DeviceConnectionError as err: + LOGGER.error( + "Error running connected events for device %s: %s", self.name, err + ) + self.last_update_success = False async def _async_run_connected_events(self) -> None: """Run connected events. @@ -699,10 +705,18 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.device.connected: try: await async_stop_scanner(self.device) + await super().shutdown() except InvalidAuthError: self.entry.async_start_reauth(self.hass) return - await super().shutdown() + except DeviceConnectionError as err: + # If the device is restarting or has gone offline before + # the ping/pong timeout happens, the shutdown command + # will fail, but we don't care since we are unloading + # and if we setup again, we will fix anything that is + # in an inconsistent state at that time. + LOGGER.debug("Error during shutdown for device %s: %s", self.name, err) + return await self._async_disconnected(False) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 2e8c2d59c1e..b1b00e40c66 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==10.0.0"], + "requirements": ["aioshelly==10.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 35d3008b416..11839caf8be 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -2,6 +2,7 @@ from __future__ import annotations +from aiohttp import ClientTimeout from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED from synology_dsm.exceptions import ( SynologyDSMAPIErrorException, @@ -40,7 +41,7 @@ DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 # Options DEFAULT_SCAN_INTERVAL = 15 # min -DEFAULT_TIMEOUT = 30 # sec +DEFAULT_TIMEOUT = ClientTimeout(total=60, connect=15) DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index caecfcbd0c9..b1133fd61ad 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "iot_class": "local_polling", "loggers": ["synology_dsm"], - "requirements": ["py-synologydsm-api==2.4.2"], + "requirements": ["py-synologydsm-api==2.4.4"], "ssdp": [ { "manufacturer": "Synology", diff --git a/homeassistant/components/thethingsnetwork/manifest.json b/homeassistant/components/thethingsnetwork/manifest.json index b8b1dbd7e1d..bc132d171f2 100644 --- a/homeassistant/components/thethingsnetwork/manifest.json +++ b/homeassistant/components/thethingsnetwork/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["ttn_client==0.0.4"] + "requirements": ["ttn_client==1.0.0"] } diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index d85f91be860..0f41011361d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -6,14 +6,14 @@ from datetime import timedelta import logging from aiohttp.client_exceptions import ServerDisconnectedError -from pyunifiprotect.data import Bootstrap -from pyunifiprotect.data.types import FirmwareReleaseChannel -from pyunifiprotect.exceptions import ClientError, NotAuthorized +from uiprotect.data import Bootstrap +from uiprotect.data.types import FirmwareReleaseChannel +from uiprotect.exceptions import ClientError, NotAuthorized -# Import the test_util.anonymize module from the pyunifiprotect package +# Import the test_util.anonymize module from the uiprotect package # in __init__ to ensure it gets imported in the executor since the # diagnostics module will not be imported in the executor. -from pyunifiprotect.test_util.anonymize import anonymize_data # noqa: F401 +from uiprotect.test_util.anonymize import anonymize_data # noqa: F401 from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index f779fc7a1ad..b6aaed8f975 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -6,7 +6,7 @@ import dataclasses import logging from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( NVR, Camera, Light, @@ -16,7 +16,7 @@ from pyunifiprotect.data import ( ProtectModelWithId, Sensor, ) -from pyunifiprotect.data.nvr import UOSDisk +from uiprotect.data.nvr import UOSDisk from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index db27306aedf..0db05a6cdc9 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -6,7 +6,7 @@ from dataclasses import dataclass import logging from typing import Final -from pyunifiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId +from uiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId from homeassistant.components.button import ( ButtonDeviceClass, diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 8e10c09872b..6b667c1f57e 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -6,7 +6,7 @@ from collections.abc import Generator import logging from typing import Any, cast -from pyunifiprotect.data import ( +from uiprotect.data import ( Camera as UFPCamera, CameraChannel, ModelType, diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 19561a6003d..284b7003485 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -8,9 +8,9 @@ from pathlib import Path from typing import Any from aiohttp import CookieJar -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import NVR -from pyunifiprotect.exceptions import ClientError, NotAuthorized +from uiprotect import ProtectApiClient +from uiprotect.data import NVR +from uiprotect.exceptions import ClientError, NotAuthorized from unifi_discovery import async_console_is_alive import voluptuous as vol diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 39be5f0e7cb..f51a58aadc7 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -1,6 +1,6 @@ """Constant definitions for UniFi Protect Integration.""" -from pyunifiprotect.data import ModelType, Version +from uiprotect.data import ModelType, Version from homeassistant.const import Platform diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index b64a08749d5..7b1c73d6dcc 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -8,8 +8,8 @@ from functools import partial import logging from typing import Any, cast -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import ( +from uiprotect import ProtectApiClient +from uiprotect.data import ( NVR, Bootstrap, Camera, @@ -20,8 +20,8 @@ from pyunifiprotect.data import ( ProtectAdoptableDeviceModel, WSSubscriptionMessage, ) -from pyunifiprotect.exceptions import ClientError, NotAuthorized -from pyunifiprotect.utils import log_event +from uiprotect.exceptions import ClientError, NotAuthorized +from uiprotect.utils import log_event from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback diff --git a/homeassistant/components/unifiprotect/diagnostics.py b/homeassistant/components/unifiprotect/diagnostics.py index b85870a08c5..ac651f6138d 100644 --- a/homeassistant/components/unifiprotect/diagnostics.py +++ b/homeassistant/components/unifiprotect/diagnostics.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any, cast -from pyunifiprotect.test_util.anonymize import anonymize_data +from uiprotect.test_util.anonymize import anonymize_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 49478ce0582..766c93949bd 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Sequence import logging from typing import TYPE_CHECKING, Any -from pyunifiprotect.data import ( +from uiprotect.data import ( NVR, Camera, Chime, diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index 3ce236b3e23..18e611f2307 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( Light, ModelType, ProtectAdoptableDeviceModel, diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index c54f9b316ff..6bb1dd7b4ee 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging from typing import Any, cast -from pyunifiprotect.data import ( +from uiprotect.data import ( Doorlock, LockStatusType, ModelType, diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 5570d088a7d..00a96483f70 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -1,7 +1,7 @@ { "domain": "unifiprotect", "name": "UniFi Protect", - "codeowners": ["@bdraco"], + "codeowners": [], "config_flow": true, "dependencies": ["http", "repairs"], "dhcp": [ @@ -39,9 +39,8 @@ "documentation": "https://www.home-assistant.io/integrations/unifiprotect", "integration_type": "hub", "iot_class": "local_push", - "loggers": ["pyunifiprotect", "unifi_discovery"], - "quality_scale": "platinum", - "requirements": ["pyunifiprotect==5.1.2", "unifi-discovery==1.1.8"], + "loggers": ["uiprotect", "unifi_discovery"], + "requirements": ["uiprotect==0.4.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 50fec39e9cb..eb17137842b 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -5,14 +5,14 @@ from __future__ import annotations import logging from typing import Any, cast -from pyunifiprotect.data import ( +from uiprotect.data import ( Camera, ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, StateType, ) -from pyunifiprotect.exceptions import StreamError +from uiprotect.exceptions import StreamError from homeassistant.components import media_source from homeassistant.components.media_player import ( diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 0ff27f562ea..1a67efcfd03 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -7,15 +7,9 @@ from datetime import date, datetime, timedelta from enum import Enum from typing import Any, NoReturn, cast -from pyunifiprotect.data import ( - Camera, - Event, - EventType, - ModelType, - SmartDetectObjectType, -) -from pyunifiprotect.exceptions import NvrError -from pyunifiprotect.utils import from_js_time +from uiprotect.data import Camera, Event, EventType, ModelType, SmartDetectObjectType +from uiprotect.exceptions import NvrError +from uiprotect.utils import from_js_time from yarl import URL from homeassistant.components.camera import CameraImageView diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py index cfc8cff7618..a95341f497a 100644 --- a/homeassistant/components/unifiprotect/migrate.py +++ b/homeassistant/components/unifiprotect/migrate.py @@ -6,8 +6,8 @@ from itertools import chain import logging from typing import TypedDict -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import Bootstrap +from uiprotect import ProtectApiClient +from uiprotect.data import Bootstrap from homeassistant.components.automation import automations_with_entity from homeassistant.components.script import scripts_with_entity diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index a9c79556135..d2ab31d672d 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -8,7 +8,7 @@ from enum import Enum import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar -from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel +from uiprotect.data import NVR, Event, ProtectAdoptableDeviceModel from homeassistant.helpers.entity import EntityDescription diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 49c629ac42f..ceb8614e77e 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -7,7 +7,7 @@ from datetime import timedelta import logging from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( Camera, Doorlock, Light, diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py index baf08c9b5cf..3cc8967ea0d 100644 --- a/homeassistant/components/unifiprotect/repairs.py +++ b/homeassistant/components/unifiprotect/repairs.py @@ -4,9 +4,9 @@ from __future__ import annotations from typing import cast -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import Bootstrap, Camera, ModelType -from pyunifiprotect.data.types import FirmwareReleaseChannel +from uiprotect import ProtectApiClient +from uiprotect.data import Bootstrap, Camera, ModelType +from uiprotect.data.types import FirmwareReleaseChannel import voluptuous as vol from homeassistant import data_entry_flow diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 6ba90948fca..f4a9d58e346 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -8,8 +8,8 @@ from enum import Enum import logging from typing import Any, Final -from pyunifiprotect.api import ProtectApiClient -from pyunifiprotect.data import ( +from uiprotect.api import ProtectApiClient +from uiprotect.data import ( Camera, ChimeType, DoorbellMessageType, diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 63c9e11c660..00849c095f0 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -7,7 +7,7 @@ from datetime import datetime import logging from typing import Any, cast -from pyunifiprotect.data import ( +from uiprotect.data import ( NVR, Camera, Light, diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 8c62664f55b..c5c2ffc8bfe 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -7,9 +7,9 @@ import functools from typing import Any, cast from pydantic import ValidationError -from pyunifiprotect.api import ProtectApiClient -from pyunifiprotect.data import Camera, Chime -from pyunifiprotect.exceptions import ClientError +from uiprotect.api import ProtectApiClient +from uiprotect.data import Camera, Chime +from uiprotect.exceptions import ClientError import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorDeviceClass diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index bd7cfa4d2a2..d17b208de12 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -6,7 +6,7 @@ from dataclasses import dataclass import logging from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( NVR, Camera, ProtectAdoptableDeviceModel, diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index 584bd511ee5..05e6712fa65 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( Camera, DoorbellMessageType, ProtectAdoptableDeviceModel, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 8199d729943..8a3028bcea7 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -10,8 +10,8 @@ import socket from typing import Any from aiohttp import CookieJar -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import ( +from uiprotect import ProtectApiClient +from uiprotect.data import ( Bootstrap, CameraChannel, Light, diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py index 0f9bff63689..b359fd5d948 100644 --- a/homeassistant/components/unifiprotect/views.py +++ b/homeassistant/components/unifiprotect/views.py @@ -9,8 +9,8 @@ from typing import Any from urllib.parse import urlencode from aiohttp import web -from pyunifiprotect.data import Camera, Event -from pyunifiprotect.exceptions import ClientError +from uiprotect.data import Camera, Event +from uiprotect.exceptions import ClientError from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index d742fd72858..cb04bd7d6ac 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waqi", "iot_class": "cloud_polling", "loggers": ["aiowaqi"], - "requirements": ["aiowaqi==3.0.1"] + "requirements": ["aiowaqi==3.1.0"] } diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 205f500746e..5df8e6c3d75 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -269,7 +269,7 @@ class IsWorkdaySensor(BinarySensorEntity): def _update_state_and_setup_listener(self) -> None: """Update state and setup listener for next interval.""" - now = dt_util.utcnow() + now = dt_util.now() self.update_data(now) self.unsub = async_track_point_in_utc_time( self.hass, self.point_in_time_listener, self.get_next_interval(now) diff --git a/homeassistant/const.py b/homeassistant/const.py index 86be19b95d8..500a74140f2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 3c240692d52..903e52af1a2 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -5,8 +5,10 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum +from functools import cache, partial from typing import Any +import slugify as unicode_slug import voluptuous as vol from homeassistant.components.climate.intent import INTENT_GET_TEMPERATURE @@ -175,10 +177,11 @@ class IntentTool(Tool): def __init__( self, + name: str, intent_handler: intent.IntentHandler, ) -> None: """Init the class.""" - self.name = intent_handler.intent_type + self.name = name self.description = ( intent_handler.description or f"Execute Home Assistant {self.name} intent" ) @@ -261,6 +264,9 @@ class AssistAPI(API): id=LLM_API_ASSIST, name="Assist", ) + self.cached_slugify = cache( + partial(unicode_slug.slugify, separator="_", lowercase=False) + ) async def async_get_api_instance(self, llm_context: LLMContext) -> APIInstance: """Return the instance of the API.""" @@ -373,7 +379,10 @@ class AssistAPI(API): or intent_handler.platforms & exposed_domains ] - return [IntentTool(intent_handler) for intent_handler in intent_handlers] + return [ + IntentTool(self.cached_slugify(intent_handler.intent_type), intent_handler) + for intent_handler in intent_handlers + ] def _get_exposed_entities( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 4d315f428c3..1a4d57e6929 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1758,10 +1758,6 @@ class Script: # runs before sleeping as otherwise if two runs are started at the exact # same time they will cancel each other out. self._log("Restarting") - # Important: yield to the event loop to allow the script to start in case - # the script is restarting itself so it ends up in the script stack and - # the recursion check above will prevent the script from running. - await asyncio.sleep(0) await self.async_stop(update_state=False, spare=run) if started_action: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b1d82e3c58b..c8c9419339d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.1 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240605.0 +home-assistant-frontend==20240610.0 home-assistant-intents==2024.6.5 httpx==0.27.0 ifaddr==0.2.0 diff --git a/mypy.ini b/mypy.ini index 4e4d9cc624b..ac3945872a1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1393,16 +1393,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.electrasmart.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.electric_kiwi.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/pyproject.toml b/pyproject.toml index 867bc1d1513..b71f80bbaf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.6.1" +version = "2024.6.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index 5d0a195b8e8..34c2e2bfa46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -353,7 +353,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.0 +aioshelly==10.0.1 # homeassistant.components.skybell aioskybell==22.7.0 @@ -389,7 +389,7 @@ aiovlc==0.3.2 aiovodafone==0.6.0 # homeassistant.components.waqi -aiowaqi==3.0.1 +aiowaqi==3.1.0 # homeassistant.components.watttime aiowatttime==0.1.1 @@ -912,7 +912,7 @@ fyta_cli==0.4.1 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena-bluetooth==1.4.1 +gardena-bluetooth==1.4.2 # homeassistant.components.google_assistant_sdk gassist-text==0.0.11 @@ -955,7 +955,7 @@ gios==4.0.0 gitterpy==0.1.7 # homeassistant.components.glances -glances-api==0.7.0 +glances-api==0.8.0 # homeassistant.components.goalzero goalzero==0.2.2 @@ -974,10 +974,10 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.google_generative_ai_conversation -google-generativeai==0.5.4 +google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.4 +google-nest-sdm==4.0.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -1087,7 +1087,7 @@ hole==0.8.0 holidays==0.50 # homeassistant.components.frontend -home-assistant-frontend==20240605.0 +home-assistant-frontend==20240610.0 # homeassistant.components.conversation home-assistant-intents==2024.6.5 @@ -1146,7 +1146,7 @@ iglo==1.2.7 ihcsdk==2.8.5 # homeassistant.components.imgw_pib -imgw_pib==1.0.4 +imgw_pib==1.0.5 # homeassistant.components.incomfort incomfort-client==0.5.0 @@ -1501,7 +1501,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.6 +opower==0.4.7 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1649,7 +1649,7 @@ py-schluter==0.1.7 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.2 +py-synologydsm-api==2.4.4 # homeassistant.components.zabbix py-zabbix==1.1.7 @@ -1670,7 +1670,7 @@ pyControl4==1.1.0 pyDuotecno==2024.5.1 # homeassistant.components.electrasmart -pyElectra==1.2.0 +pyElectra==1.2.1 # homeassistant.components.emby pyEmby==1.9 @@ -2306,7 +2306,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.2.3 +python-roborock==2.3.0 # homeassistant.components.smarttub python-smarttub==0.0.36 @@ -2357,9 +2357,6 @@ pytrydan==0.6.1 # homeassistant.components.usb pyudev==0.24.1 -# homeassistant.components.unifiprotect -pyunifiprotect==5.1.2 - # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2764,7 +2761,7 @@ transmission-rpc==7.0.3 ttls==1.5.1 # homeassistant.components.thethingsnetwork -ttn_client==0.0.4 +ttn_client==1.0.0 # homeassistant.components.tuya tuya-device-sharing-sdk==0.1.9 @@ -2781,6 +2778,9 @@ twitchAPI==4.0.0 # homeassistant.components.ukraine_alarm uasiren==0.0.1 +# homeassistant.components.unifiprotect +uiprotect==0.4.1 + # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae1a1f3fd72..5a0dd8f939e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.0 +aioshelly==10.0.1 # homeassistant.components.skybell aioskybell==22.7.0 @@ -362,7 +362,7 @@ aiovlc==0.3.2 aiovodafone==0.6.0 # homeassistant.components.waqi -aiowaqi==3.0.1 +aiowaqi==3.1.0 # homeassistant.components.watttime aiowatttime==0.1.1 @@ -747,7 +747,7 @@ fyta_cli==0.4.1 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena-bluetooth==1.4.1 +gardena-bluetooth==1.4.2 # homeassistant.components.google_assistant_sdk gassist-text==0.0.11 @@ -784,7 +784,7 @@ getmac==0.9.4 gios==4.0.0 # homeassistant.components.glances -glances-api==0.7.0 +glances-api==0.8.0 # homeassistant.components.goalzero goalzero==0.2.2 @@ -800,10 +800,10 @@ google-api-python-client==2.71.0 google-cloud-pubsub==2.13.11 # homeassistant.components.google_generative_ai_conversation -google-generativeai==0.5.4 +google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.4 +google-nest-sdm==4.0.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -889,7 +889,7 @@ hole==0.8.0 holidays==0.50 # homeassistant.components.frontend -home-assistant-frontend==20240605.0 +home-assistant-frontend==20240610.0 # homeassistant.components.conversation home-assistant-intents==2024.6.5 @@ -933,7 +933,7 @@ idasen-ha==2.5.3 ifaddr==0.2.0 # homeassistant.components.imgw_pib -imgw_pib==1.0.4 +imgw_pib==1.0.5 # homeassistant.components.influxdb influxdb-client==1.24.0 @@ -1201,7 +1201,7 @@ openhomedevice==2.2.0 openwebifpy==4.2.4 # homeassistant.components.opower -opower==0.4.6 +opower==0.4.7 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1311,7 +1311,7 @@ py-nightscout==1.2.2 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.2 +py-synologydsm-api==2.4.4 # homeassistant.components.seventeentrack py17track==2021.12.2 @@ -1326,7 +1326,7 @@ pyControl4==1.1.0 pyDuotecno==2024.5.1 # homeassistant.components.electrasmart -pyElectra==1.2.0 +pyElectra==1.2.1 # homeassistant.components.rfxtrx pyRFXtrx==0.31.1 @@ -1794,7 +1794,7 @@ python-qbittorrent==0.4.3 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.2.3 +python-roborock==2.3.0 # homeassistant.components.smarttub python-smarttub==0.0.36 @@ -1836,9 +1836,6 @@ pytrydan==0.6.1 # homeassistant.components.usb pyudev==0.24.1 -# homeassistant.components.unifiprotect -pyunifiprotect==5.1.2 - # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2138,7 +2135,7 @@ transmission-rpc==7.0.3 ttls==1.5.1 # homeassistant.components.thethingsnetwork -ttn_client==0.0.4 +ttn_client==1.0.0 # homeassistant.components.tuya tuya-device-sharing-sdk==0.1.9 @@ -2155,6 +2152,9 @@ twitchAPI==4.0.0 # homeassistant.components.ukraine_alarm uasiren==0.0.1 +# homeassistant.components.unifiprotect +uiprotect==0.4.1 + # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index f9a8ec2db92..d35d96121c5 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -30,7 +30,6 @@ PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") IGNORE_STANDARD_LIBRARY_VIOLATIONS = { # Integrations which have standard library requirements. - "electrasmart", "slide", "suez_water", } diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7b3d4c4010e..bd5957326ec 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -2771,6 +2771,7 @@ async def test_recursive_automation_starting_script( ], "action": [ {"service": "test.automation_started"}, + {"delay": 0.001}, {"service": "script.script1"}, ], } @@ -2817,7 +2818,10 @@ async def test_recursive_automation_starting_script( assert script_warning_msg in caplog.text -@pytest.mark.parametrize("automation_mode", SCRIPT_MODE_CHOICES) +@pytest.mark.parametrize( + "automation_mode", + [mode for mode in SCRIPT_MODE_CHOICES if mode != SCRIPT_MODE_RESTART], +) @pytest.mark.parametrize("wait_for_stop_scripts_after_shutdown", [True]) async def test_recursive_automation( hass: HomeAssistant, automation_mode, caplog: pytest.LogCaptureFixture @@ -2878,6 +2882,68 @@ async def test_recursive_automation( assert "Disallowed recursion detected" not in caplog.text +@pytest.mark.parametrize("wait_for_stop_scripts_after_shutdown", [True]) +async def test_recursive_automation_restart_mode( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test automation restarting itself. + + The automation is an infinite loop since it keeps restarting itself + + - Illegal recursion detection should not be triggered + - Home Assistant should not hang on shut down + """ + stop_scripts_at_shutdown_called = asyncio.Event() + real_stop_scripts_at_shutdown = _async_stop_scripts_at_shutdown + + async def stop_scripts_at_shutdown(*args): + await real_stop_scripts_at_shutdown(*args) + stop_scripts_at_shutdown_called.set() + + with patch( + "homeassistant.helpers.script._async_stop_scripts_at_shutdown", + wraps=stop_scripts_at_shutdown, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "mode": SCRIPT_MODE_RESTART, + "trigger": [ + {"platform": "event", "event_type": "trigger_automation"}, + ], + "action": [ + {"event": "trigger_automation"}, + {"service": "test.automation_done"}, + ], + } + }, + ) + + service_called = asyncio.Event() + + async def async_service_handler(service): + if service.service == "automation_done": + service_called.set() + + hass.services.async_register("test", "automation_done", async_service_handler) + + hass.bus.async_fire("trigger_automation") + await asyncio.sleep(0) + + # Trigger 1st stage script shutdown + hass.set_state(CoreState.stopping) + hass.bus.async_fire("homeassistant_stop") + await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1) + + # Trigger 2nd stage script shutdown + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) + await hass.async_block_till_done() + + assert "Disallowed recursion detected" not in caplog.text + + async def test_websocket_config( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -3097,3 +3163,72 @@ async def test_two_automations_call_restart_script_same_time( await hass.async_block_till_done() assert len(events) == 2 cancel() + + +async def test_two_automation_call_restart_script_right_after_each_other( + hass: HomeAssistant, +) -> None: + """Test two automations call a restart script right after each other.""" + + events = async_capture_events(hass, "repeat_test_script_finished") + + assert await async_setup_component( + hass, + input_boolean.DOMAIN, + { + input_boolean.DOMAIN: { + "test_1": None, + "test_2": None, + } + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "state", + "entity_id": ["input_boolean.test_1", "input_boolean.test_1"], + "from": "off", + "to": "on", + }, + "action": [ + { + "repeat": { + "count": 2, + "sequence": [ + { + "delay": { + "hours": 0, + "minutes": 0, + "seconds": 0, + "milliseconds": 100, + } + } + ], + } + }, + {"event": "repeat_test_script_finished", "event_data": {}}, + ], + "id": "automation_0", + "mode": "restart", + }, + ] + }, + ) + hass.states.async_set("input_boolean.test_1", "off") + hass.states.async_set("input_boolean.test_2", "off") + await hass.async_block_till_done() + hass.states.async_set("input_boolean.test_1", "on") + hass.states.async_set("input_boolean.test_2", "on") + await asyncio.sleep(0) + hass.states.async_set("input_boolean.test_1", "off") + hass.states.async_set("input_boolean.test_2", "off") + await asyncio.sleep(0) + hass.states.async_set("input_boolean.test_1", "on") + hass.states.async_set("input_boolean.test_2", "on") + await hass.async_block_till_done() + assert len(events) == 1 diff --git a/tests/components/azure_data_explorer/const.py b/tests/components/azure_data_explorer/const.py index d29f4d5ba93..d20be1584a1 100644 --- a/tests/components/azure_data_explorer/const.py +++ b/tests/components/azure_data_explorer/const.py @@ -8,7 +8,7 @@ from homeassistant.components.azure_data_explorer.const import ( CONF_APP_REG_SECRET, CONF_AUTHORITY_ID, CONF_SEND_INTERVAL, - CONF_USE_FREE, + CONF_USE_QUEUED_CLIENT, ) AZURE_DATA_EXPLORER_PATH = "homeassistant.components.azure_data_explorer" @@ -29,7 +29,7 @@ BASE_CONFIG_URI = { } BASIC_OPTIONS = { - CONF_USE_FREE: False, + CONF_USE_QUEUED_CLIENT: False, CONF_SEND_INTERVAL: 5, } @@ -39,10 +39,10 @@ BASE_CONFIG_FULL = BASE_CONFIG | BASIC_OPTIONS | BASE_CONFIG_URI BASE_CONFIG_IMPORT = { CONF_ADX_CLUSTER_INGEST_URI: "https://cluster.region.kusto.windows.net", - CONF_USE_FREE: False, + CONF_USE_QUEUED_CLIENT: False, CONF_SEND_INTERVAL: 5, } -FREE_OPTIONS = {CONF_USE_FREE: True, CONF_SEND_INTERVAL: 5} +FREE_OPTIONS = {CONF_USE_QUEUED_CLIENT: True, CONF_SEND_INTERVAL: 5} BASE_CONFIG_FREE = BASE_CONFIG | FREE_OPTIONS diff --git a/tests/components/elgato/fixtures/light-strip/info.json b/tests/components/elgato/fixtures/light-strip/info.json index e2a816df26e..a8c3200e4b9 100644 --- a/tests/components/elgato/fixtures/light-strip/info.json +++ b/tests/components/elgato/fixtures/light-strip/info.json @@ -1,5 +1,5 @@ { - "productName": "Elgato Key Light", + "productName": "Elgato Light Strip", "hardwareBoardType": 53, "firmwareBuildNumber": 192, "firmwareVersion": "1.0.3", diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index 6ef773a7304..e2f663d294b 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -218,7 +218,7 @@ 'labels': set({ }), 'manufacturer': 'Elgato', - 'model': 'Elgato Key Light', + 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, 'serial_number': 'CN11A1A00001', @@ -333,7 +333,7 @@ 'labels': set({ }), 'manufacturer': 'Elgato', - 'model': 'Elgato Key Light', + 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, 'serial_number': 'CN11A1A00001', diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 0ed4d960edc..c72ab3cb85e 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1931,7 +1931,10 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None: } } - assert trt.query_attributes() == {"isArmed": False} + assert trt.query_attributes() == { + "currentArmLevel": "armed_custom_bypass", + "isArmed": False, + } assert trt.can_execute(trait.COMMAND_ARMDISARM, {"arm": False}) diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 901216d262f..e84efffe7df 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -12,6 +12,9 @@ import voluptuous as vol from homeassistant.components import conversation from homeassistant.components.conversation import trace +from homeassistant.components.google_generative_ai_conversation.conversation import ( + _escape_decode, +) from homeassistant.const import CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -504,3 +507,18 @@ async def test_conversation_agent( mock_config_entry.entry_id ) assert agent.supported_languages == "*" + + +async def test_escape_decode() -> None: + """Test _escape_decode.""" + assert _escape_decode( + { + "param1": ["test_value", "param1\\'s value"], + "param2": "param2\\'s value", + "param3": {"param31": "Cheminée", "param32": "Chemin\\303\\251e"}, + } + ) == { + "param1": ["test_value", "param1's value"], + "param2": "param2's value", + "param3": {"param31": "Cheminée", "param32": "Cheminée"}, + } diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index c5331aa2f60..db642506361 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -763,3 +763,52 @@ async def test_last_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.test_last") assert str(float(value)) == state.state assert entity_id == state.attributes.get("last_entity_id") + + +async def test_sensors_attributes_added_when_entity_info_available( + hass: HomeAssistant, +) -> None: + """Test the sensor calculate attributes once all entities attributes are available.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": DEFAULT_NAME, + "type": "sum", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id", + } + } + + entity_ids = config["sensor"]["entities"] + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.sensor_group_sum") + + assert state.state == STATE_UNAVAILABLE + assert state.attributes.get(ATTR_ENTITY_ID) is None + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): + hass.states.async_set( + entity_id, + value, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + ATTR_UNIT_OF_MEASUREMENT: "L", + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.sensor_group_sum") + + assert float(state.state) == pytest.approx(float(SUM_VALUE)) + assert state.attributes.get(ATTR_ENTITY_ID) == entity_ids + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME + assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "L" diff --git a/tests/components/idasen_desk/test_config_flow.py b/tests/components/idasen_desk/test_config_flow.py index a861dc5f5e2..c27cdea58aa 100644 --- a/tests/components/idasen_desk/test_config_flow.py +++ b/tests/components/idasen_desk/test_config_flow.py @@ -305,4 +305,4 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: } assert result2["result"].unique_id == IDASEN_DISCOVERY_INFO.address assert len(mock_setup_entry.mock_calls) == 1 - desk_connect.assert_called_with(ANY, auto_reconnect=False) + desk_connect.assert_called_with(ANY, retry=False) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 9c8e0a9203a..b5c0f0bf02b 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -7,6 +7,7 @@ import threading from unittest.mock import ANY, patch from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components import recorder @@ -794,6 +795,347 @@ async def test_statistic_during_period_hole( } +@pytest.mark.parametrize( + "frozen_time", + [ + # This is the normal case, all statistics runs are available + datetime.datetime(2022, 10, 21, 6, 31, tzinfo=datetime.UTC), + # Statistic only available up until 6:25, this can happen if + # core has been shut down for an hour + datetime.datetime(2022, 10, 21, 7, 31, tzinfo=datetime.UTC), + ], +) +async def test_statistic_during_period_partial_overlap( + recorder_mock: Recorder, + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + frozen_time: datetime, +) -> None: + """Test statistic_during_period.""" + client = await hass_ws_client() + + freezer.move_to(frozen_time) + now = dt_util.utcnow() + + await async_recorder_block_till_done(hass) + + zero = now + start = zero.replace(hour=0, minute=0, second=0, microsecond=0) + + # Sum shall be tracking a hypothetical sensor that is 0 at midnight, and grows by 1 per minute. + # The test will have 4 hours of LTS-only data (0:00-3:59:59), followed by 2 hours of overlapping STS/LTS (4:00-5:59:59), followed by 30 minutes of STS only (6:00-6:29:59) + # similar to how a real recorder might look after purging STS. + + # The datapoint at i=0 (start = 0:00) will be 60 as that is the growth during the hour starting at the start period + imported_stats_hours = [ + { + "start": (start + timedelta(hours=i)), + "min": i * 60, + "max": i * 60 + 60, + "mean": i * 60 + 30, + "sum": (i + 1) * 60, + } + for i in range(6) + ] + + # The datapoint at i=0 (start = 4:00) would be the sensor's value at t=4:05, or 245 + imported_stats_5min = [ + { + "start": (start + timedelta(hours=4, minutes=5 * i)), + "min": 4 * 60 + i * 5, + "max": 4 * 60 + i * 5 + 5, + "mean": 4 * 60 + i * 5 + 2.5, + "sum": 4 * 60 + (i + 1) * 5, + } + for i in range(30) + ] + + assert imported_stats_hours[-1]["sum"] == 360 + assert imported_stats_hours[-1]["start"] == start.replace( + hour=5, minute=0, second=0, microsecond=0 + ) + assert imported_stats_5min[-1]["sum"] == 390 + assert imported_stats_5min[-1]["start"] == start.replace( + hour=6, minute=25, second=0, microsecond=0 + ) + + statId = "sensor.test_overlapping" + imported_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy overlapping", + "source": "recorder", + "statistic_id": statId, + "unit_of_measurement": "kWh", + } + + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats_hours, + Statistics, + ) + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats_5min, + StatisticsShortTerm, + ) + await async_wait_recording_done(hass) + + metadata = get_metadata(hass, statistic_ids={statId}) + metadata_id = metadata[statId][0] + run_cache = get_short_term_statistics_run_cache(hass) + # Verify the import of the short term statistics + # also updates the run cache + assert run_cache.get_latest_ids({metadata_id}) is not None + + # Get all the stats, should consider all hours and 5mins + await client.send_json_auto_id( + { + "type": "recorder/statistic_during_period", + "statistic_id": statId, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "change": 390, + "max": 390, + "min": 0, + "mean": 195, + } + + async def assert_stat_during_fixed(client, start_time, end_time, expect): + json = { + "type": "recorder/statistic_during_period", + "types": list(expect.keys()), + "statistic_id": statId, + "fixed_period": {}, + } + if start_time: + json["fixed_period"]["start_time"] = start_time.isoformat() + if end_time: + json["fixed_period"]["end_time"] = end_time.isoformat() + + await client.send_json_auto_id(json) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expect + + # One hours worth of growth in LTS-only + start_time = start.replace(hour=1) + end_time = start.replace(hour=2) + await assert_stat_during_fixed( + client, start_time, end_time, {"change": 60, "min": 60, "max": 120, "mean": 90} + ) + + # Five minutes of growth in STS-only + start_time = start.replace(hour=6, minute=15) + end_time = start.replace(hour=6, minute=20) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 6 * 60 + 15, + "max": 6 * 60 + 20, + "mean": 6 * 60 + (15 + 20) / 2, + }, + ) + + # Six minutes of growth in STS-only + start_time = start.replace(hour=6, minute=14) + end_time = start.replace(hour=6, minute=20) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 6 * 60 + 15, + "max": 6 * 60 + 20, + "mean": 6 * 60 + (15 + 20) / 2, + }, + ) + + # Six minutes of growth in STS-only + # 5-minute Change includes start times exactly on or before a statistics start, but end times are not counted unless they are greater than start. + start_time = start.replace(hour=6, minute=15) + end_time = start.replace(hour=6, minute=21) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 10, + "min": 6 * 60 + 15, + "max": 6 * 60 + 25, + "mean": 6 * 60 + (15 + 25) / 2, + }, + ) + + # Five minutes of growth in overlapping LTS+STS + start_time = start.replace(hour=5, minute=15) + end_time = start.replace(hour=5, minute=20) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 5 * 60 + 15, + "max": 5 * 60 + 20, + "mean": 5 * 60 + (15 + 20) / 2, + }, + ) + + # Five minutes of growth in overlapping LTS+STS (start of hour) + start_time = start.replace(hour=5, minute=0) + end_time = start.replace(hour=5, minute=5) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 5, "min": 5 * 60, "max": 5 * 60 + 5, "mean": 5 * 60 + (5) / 2}, + ) + + # Five minutes of growth in overlapping LTS+STS (end of hour) + start_time = start.replace(hour=4, minute=55) + end_time = start.replace(hour=5, minute=0) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 4 * 60 + 55, + "max": 5 * 60, + "mean": 4 * 60 + (55 + 60) / 2, + }, + ) + + # Five minutes of growth in STS-only, with a minute offset. Despite that this does not cover the full period, result is still 5 + start_time = start.replace(hour=6, minute=16) + end_time = start.replace(hour=6, minute=21) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 6 * 60 + 20, + "max": 6 * 60 + 25, + "mean": 6 * 60 + (20 + 25) / 2, + }, + ) + + # 7 minutes of growth in STS-only, spanning two intervals + start_time = start.replace(hour=6, minute=14) + end_time = start.replace(hour=6, minute=21) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 10, + "min": 6 * 60 + 15, + "max": 6 * 60 + 25, + "mean": 6 * 60 + (15 + 25) / 2, + }, + ) + + # One hours worth of growth in LTS-only, with arbitrary minute offsets + # Since this does not fully cover the hour, result is None? + start_time = start.replace(hour=1, minute=40) + end_time = start.replace(hour=2, minute=12) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": None, "min": None, "max": None, "mean": None}, + ) + + # One hours worth of growth in LTS-only, with arbitrary minute offsets, covering a whole 1-hour period + start_time = start.replace(hour=1, minute=40) + end_time = start.replace(hour=3, minute=12) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 60, "min": 120, "max": 180, "mean": 150}, + ) + + # 90 minutes of growth in window overlapping LTS+STS/STS-only (4:41 - 6:11) + start_time = start.replace(hour=4, minute=41) + end_time = start_time + timedelta(minutes=90) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 90, + "min": 4 * 60 + 45, + "max": 4 * 60 + 45 + 90, + "mean": 4 * 60 + 45 + 45, + }, + ) + + # 4 hours of growth in overlapping LTS-only/LTS+STS (2:01-6:01) + start_time = start.replace(hour=2, minute=1) + end_time = start_time + timedelta(minutes=240) + # 60 from LTS (3:00-3:59), 125 from STS (25 intervals) (4:00-6:01) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 185, "min": 3 * 60, "max": 3 * 60 + 185, "mean": 3 * 60 + 185 / 2}, + ) + + # 4 hours of growth in overlapping LTS-only/LTS+STS (1:31-5:31) + start_time = start.replace(hour=1, minute=31) + end_time = start_time + timedelta(minutes=240) + # 120 from LTS (2:00-3:59), 95 from STS (19 intervals) 4:00-5:31 + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 215, "min": 2 * 60, "max": 2 * 60 + 215, "mean": 2 * 60 + 215 / 2}, + ) + + # 5 hours of growth, start time only (1:31-end) + start_time = start.replace(hour=1, minute=31) + end_time = None + # will be actually 2:00 - end + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 4 * 60 + 30, "min": 120, "max": 390, "mean": (390 + 120) / 2}, + ) + + # 5 hours of growth, end_time_only (0:00-5:00) + start_time = None + end_time = start.replace(hour=5) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 5 * 60, "min": 0, "max": 5 * 60, "mean": (5 * 60) / 2}, + ) + + # 5 hours 1 minute of growth, end_time_only (0:00-5:01) + start_time = None + end_time = start.replace(hour=5, minute=1) + # 4 hours LTS, 1 hour and 5 minutes STS (4:00-5:01) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 5 * 60 + 5, "min": 0, "max": 5 * 60 + 5, "mean": (5 * 60 + 5) / 2}, + ) + + @pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC)) @pytest.mark.parametrize( ("calendar_period", "start_time", "end_time"), diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 4efcf62c1dd..479664d4ec0 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -220,10 +220,14 @@ async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.xfail async def test_cleanup_mac( hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion ) -> None: - """Test for `none` mac cleanup #103512.""" + """Test for `none` mac cleanup #103512. + + Reverted due to device registry collisions in #119249 / #119082 + """ entry = MockConfigEntry( domain=SAMSUNGTV_DOMAIN, data=MOCK_ENTRY_WS_WITH_MAC, diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1dc45a98c44..cd750e53f0b 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -15,12 +15,14 @@ from homeassistant.components.shelly.const import ( ATTR_CLICK_TYPE, ATTR_DEVICE, ATTR_GENERATION, + CONF_BLE_SCANNER_MODE, DOMAIN, ENTRY_RELOAD_COOLDOWN, MAX_PUSH_UPDATE_FAILURES, RPC_RECONNECT_INTERVAL, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, + BLEScannerMode, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE @@ -485,6 +487,25 @@ async def test_rpc_reload_with_invalid_auth( assert flow["context"].get("entry_id") == entry.entry_id +async def test_rpc_connection_error_during_unload( + hass: HomeAssistant, mock_rpc_device: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test RPC DeviceConnectionError suppressed during config entry unload.""" + entry = await init_integration(hass, 2) + + assert entry.state is ConfigEntryState.LOADED + + with patch( + "homeassistant.components.shelly.coordinator.async_stop_scanner", + side_effect=DeviceConnectionError, + ): + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert "Error during shutdown for device" in caplog.text + assert entry.state is ConfigEntryState.NOT_LOADED + + async def test_rpc_click_event( hass: HomeAssistant, mock_rpc_device: Mock, @@ -713,6 +734,32 @@ async def test_rpc_reconnect_error( assert get_entity_state(hass, "switch.test_switch_0") == STATE_UNAVAILABLE +async def test_rpc_error_running_connected_events( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test RPC error while running connected events.""" + with patch( + "homeassistant.components.shelly.coordinator.async_ensure_ble_enabled", + side_effect=DeviceConnectionError, + ): + await init_integration( + hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} + ) + + assert "Error running connected events for device" in caplog.text + assert get_entity_state(hass, "switch.test_switch_0") == STATE_UNAVAILABLE + + # Move time to generate reconnect without error + freezer.tick(timedelta(seconds=RPC_RECONNECT_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert get_entity_state(hass, "switch.test_switch_0") == STATE_ON + + async def test_rpc_polling_connection_error( hass: HomeAssistant, freezer: FrozenDateTimeFactory, diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 5b3f9653d75..9eb1ea312c6 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -13,8 +13,8 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import ( +from uiprotect import ProtectApiClient +from uiprotect.data import ( NVR, Bootstrap, Camera, diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 81ed02869b8..dbe8f72b244 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -5,8 +5,8 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from pyunifiprotect.data import Camera, Event, EventType, Light, MountType, Sensor -from pyunifiprotect.data.nvr import EventMetadata +from uiprotect.data import Camera, Event, EventType, Light, MountType, Sensor +from uiprotect.data.nvr import EventMetadata from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.unifiprotect.binary_sensor import ( diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index a38a29b5999..3a283093179 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data.devices import Camera, Chime, Doorlock +from uiprotect.data.devices import Camera, Chime, Doorlock from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index d374f61c2b0..444898fbd85 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -4,8 +4,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Camera as ProtectCamera, CameraChannel, StateType -from pyunifiprotect.exceptions import NvrError +from uiprotect.data import Camera as ProtectCamera, CameraChannel, StateType +from uiprotect.exceptions import NvrError from homeassistant.components.camera import ( CameraEntityFeature, diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 845766809b2..5d02e1cf098 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -7,8 +7,8 @@ import socket from unittest.mock import patch import pytest -from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient -from pyunifiprotect.data import NVR, Bootstrap, CloudAccount +from uiprotect import NotAuthorized, NvrError, ProtectApiClient +from uiprotect.data import NVR, Bootstrap, CloudAccount from homeassistant import config_entries from homeassistant.components import dhcp, ssdp diff --git a/tests/components/unifiprotect/test_diagnostics.py b/tests/components/unifiprotect/test_diagnostics.py index b13c069b37c..fd882929e96 100644 --- a/tests/components/unifiprotect/test_diagnostics.py +++ b/tests/components/unifiprotect/test_diagnostics.py @@ -1,6 +1,6 @@ """Test UniFi Protect diagnostics.""" -from pyunifiprotect.data import NVR, Light +from uiprotect.data import NVR, Light from homeassistant.components.unifiprotect.const import CONF_ALLOW_EA from homeassistant.core import HomeAssistant diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 9bb2141631b..3b75afaace8 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -4,8 +4,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch -from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient -from pyunifiprotect.data import NVR, Bootstrap, CloudAccount, Light +from uiprotect import NotAuthorized, NvrError, ProtectApiClient +from uiprotect.data import NVR, Bootstrap, CloudAccount, Light from homeassistant.components.unifiprotect.const import ( AUTH_RETRIES, diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index 57867a3c7e9..bb0b6992e4e 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -4,8 +4,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Light -from pyunifiprotect.data.types import LEDLevel +from uiprotect.data import Light +from uiprotect.data.types import LEDLevel from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 6785ea2a4f6..62a1cb9ff46 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Doorlock, LockStatusType +from uiprotect.data import Doorlock, LockStatusType from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.const import ( diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index 1558d11fbbe..642a3a1e372 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -5,8 +5,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock, patch import pytest -from pyunifiprotect.data import Camera -from pyunifiprotect.exceptions import StreamError +from uiprotect.data import Camera +from uiprotect.exceptions import StreamError from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 7e51031128e..2cdebeafb04 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -5,7 +5,7 @@ from ipaddress import IPv4Address from unittest.mock import AsyncMock, Mock, patch import pytest -from pyunifiprotect.data import ( +from uiprotect.data import ( Bootstrap, Camera, Event, @@ -13,7 +13,7 @@ from pyunifiprotect.data import ( Permission, SmartDetectObjectType, ) -from pyunifiprotect.exceptions import NvrError +from uiprotect.exceptions import NvrError from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source import MediaSourceItem diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index a48925d9c67..1fbb650b800 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import patch -from pyunifiprotect.data import Camera +from uiprotect.data import Camera from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.repairs.issue_handler import ( diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 3050992457c..77a409551b1 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -6,7 +6,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Doorlock, IRLEDMode, Light +from uiprotect.data import Camera, Doorlock, IRLEDMode, Light from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.number import ( diff --git a/tests/components/unifiprotect/test_recorder.py b/tests/components/unifiprotect/test_recorder.py index 3e1a8599ea7..94c93413de5 100644 --- a/tests/components/unifiprotect/test_recorder.py +++ b/tests/components/unifiprotect/test_recorder.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from pyunifiprotect.data import Camera, Event, EventType +from uiprotect.data import Camera, Event, EventType from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index f4be3164fd5..7d76550f7c7 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -6,7 +6,7 @@ from copy import copy, deepcopy from http import HTTPStatus from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Camera, CloudAccount, ModelType, Version +from uiprotect.data import Camera, CloudAccount, ModelType, Version from homeassistant.components.repairs.issue_handler import ( async_process_repairs_platforms, diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index 4ac82f45173..8795af57214 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -5,7 +5,7 @@ from __future__ import annotations from copy import copy from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import ( +from uiprotect.data import ( Camera, DoorbellMessageType, IRLEDMode, @@ -17,7 +17,7 @@ from pyunifiprotect.data import ( RecordingMode, Viewer, ) -from pyunifiprotect.data.nvr import DoorbellMessage +from uiprotect.data.nvr import DoorbellMessage from homeassistant.components.select import ATTR_OPTIONS from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index e593f224378..d8014079bf1 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -5,15 +5,8 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from pyunifiprotect.data import ( - NVR, - Camera, - Event, - EventType, - Sensor, - SmartDetectObjectType, -) -from pyunifiprotect.data.nvr import EventMetadata, LicensePlateMetadata +from uiprotect.data import NVR, Camera, Event, EventType, Sensor, SmartDetectObjectType +from uiprotect.data.nvr import EventMetadata, LicensePlateMetadata from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.sensor import ( diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 98decab9e4a..919af53ef10 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -5,9 +5,9 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Chime, Color, Light, ModelType -from pyunifiprotect.data.devices import CameraZone -from pyunifiprotect.exceptions import BadRequest +from uiprotect.data import Camera, Chime, Color, Light, ModelType +from uiprotect.data.devices import CameraZone +from uiprotect.exceptions import BadRequest from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN from homeassistant.components.unifiprotect.services import ( diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index e421937632c..16e471c2e7a 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Light, Permission, RecordingMode, VideoMode +from uiprotect.data import Camera, Light, Permission, RecordingMode, VideoMode from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.switch import ( diff --git a/tests/components/unifiprotect/test_text.py b/tests/components/unifiprotect/test_text.py index be2ae93203a..3ca11744abb 100644 --- a/tests/components/unifiprotect/test_text.py +++ b/tests/components/unifiprotect/test_text.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Camera, DoorbellMessageType, LCDMessage +from uiprotect.data import Camera, DoorbellMessageType, LCDMessage from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.text import CAMERA diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index f7930e5ff9a..6d190eb4dd6 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -6,8 +6,8 @@ from unittest.mock import AsyncMock, Mock from aiohttp import ClientResponse import pytest -from pyunifiprotect.data import Camera, Event, EventType -from pyunifiprotect.exceptions import ClientError +from uiprotect.data import Camera, Event, EventType +from uiprotect.exceptions import ClientError from homeassistant.components.unifiprotect.views import ( async_generate_event_video_url, diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 1ade39dafca..ab3aefaa09d 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -8,8 +8,8 @@ from datetime import timedelta from typing import Any from unittest.mock import Mock -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import ( +from uiprotect import ProtectApiClient +from uiprotect.data import ( Bootstrap, Camera, Event, @@ -18,8 +18,8 @@ from pyunifiprotect.data import ( ProtectAdoptableDeviceModel, WSSubscriptionMessage, ) -from pyunifiprotect.data.bootstrap import ProtectDeviceRef -from pyunifiprotect.test_util.anonymize import random_hex +from uiprotect.data.bootstrap import ProtectDeviceRef +from uiprotect.test_util.anonymize import random_hex from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index e9f0e8023bc..e973a9f9c28 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -1,6 +1,6 @@ """Tests the Home Assistant workday binary sensor.""" -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from typing import Any from freezegun.api import FrozenDateTimeFactory @@ -10,6 +10,7 @@ from homeassistant.components.workday.binary_sensor import SERVICE_CHECK_DATE from homeassistant.components.workday.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.dt import UTC from . import ( @@ -68,7 +69,9 @@ async def test_setup( freezer: FrozenDateTimeFactory, ) -> None: """Test setup from various configs.""" - freezer.move_to(datetime(2022, 4, 15, 12, tzinfo=UTC)) # Friday + # Start on a Friday + await hass.config.async_set_time_zone("Europe/Paris") + freezer.move_to(datetime(2022, 4, 15, 0, tzinfo=timezone(timedelta(hours=1)))) await init_integration(hass, config) state = hass.states.get("binary_sensor.workday_sensor") @@ -142,18 +145,59 @@ async def test_setup_add_holiday( assert state.state == "off" +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_setup_no_country_weekend( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test setup shows weekend as non-workday with no country.""" - freezer.move_to(datetime(2020, 2, 23, 12, tzinfo=UTC)) # Sunday + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2020, 2, 22, 0, 1, 1, tzinfo=zone)) # Saturday await init_integration(hass, TEST_CONFIG_NO_COUNTRY) state = hass.states.get("binary_sensor.workday_sensor") assert state is not None assert state.state == "off" + freezer.move_to(datetime(2020, 2, 24, 23, 59, 59, tzinfo=zone)) # Monday + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state is not None + assert state.state == "on" + + +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) +async def test_setup_no_country_weekday( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + time_zone: str, +) -> None: + """Test setup shows a weekday as a workday with no country.""" + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2020, 2, 21, 23, 59, 59, tzinfo=zone)) # Friday + await init_integration(hass, TEST_CONFIG_NO_COUNTRY) + + state = hass.states.get("binary_sensor.workday_sensor") + assert state is not None + assert state.state == "on" + + freezer.move_to(datetime(2020, 2, 22, 23, 59, 59, tzinfo=zone)) # Saturday + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state is not None + assert state.state == "off" + async def test_setup_remove_holiday( hass: HomeAssistant, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 3f61ed8a0ed..6ac17a2fe0e 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -249,6 +249,39 @@ async def test_assist_api_get_timer_tools( assert "HassStartTimer" in [tool.name for tool in api.tools] +async def test_assist_api_tools( + hass: HomeAssistant, llm_context: llm.LLMContext +) -> None: + """Test getting timer tools with Assist API.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "intent", {}) + + llm_context.device_id = "test_device" + + async_register_timer_handler(hass, "test_device", lambda *args: None) + + class MyIntentHandler(intent.IntentHandler): + intent_type = "Super crazy intent with unique nåme" + description = "my intent handler" + + intent.async_register(hass, MyIntentHandler()) + + api = await llm.async_get_api(hass, "assist", llm_context) + assert [tool.name for tool in api.tools] == [ + "HassTurnOn", + "HassTurnOff", + "HassSetPosition", + "HassStartTimer", + "HassCancelTimer", + "HassIncreaseTimer", + "HassDecreaseTimer", + "HassPauseTimer", + "HassUnpauseTimer", + "HassTimerStatus", + "Super_crazy_intent_with_unique_name", + ] + + async def test_assist_api_description( hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: