diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 624121b8828..ea27b58d34c 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -249,10 +249,11 @@ class AugustData(AugustSubscriberMixin): device = self.get_device_detail(device_id) activities = activities_from_pubnub_message(device, date_time, message) activity_stream = self.activity_stream - if activities: - activity_stream.async_process_newer_device_activities(activities) + if activities and activity_stream.async_process_newer_device_activities( + activities + ): self.async_signal_device_id_update(device.device_id) - activity_stream.async_schedule_house_id_refresh(device.house_id) + activity_stream.async_schedule_house_id_refresh(device.house_id) @callback def async_stop(self) -> None: diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 97963b19378..a1a7adb4ede 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.10.0", "yalexs-ble==2.4.1"] + "requirements": ["yalexs==1.11.2", "yalexs-ble==2.4.1"] } diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index a678868ee18..a952f016671 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -8,6 +8,7 @@ from aioelectricitymaps import ( ElectricityMaps, ElectricityMapsError, ElectricityMapsInvalidTokenError, + ElectricityMapsNoDataError, ) import voluptuous as vol @@ -151,6 +152,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await fetch_latest_carbon_intensity(self.hass, em, data) except ElectricityMapsInvalidTokenError: errors["base"] = "invalid_auth" + except ElectricityMapsNoDataError: + errors["base"] = "no_data" except ElectricityMapsError: errors["base"] = "unknown" else: diff --git a/homeassistant/components/co2signal/strings.json b/homeassistant/components/co2signal/strings.json index 89289dd816d..7444cde73d7 100644 --- a/homeassistant/components/co2signal/strings.json +++ b/homeassistant/components/co2signal/strings.json @@ -28,12 +28,9 @@ "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "api_ratelimit": "API Ratelimit exceeded" + "no_data": "No data is available for the location you have selected." }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "unknown": "[%key:common::config_flow::error::unknown%]", - "api_ratelimit": "[%key:component::co2signal::config::error::api_ratelimit%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index ab5d035dd54..128822cf289 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index d4a74725467..aaa6e1ee7de 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.38.1"], + "requirements": ["async-upnp-client==0.38.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 3fcb2b3211e..61bd425b139 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.8", "deebot-client==5.1.1"] + "requirements": ["py-sucks==0.9.9", "deebot-client==5.2.1"] } diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index b633e1ae620..113fe2ac84e 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -10,7 +10,7 @@ from types import MappingProxyType from typing import Any from elkm1_lib.elements import Element -from elkm1_lib.elk import Elk +from elkm1_lib.elk import Elk, Panel from elkm1_lib.util import parse_url import voluptuous as vol @@ -398,22 +398,30 @@ async def async_wait_for_elk_to_sync( return success +@callback +def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel: + """Get the ElkM1 panel from a service call.""" + prefix = service.data["prefix"] + elk = _find_elk_by_prefix(hass, prefix) + if elk is None: + raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found") + return elk.panel + + def _create_elk_services(hass: HomeAssistant) -> None: - def _getelk(service: ServiceCall) -> Elk: - prefix = service.data["prefix"] - elk = _find_elk_by_prefix(hass, prefix) - if elk is None: - raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found") - return elk + """Create ElkM1 services.""" + @callback def _speak_word_service(service: ServiceCall) -> None: - _getelk(service).panel.speak_word(service.data["number"]) + _async_get_elk_panel(hass, service).speak_word(service.data["number"]) + @callback def _speak_phrase_service(service: ServiceCall) -> None: - _getelk(service).panel.speak_phrase(service.data["number"]) + _async_get_elk_panel(hass, service).speak_phrase(service.data["number"]) + @callback def _set_time_service(service: ServiceCall) -> None: - _getelk(service).panel.set_time(dt_util.now()) + _async_get_elk_panel(hass, service).set_time(dt_util.now()) hass.services.async_register( DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 0c9bb44d06a..6b893dc8f48 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/evohome", "iot_class": "cloud_polling", "loggers": ["evohomeasync", "evohomeasync2"], - "requirements": ["evohome-async==0.4.18"] + "requirements": ["evohome-async==0.4.19"] } diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 59b5d65710a..7441def7d4d 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN -from .router import get_api +from .router import get_api, get_hosts_list_if_supported _LOGGER = logging.getLogger(__name__) @@ -69,7 +69,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Check permissions await fbx.system.get_config() - await fbx.lan.get_hosts_list() + await get_hosts_list_if_supported(fbx) # Close connection await fbx.close() diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 15e3b34bd77..3b13fad0572 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -64,6 +64,33 @@ async def get_api(hass: HomeAssistant, host: str) -> Freepybox: return Freepybox(APP_DESC, token_file, API_VERSION) +async def get_hosts_list_if_supported( + fbx_api: Freepybox, +) -> tuple[bool, list[dict[str, Any]]]: + """Hosts list is not supported when freebox is configured in bridge mode.""" + supports_hosts: bool = True + fbx_devices: list[dict[str, Any]] = [] + try: + fbx_devices = await fbx_api.lan.get_hosts_list() or [] + except HttpRequestError as err: + if ( + (matcher := re.search(r"Request failed \(APIResponse: (.+)\)", str(err))) + and is_json(json_str := matcher.group(1)) + and (json_resp := json.loads(json_str)).get("error_code") == "nodev" + ): + # No need to retry, Host list not available + supports_hosts = False + _LOGGER.debug( + "Host list is not available using bridge mode (%s)", + json_resp.get("msg"), + ) + + else: + raise err + + return supports_hosts, fbx_devices + + class FreeboxRouter: """Representation of a Freebox router.""" @@ -111,27 +138,9 @@ class FreeboxRouter: # Access to Host list not available in bridge mode, API return error_code 'nodev' if self.supports_hosts: - try: - fbx_devices = await self._api.lan.get_hosts_list() - except HttpRequestError as err: - if ( - ( - matcher := re.search( - r"Request failed \(APIResponse: (.+)\)", str(err) - ) - ) - and is_json(json_str := matcher.group(1)) - and (json_resp := json.loads(json_str)).get("error_code") == "nodev" - ): - # No need to retry, Host list not available - self.supports_hosts = False - _LOGGER.debug( - "Host list is not available using bridge mode (%s)", - json_resp.get("msg"), - ) - - else: - raise err + self.supports_hosts, fbx_devices = await get_hosts_list_if_supported( + self._api + ) # Adds the Freebox itself fbx_devices.append( diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 47695a275fc..8e1a0a24207 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -476,7 +476,7 @@ class SensorGroup(GroupEntity, SensorEntity): translation_placeholders={ "entity_id": self.entity_id, "source_entities": ", ".join(self._entity_ids), - "state_classes:": ", ".join(state_classes), + "state_classes": ", ".join(state_classes), }, ) return None @@ -519,7 +519,7 @@ class SensorGroup(GroupEntity, SensorEntity): translation_placeholders={ "entity_id": self.entity_id, "source_entities": ", ".join(self._entity_ids), - "device_classes:": ", ".join(device_classes), + "device_classes": ", ".join(device_classes), }, ) return None diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 25ae20da995..ba571bb1008 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -265,7 +265,7 @@ }, "state_classes_not_matching": { "title": "State classes is not correct", - "description": "Device classes `{state_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the state classes on the source entities and reload the group sensor to fix this issue." + "description": "State classes `{state_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the state classes on the source entities and reload the group sensor to fix this issue." } } } diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 397af9ac181..6a304f7de5f 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,7 +12,7 @@ "quality_scale": "platinum", "requirements": [ "xknx==2.12.0", - "xknxproject==3.5.0", + "xknxproject==3.6.0", "knx-frontend==2024.1.20.105944" ] } diff --git a/homeassistant/components/linear_garage_door/coordinator.py b/homeassistant/components/linear_garage_door/coordinator.py index 5a17d5a39e4..e9234327429 100644 --- a/homeassistant/components/linear_garage_door/coordinator.py +++ b/homeassistant/components/linear_garage_door/coordinator.py @@ -6,11 +6,12 @@ import logging from typing import Any from linear_garage_door import Linear -from linear_garage_door.errors import InvalidLoginError, ResponseError +from linear_garage_door.errors import InvalidLoginError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -55,6 +56,7 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): email=self._email, password=self._password, device_id=self._device_id, + client_session=async_get_clientsession(self.hass), ) except InvalidLoginError as err: if ( @@ -63,8 +65,6 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): ): raise ConfigEntryAuthFailed from err raise ConfigEntryNotReady from err - except ResponseError as err: - raise ConfigEntryNotReady from err if not self._devices: self._devices = await linear.get_devices(self._site_id) diff --git a/homeassistant/components/linear_garage_door/manifest.json b/homeassistant/components/linear_garage_door/manifest.json index c7918e21e20..f1eb4302cf0 100644 --- a/homeassistant/components/linear_garage_door/manifest.json +++ b/homeassistant/components/linear_garage_door/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/linear_garage_door", "iot_class": "cloud_polling", - "requirements": ["linear-garage-door==0.2.7"] + "requirements": ["linear-garage-door==0.2.9"] } diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index 6444aa306a2..73f1028bb72 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/lutron", "iot_class": "local_polling", "loggers": ["pylutron"], - "requirements": ["pylutron==0.2.8"] + "requirements": ["pylutron==0.2.12"] } diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index e00215f6073..a658de9a024 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -4,9 +4,10 @@ from __future__ import annotations import asyncio import logging import re -import sys from typing import Any +import datapoint + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, @@ -16,7 +17,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator @@ -34,9 +35,6 @@ from .const import ( from .data import MetOfficeData from .helpers import fetch_data, fetch_site -if sys.version_info < (3, 12): - import datapoint - _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR, Platform.WEATHER] @@ -44,10 +42,6 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Met Office entry.""" - if sys.version_info >= (3, 12): - raise HomeAssistantError( - "Met Office is not supported on Python 3.12. Please use Python 3.11." - ) latitude = entry.data[CONF_LATITUDE] longitude = entry.data[CONF_LONGITUDE] diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py index 8512dd4c7a6..c6bb2b4c01b 100644 --- a/homeassistant/components/metoffice/data.py +++ b/homeassistant/components/metoffice/data.py @@ -2,12 +2,10 @@ from __future__ import annotations from dataclasses import dataclass -import sys -if sys.version_info < (3, 12): - from datapoint.Forecast import Forecast - from datapoint.Site import Site - from datapoint.Timestep import Timestep +from datapoint.Forecast import Forecast +from datapoint.Site import Site +from datapoint.Timestep import Timestep @dataclass diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 389462d573a..5b698bf19da 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -2,7 +2,9 @@ from __future__ import annotations import logging -import sys + +import datapoint +from datapoint.Site import Site from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.util.dt import utcnow @@ -10,11 +12,6 @@ from homeassistant.util.dt import utcnow from .const import MODE_3HOURLY from .data import MetOfficeData -if sys.version_info < (3, 12): - import datapoint - from datapoint.Site import Site - - _LOGGER = logging.getLogger(__name__) @@ -34,7 +31,7 @@ def fetch_site( def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOfficeData: """Fetch weather and forecast from Datapoint API.""" try: - forecast = connection.get_forecast_for_site(site.id, mode) + forecast = connection.get_forecast_for_site(site.location_id, mode) except (ValueError, datapoint.exceptions.APIException) as err: _LOGGER.error("Check Met Office connection: %s", err.args) raise UpdateFailed from err diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index 401f2c9d265..17643d7e061 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -3,9 +3,8 @@ "name": "Met Office", "codeowners": ["@MrHarcombe", "@avee87"], "config_flow": true, - "disabled": "Integration library not compatible with Python 3.12", "documentation": "https://www.home-assistant.io/integrations/metoffice", "iot_class": "cloud_polling", "loggers": ["datapoint"], - "requirements": ["datapoint==0.9.8;python_version<'3.12'"] + "requirements": ["datapoint==0.9.9"] } diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 371c396a829..84a51a0d584 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -251,6 +251,6 @@ class MetOfficeCurrentSensor( return { ATTR_LAST_UPDATE: self.coordinator.data.now.date, ATTR_SENSOR_ID: self.entity_description.key, - ATTR_SITE_ID: self.coordinator.data.site.id, + ATTR_SITE_ID: self.coordinator.data.site.location_id, ATTR_SITE_NAME: self.coordinator.data.site.name, } diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index cdc1e7a6986..ac11bab303d 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -199,6 +199,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): self._precision = config.get(CONF_PRECISION, 2) else: self._precision = config.get(CONF_PRECISION, 0) + if self._precision > 0 or self._scale != int(self._scale): + self._value_is_int = False def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: """Do swap as needed.""" diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index c987e1bb10a..9dde08af5f0 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -400,6 +400,7 @@ class MotionTDBUDevice(MotionPositionDevice): def __init__(self, coordinator, blind, device_class, motor): """Initialize the blind.""" super().__init__(coordinator, blind, device_class) + delattr(self, "_attr_name") self._motor = motor self._motor_key = motor[0] self._attr_translation_key = motor.lower() diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 9faa2f361b9..491ee0efe59 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -212,7 +212,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host = entry.data[CONF_HOST] password = entry.data[CONF_PASSWORD] - if DOMAIN not in hass.data: + if not (data := hass.data.get(DOMAIN)) or data.websession.closed: websession = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) hass.data[DOMAIN] = LTEData(websession) @@ -258,7 +258,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.state == ConfigEntryState.LOADED ] if len(loaded_entries) == 1: - hass.data.pop(DOMAIN) + hass.data.pop(DOMAIN, None) return unload_ok diff --git a/homeassistant/components/pegel_online/manifest.json b/homeassistant/components/pegel_online/manifest.json index 9546017d4ff..f38d320b454 100644 --- a/homeassistant/components/pegel_online/manifest.json +++ b/homeassistant/components/pegel_online/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aiopegelonline"], - "requirements": ["aiopegelonline==0.0.6"] + "requirements": ["aiopegelonline==0.0.8"] } diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index e17ae1190a4..86163704797 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -21,7 +21,10 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, HVACAction, ) -from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, +) from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY from homeassistant.components.light import ATTR_BRIGHTNESS @@ -437,7 +440,7 @@ class PrometheusMetrics: float(cover_state == state.state) ) - position = state.attributes.get(ATTR_POSITION) + position = state.attributes.get(ATTR_CURRENT_POSITION) if position is not None: position_metric = self._metric( "cover_position", @@ -446,7 +449,7 @@ class PrometheusMetrics: ) position_metric.labels(**self._labels(state)).set(float(position)) - tilt_position = state.attributes.get(ATTR_TILT_POSITION) + tilt_position = state.attributes.get(ATTR_CURRENT_TILT_POSITION) if tilt_position is not None: tilt_position_metric = self._metric( "cover_tilt_position", diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 0b4dfa29e78..a5c896f3740 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -115,6 +115,7 @@ async def setup_device( device.name, ) _LOGGER.debug(err) + await mqtt_client.async_release() raise err coordinator = RoborockDataUpdateCoordinator( hass, device, networking, product_info, mqtt_client @@ -125,6 +126,7 @@ async def setup_device( try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: + await coordinator.release() if isinstance(coordinator.api, RoborockMqttClient): _LOGGER.warning( "Not setting up %s because the we failed to get data for the first time using the online client. " @@ -153,14 +155,10 @@ async def setup_device( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle removal of an entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - await asyncio.gather( - *( - coordinator.release() - for coordinator in hass.data[DOMAIN][entry.entry_id].values() - ) - ) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + release_tasks = set() + for coordinator in hass.data[DOMAIN][entry.entry_id].values(): + release_tasks.add(coordinator.release()) hass.data[DOMAIN].pop(entry.entry_id) - + await asyncio.gather(*release_tasks) return unload_ok diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index cd08cf871d4..d0ed508df4c 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -77,7 +77,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): async def release(self) -> None: """Disconnect from API.""" - await self.api.async_disconnect() + await self.api.async_release() + await self.cloud_api.async_release() async def _update_device_prop(self) -> None: """Update device properties.""" diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 17531f6c627..2921a372e00 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -1,5 +1,4 @@ """Support for Roborock device base class.""" - from typing import Any from roborock.api import AttributeCache, RoborockClient @@ -7,6 +6,7 @@ from roborock.cloud_api import RoborockMqttClient from roborock.command_cache import CacheableAttribute from roborock.containers import Consumable, Status from roborock.exceptions import RoborockException +from roborock.roborock_message import RoborockDataProtocol from roborock.roborock_typing import RoborockCommand from homeassistant.exceptions import HomeAssistantError @@ -24,7 +24,10 @@ class RoborockEntity(Entity): _attr_has_entity_name = True def __init__( - self, unique_id: str, device_info: DeviceInfo, api: RoborockClient + self, + unique_id: str, + device_info: DeviceInfo, + api: RoborockClient, ) -> None: """Initialize the coordinated Roborock Device.""" self._attr_unique_id = unique_id @@ -75,6 +78,9 @@ class RoborockCoordinatedEntity( self, unique_id: str, coordinator: RoborockDataUpdateCoordinator, + listener_request: list[RoborockDataProtocol] + | RoborockDataProtocol + | None = None, ) -> None: """Initialize the coordinated Roborock Device.""" RoborockEntity.__init__( @@ -85,6 +91,23 @@ class RoborockCoordinatedEntity( ) CoordinatorEntity.__init__(self, coordinator=coordinator) self._attr_unique_id = unique_id + if isinstance(listener_request, RoborockDataProtocol): + listener_request = [listener_request] + self.listener_requests = listener_request or [] + + async def async_added_to_hass(self) -> None: + """Add listeners when the device is added to hass.""" + await super().async_added_to_hass() + for listener_request in self.listener_requests: + self.api.add_listener( + listener_request, self._update_from_listener, cache=self.api.cache + ) + + async def async_will_remove_from_hass(self) -> None: + """Remove listeners when the device is removed from hass.""" + for listener_request in self.listener_requests: + self.api.remove_listener(listener_request, self._update_from_listener) + await super().async_will_remove_from_hass() @property def _device_status(self) -> Status: @@ -107,7 +130,7 @@ class RoborockCoordinatedEntity( await self.coordinator.async_refresh() return res - def _update_from_listener(self, value: Status | Consumable): + def _update_from_listener(self, value: Status | Consumable) -> None: """Update the status or consumable data from a listener and then write the new entity state.""" if isinstance(value, Status): self.coordinator.roborock_device_info.props.status = value diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index ae5dd12689d..3fdd10c97d5 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -107,10 +107,8 @@ class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity): ) -> None: """Create a select entity.""" self.entity_description = entity_description - super().__init__(unique_id, coordinator) + super().__init__(unique_id, coordinator, entity_description.protocol_listener) self._attr_options = options - if (protocol := self.entity_description.protocol_listener) is not None: - self.api.add_listener(protocol, self._update_from_listener, self.api.cache) async def async_select_option(self, option: str) -> None: """Set the option.""" diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index d5258879acb..8d723ec57cd 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -232,10 +232,8 @@ class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity): description: RoborockSensorDescription, ) -> None: """Initialize the entity.""" - super().__init__(unique_id, coordinator) self.entity_description = description - if (protocol := self.entity_description.protocol_listener) is not None: - self.api.add_listener(protocol, self._update_from_listener, self.api.cache) + super().__init__(unique_id, coordinator, description.protocol_listener) @property def native_value(self) -> StateType | datetime.datetime: diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 3b8f0e756b7..dafbb731bd2 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -92,14 +92,16 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): ) -> None: """Initialize a vacuum.""" StateVacuumEntity.__init__(self) - RoborockCoordinatedEntity.__init__(self, unique_id, coordinator) + RoborockCoordinatedEntity.__init__( + self, + unique_id, + coordinator, + listener_request=[ + RoborockDataProtocol.FAN_POWER, + RoborockDataProtocol.STATE, + ], + ) self._attr_fan_speed_list = self._device_status.fan_power_options - self.api.add_listener( - RoborockDataProtocol.FAN_POWER, self._update_from_listener, self.api.cache - ) - self.api.add_listener( - RoborockDataProtocol.STATE, self._update_from_listener, self.api.cache - ) @property def state(self) -> str | None: diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 780d47e4743..00b8fec8e6a 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.38.1" + "async-upnp-client==0.38.2" ], "ssdp": [ { diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index bcc851e02ae..0ad2a0a714f 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -173,9 +173,9 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_ENABLE_CLIMATE_REACT, { - vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): float, + vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): vol.Coerce(float), vol.Required(ATTR_HIGH_TEMPERATURE_STATE): dict, - vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): float, + vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): vol.Coerce(float), vol.Required(ATTR_LOW_TEMPERATURE_STATE): dict, vol.Required(ATTR_SMART_TYPE): vol.In( ["temperature", "feelsLike", "humidity"] diff --git a/homeassistant/components/sensor/icons.json b/homeassistant/components/sensor/icons.json index 24245d9bf03..f23826cfe95 100644 --- a/homeassistant/components/sensor/icons.json +++ b/homeassistant/components/sensor/icons.json @@ -117,7 +117,7 @@ "speed": { "default": "mdi:speedometer" }, - "sulfur_dioxide": { + "sulphur_dioxide": { "default": "mdi:molecule" }, "temperature": { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 8afed8b4fd1..2737565822d 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.38.1"] + "requirements": ["async-upnp-client==0.38.2"] } diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 1a43601940e..4f02ee1a1f6 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -139,7 +139,7 @@ class StarlineSensor(StarlineEntity, SensorEntity): if self._key == "mileage" and self._device.mileage: return self._device.mileage.get("val") if self._key == "gps_count" and self._device.position: - return self._device.position["sat_qty"] + return self._device.position.get("sat_qty") return None @property diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 2f92726a6da..401d85e7376 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.44.0"] + "requirements": ["PySwitchbot==0.45.0"] } diff --git a/homeassistant/components/systemmonitor/coordinator.py b/homeassistant/components/systemmonitor/coordinator.py index 9143d31f163..bf625eacf9a 100644 --- a/homeassistant/components/systemmonitor/coordinator.py +++ b/homeassistant/components/systemmonitor/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinators for the System monitor integration.""" + from __future__ import annotations from abc import abstractmethod @@ -43,7 +44,8 @@ dataT = TypeVar( | sswap | VirtualMemory | tuple[float, float, float] - | sdiskusage, + | sdiskusage + | None, ) @@ -130,12 +132,15 @@ class SystemMonitorLoadCoordinator(MonitorCoordinator[tuple[float, float, float] return os.getloadavg() -class SystemMonitorProcessorCoordinator(MonitorCoordinator[float]): +class SystemMonitorProcessorCoordinator(MonitorCoordinator[float | None]): """A System monitor Processor Data Update Coordinator.""" - def update_data(self) -> float: + def update_data(self) -> float | None: """Fetch data.""" - return psutil.cpu_percent(interval=None) + cpu_percent = psutil.cpu_percent(interval=None) + if cpu_percent > 0.0: + return cpu_percent + return None class SystemMonitorBootTimeCoordinator(MonitorCoordinator[datetime]): diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index e751ffebb12..813104e2de3 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -344,7 +344,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription[Any]] = { native_unit_of_measurement=PERCENTAGE, icon=get_cpu_icon(), state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data), + value_fn=lambda entity: ( + round(entity.coordinator.data) if entity.coordinator.data else None + ), ), "processor_temperature": SysMonitorSensorEntityDescription[ dict[str, list[shwtemp]] diff --git a/homeassistant/components/technove/manifest.json b/homeassistant/components/technove/manifest.json index 33739bbd867..c63151560f8 100644 --- a/homeassistant/components/technove/manifest.json +++ b/homeassistant/components/technove/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/technove", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["python-technove==1.2.1"], + "requirements": ["python-technove==1.2.2"], "zeroconf": ["_technove-stations._tcp.local."] } diff --git a/homeassistant/components/technove/strings.json b/homeassistant/components/technove/strings.json index 8a850ee610c..f38bf61d8ed 100644 --- a/homeassistant/components/technove/strings.json +++ b/homeassistant/components/technove/strings.json @@ -63,7 +63,9 @@ "state": { "unplugged": "Unplugged", "plugged_waiting": "Plugged, waiting", - "plugged_charging": "Plugged, charging" + "plugged_charging": "Plugged, charging", + "out_of_activation_period": "Out of activation period", + "high_charge_period": "High charge period" } } } diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index e1e51f19e3a..10c0c16ff7f 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -61,7 +61,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" return await self._async_handle_discovery( - discovery_info.ip, discovery_info.macaddress + discovery_info.ip, dr.format_mac(discovery_info.macaddress) ) async def async_step_integration_discovery( diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index f69dffc2d57..f3092811227 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==70"], + "requirements": ["aiounifi==71"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 8ce32158016..edfde84a2ac 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index 6dbe43cb4e3..ffb9412703f 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -6,5 +6,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aio_geojson_usgs_earthquakes"], - "requirements": ["aio-geojson-usgs-earthquakes==0.2"] + "requirements": ["aio-geojson-usgs-earthquakes==0.3"] } diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index f2a11aaf1fe..20f8ed3ed4d 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.1"], + "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.2"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 01ec041e9d8..86593c36737 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -135,7 +135,7 @@ def async_active_zone( is None # Skip zone that are outside the radius aka the # lat/long is outside the zone - or not (zone_dist - (radius := zone_attrs[ATTR_RADIUS]) < radius) + or not (zone_dist - (zone_radius := zone_attrs[ATTR_RADIUS]) < radius) ): continue @@ -144,7 +144,7 @@ def async_active_zone( zone_dist < min_dist or ( # If same distance, prefer smaller zone - zone_dist == min_dist and radius < closest.attributes[ATTR_RADIUS] + zone_dist == min_dist and zone_radius < closest.attributes[ATTR_RADIUS] ) ): continue diff --git a/homeassistant/const.py b/homeassistant/const.py index a19ff18d8f3..84730bbf6d2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -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, 11, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6a9734ad1fa..380eb593a55 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiohttp-zlib-ng==0.3.1 aiohttp==3.9.3 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.38.1 +async-upnp-client==0.38.2 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.2.0 @@ -36,7 +36,7 @@ janus==1.0.0 Jinja2==3.1.3 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.9.13 +orjson==3.9.14 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.2.0 diff --git a/machine/raspberrypi b/machine/raspberrypi index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi +++ b/machine/raspberrypi @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi2 b/machine/raspberrypi2 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi2 +++ b/machine/raspberrypi2 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi3 b/machine/raspberrypi3 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi3 +++ b/machine/raspberrypi3 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi3-64 b/machine/raspberrypi3-64 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi3-64 +++ b/machine/raspberrypi3-64 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi4 b/machine/raspberrypi4 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi4 +++ b/machine/raspberrypi4 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi4-64 b/machine/raspberrypi4-64 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi4-64 +++ b/machine/raspberrypi4-64 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi5-64 b/machine/raspberrypi5-64 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi5-64 +++ b/machine/raspberrypi5-64 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/yellow b/machine/yellow index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/yellow +++ b/machine/yellow @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/pyproject.toml b/pyproject.toml index 89551988971..0f9f9187e9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.1" +version = "2024.2.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -46,7 +46,7 @@ dependencies = [ "cryptography==42.0.2", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==24.0.0", - "orjson==3.9.13", + "orjson==3.9.14", "packaging>=23.1", "pip>=21.3.1", "python-slugify==8.0.1", diff --git a/requirements.txt b/requirements.txt index 63ea582eba8..76ca93d998d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ lru-dict==1.3.0 PyJWT==2.8.0 cryptography==42.0.2 pyOpenSSL==24.0.0 -orjson==3.9.13 +orjson==3.9.14 packaging>=23.1 pip>=21.3.1 python-slugify==8.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 0c084d0b6b9..70c74a10dd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -96,7 +96,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.44.0 +PySwitchbot==0.45.0 # homeassistant.components.switchmate PySwitchmate==0.5.1 @@ -179,7 +179,7 @@ aio-geojson-geonetnz-volcano==0.9 aio-geojson-nsw-rfs-incidents==0.7 # homeassistant.components.usgs_earthquakes_feed -aio-geojson-usgs-earthquakes==0.2 +aio-geojson-usgs-earthquakes==0.3 # homeassistant.components.gdacs aio-georss-gdacs==0.9 @@ -318,7 +318,7 @@ aiooncue==0.3.5 aioopenexchangerates==0.4.0 # homeassistant.components.pegel_online -aiopegelonline==0.0.6 +aiopegelonline==0.0.8 # homeassistant.components.acmeda aiopulse==0.4.4 @@ -383,7 +383,7 @@ aiotankerkoenig==0.3.0 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==70 +aiounifi==71 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -478,7 +478,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.1 +async-upnp-client==0.38.2 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 @@ -671,6 +671,9 @@ crownstone-uart==2.1.0 # homeassistant.components.datadog datadog==0.15.0 +# homeassistant.components.metoffice +datapoint==0.9.9 + # homeassistant.components.bluetooth dbus-fast==2.21.1 @@ -684,7 +687,7 @@ debugpy==1.8.0 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==5.1.1 +deebot-client==5.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -818,7 +821,7 @@ eufylife-ble-client==0.1.8 # evdev==1.6.1 # homeassistant.components.evohome -evohome-async==0.4.18 +evohome-async==0.4.19 # homeassistant.components.faa_delays faadelays==2023.9.1 @@ -1220,7 +1223,7 @@ lightwave==0.24 limitlessled==1.1.3 # homeassistant.components.linear_garage_door -linear-garage-door==0.2.7 +linear-garage-door==0.2.9 # homeassistant.components.linode linode-api==4.1.9b1 @@ -1609,7 +1612,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.ecovacs -py-sucks==0.9.8 +py-sucks==0.9.9 # homeassistant.components.synology_dsm py-synologydsm-api==2.1.4 @@ -1928,7 +1931,7 @@ pylitterbot==2023.4.9 pylutron-caseta==0.19.0 # homeassistant.components.lutron -pylutron==0.2.8 +pylutron==0.2.12 # homeassistant.components.mailgun pymailgunner==1.4 @@ -2284,7 +2287,7 @@ python-songpal==0.16.1 python-tado==0.17.4 # homeassistant.components.technove -python-technove==1.2.1 +python-technove==1.2.2 # homeassistant.components.telegram_bot python-telegram-bot==13.1 @@ -2859,7 +2862,7 @@ xiaomi-ble==0.23.1 xknx==2.12.0 # homeassistant.components.knx -xknxproject==3.5.0 +xknxproject==3.6.0 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -2880,7 +2883,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.1 # homeassistant.components.august -yalexs==1.10.0 +yalexs==1.11.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de460cbd0c4..89ec09e301c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.44.0 +PySwitchbot==0.45.0 # homeassistant.components.syncthru PySyncThru==0.7.10 @@ -158,7 +158,7 @@ aio-geojson-geonetnz-volcano==0.9 aio-geojson-nsw-rfs-incidents==0.7 # homeassistant.components.usgs_earthquakes_feed -aio-geojson-usgs-earthquakes==0.2 +aio-geojson-usgs-earthquakes==0.3 # homeassistant.components.gdacs aio-georss-gdacs==0.9 @@ -291,7 +291,7 @@ aiooncue==0.3.5 aioopenexchangerates==0.4.0 # homeassistant.components.pegel_online -aiopegelonline==0.0.6 +aiopegelonline==0.0.8 # homeassistant.components.acmeda aiopulse==0.4.4 @@ -356,7 +356,7 @@ aiotankerkoenig==0.3.0 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==70 +aiounifi==71 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -430,7 +430,7 @@ arcam-fmj==1.4.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.1 +async-upnp-client==0.38.2 # homeassistant.components.sleepiq asyncsleepiq==1.5.2 @@ -552,6 +552,9 @@ crownstone-uart==2.1.0 # homeassistant.components.datadog datadog==0.15.0 +# homeassistant.components.metoffice +datapoint==0.9.9 + # homeassistant.components.bluetooth dbus-fast==2.21.1 @@ -559,7 +562,7 @@ dbus-fast==2.21.1 debugpy==1.8.0 # homeassistant.components.ecovacs -deebot-client==5.1.1 +deebot-client==5.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -971,7 +974,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.linear_garage_door -linear-garage-door==0.2.7 +linear-garage-door==0.2.9 # homeassistant.components.lamarzocco lmcloud==0.4.35 @@ -1259,7 +1262,7 @@ py-nextbusnext==1.0.2 py-nightscout==1.2.2 # homeassistant.components.ecovacs -py-sucks==0.9.8 +py-sucks==0.9.9 # homeassistant.components.synology_dsm py-synologydsm-api==2.1.4 @@ -1485,7 +1488,7 @@ pylitterbot==2023.4.9 pylutron-caseta==0.19.0 # homeassistant.components.lutron -pylutron==0.2.8 +pylutron==0.2.12 # homeassistant.components.mailgun pymailgunner==1.4 @@ -1751,7 +1754,7 @@ python-songpal==0.16.1 python-tado==0.17.4 # homeassistant.components.technove -python-technove==1.2.1 +python-technove==1.2.2 # homeassistant.components.telegram_bot python-telegram-bot==13.1 @@ -2188,7 +2191,7 @@ xiaomi-ble==0.23.1 xknx==2.12.0 # homeassistant.components.knx -xknxproject==3.5.0 +xknxproject==3.6.0 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -2206,7 +2209,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.1 # homeassistant.components.august -yalexs==1.10.0 +yalexs==1.11.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 29ce783f33a..518a747f852 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -5,6 +5,7 @@ from aioelectricitymaps import ( ElectricityMapsConnectionError, ElectricityMapsError, ElectricityMapsInvalidTokenError, + ElectricityMapsNoDataError, ) import pytest @@ -139,12 +140,9 @@ async def test_form_country(hass: HomeAssistant) -> None: ), (ElectricityMapsError("Something else"), "unknown"), (ElectricityMapsConnectionError("Boom"), "unknown"), + (ElectricityMapsNoDataError("I have no data"), "no_data"), ], - ids=[ - "invalid auth", - "generic error", - "json decode error", - ], + ids=["invalid auth", "generic error", "json decode error", "no data error"], ) async def test_form_error_handling( hass: HomeAssistant, diff --git a/tests/components/ecovacs/conftest.py b/tests/components/ecovacs/conftest.py index d0f0668cc8c..31d7246e6bc 100644 --- a/tests/components/ecovacs/conftest.py +++ b/tests/components/ecovacs/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch -from deebot_client.const import PATH_API_APPSVR_APP +from deebot_client import const from deebot_client.device import Device from deebot_client.exceptions import ApiError from deebot_client.models import Credentials @@ -75,9 +75,13 @@ def mock_authenticator(device_fixture: str) -> Generator[Mock, None, None]: query_params: dict[str, Any] | None = None, headers: dict[str, Any] | None = None, ) -> dict[str, Any]: - if path == PATH_API_APPSVR_APP: - return {"code": 0, "devices": devices, "errno": "0"} - raise ApiError("Path not mocked: {path}") + match path: + case const.PATH_API_APPSVR_APP: + return {"code": 0, "devices": devices, "errno": "0"} + case const.PATH_API_USERS_USER: + return {"todo": "result", "result": "ok", "devices": devices} + case _: + raise ApiError("Path not mocked: {path}") authenticator.post_authenticated.side_effect = post_authenticated yield authenticator diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index 3ba175cbc75..6042248561c 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -112,3 +112,14 @@ def mock_router_bridge_mode(mock_device_registry_devices, router): ) return router + + +@pytest.fixture +def mock_router_bridge_mode_error(mock_device_registry_devices, router): + """Mock a failed connection to Freebox Bridge mode.""" + + router().lan.get_hosts_list = AsyncMock( + side_effect=HttpRequestError("Request failed (APIResponse: some unknown error)") + ) + + return router diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 6a90bbd9ba8..c19b3c3f3b2 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -69,8 +69,8 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["step_id"] == "link" -async def test_link(hass: HomeAssistant, router: Mock) -> None: - """Test linking.""" +async def internal_test_link(hass: HomeAssistant) -> None: + """Test linking internal, common to both router modes.""" with patch( "homeassistant.components.freebox.async_setup_entry", return_value=True, @@ -91,6 +91,30 @@ async def test_link(hass: HomeAssistant, router: Mock) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_link(hass: HomeAssistant, router: Mock) -> None: + """Test link with standard router mode.""" + await internal_test_link(hass) + + +async def test_link_bridge_mode(hass: HomeAssistant, router_bridge_mode: Mock) -> None: + """Test linking for a freebox in bridge mode.""" + await internal_test_link(hass) + + +async def test_link_bridge_mode_error( + hass: HomeAssistant, mock_router_bridge_mode_error: Mock +) -> None: + """Test linking for a freebox in bridge mode, unknown error received from API.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + async def test_abort_if_already_setup(hass: HomeAssistant) -> None: """Test we abort if component is already setup.""" MockConfigEntry( diff --git a/tests/components/freebox/test_router.py b/tests/components/freebox/test_router.py index 572c168e665..88cf56de2bb 100644 --- a/tests/components/freebox/test_router.py +++ b/tests/components/freebox/test_router.py @@ -1,7 +1,11 @@ """Tests for the Freebox utility methods.""" import json +from unittest.mock import Mock -from homeassistant.components.freebox.router import is_json +from freebox_api.exceptions import HttpRequestError +import pytest + +from homeassistant.components.freebox.router import get_hosts_list_if_supported, is_json from .const import DATA_LAN_GET_HOSTS_LIST_MODE_BRIDGE, DATA_WIFI_GET_GLOBAL_CONFIG @@ -20,3 +24,33 @@ async def test_is_json() -> None: assert not is_json("") assert not is_json("XXX") assert not is_json("{XXX}") + + +async def test_get_hosts_list_if_supported( + router: Mock, +) -> None: + """In router mode, get_hosts_list is supported and list is filled.""" + supports_hosts, fbx_devices = await get_hosts_list_if_supported(router()) + assert supports_hosts is True + # List must not be empty; but it's content depends on how many unit tests are executed... + assert fbx_devices + assert "d633d0c8-958c-43cc-e807-d881b076924b" in str(fbx_devices) + + +async def test_get_hosts_list_if_supported_bridge( + router_bridge_mode: Mock, +) -> None: + """In bridge mode, get_hosts_list is NOT supported and list is empty.""" + supports_hosts, fbx_devices = await get_hosts_list_if_supported( + router_bridge_mode() + ) + assert supports_hosts is False + assert fbx_devices == [] + + +async def test_get_hosts_list_if_supported_bridge_error( + mock_router_bridge_mode_error: Mock, +) -> None: + """Other exceptions must be propagated.""" + with pytest.raises(HttpRequestError): + await get_hosts_list_if_supported(mock_router_bridge_mode_error()) diff --git a/tests/components/linear_garage_door/test_config_flow.py b/tests/components/linear_garage_door/test_config_flow.py index 64664745c54..5d1ed36ecb7 100644 --- a/tests/components/linear_garage_door/test_config_flow.py +++ b/tests/components/linear_garage_door/test_config_flow.py @@ -65,54 +65,58 @@ async def test_form(hass: HomeAssistant) -> None: async def test_reauth(hass: HomeAssistant) -> None: """Test reauthentication.""" - entry = await async_init_integration(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "title_placeholders": {"name": entry.title}, - "unique_id": entry.unique_id, - }, - data=entry.data, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - with patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.login", + "homeassistant.components.linear_garage_door.async_setup_entry", return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.get_sites", - return_value=[{"id": "test-site-id", "name": "test-site-name"}], - ), patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.close", - return_value=None, - ), patch( - "uuid.uuid4", - return_value="test-uuid", ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "new-email", - "password": "new-password", + entry = await async_init_integration(hass) + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, }, + data=entry.data, ) - await hass.async_block_till_done() + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "user" - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + with patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.login", + return_value=True, + ), patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.get_sites", + return_value=[{"id": "test-site-id", "name": "test-site-name"}], + ), patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.close", + return_value=None, + ), patch( + "uuid.uuid4", + return_value="test-uuid", + ): + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + { + "email": "new-email", + "password": "new-password", + }, + ) + await hass.async_block_till_done() - entries = hass.config_entries.async_entries() - assert len(entries) == 1 - assert entries[0].data == { - "email": "new-email", - "password": "new-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - } + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + entries = hass.config_entries.async_entries() + assert len(entries) == 1 + assert entries[0].data == { + "email": "new-email", + "password": "new-password", + "site_id": "test-site-id", + "device_id": "test-uuid", + } async def test_form_invalid_login(hass: HomeAssistant) -> None: diff --git a/tests/components/linear_garage_door/test_coordinator.py b/tests/components/linear_garage_door/test_coordinator.py index fc3087db354..1e46d294f3f 100644 --- a/tests/components/linear_garage_door/test_coordinator.py +++ b/tests/components/linear_garage_door/test_coordinator.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from linear_garage_door.errors import InvalidLoginError, ResponseError +from linear_garage_door.errors import InvalidLoginError from homeassistant.components.linear_garage_door.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -45,32 +45,6 @@ async def test_invalid_password( assert flows[0]["context"]["source"] == "reauth" -async def test_response_error(hass: HomeAssistant) -> None: - """Test response error.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - "email": "test-email", - "password": "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - }, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.login", - side_effect=ResponseError, - ): - assert not await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert entries - assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_RETRY - - async def test_invalid_login( hass: HomeAssistant, ) -> None: diff --git a/tests/components/metoffice/conftest.py b/tests/components/metoffice/conftest.py index 117bfe417e3..b1d1c9f508e 100644 --- a/tests/components/metoffice/conftest.py +++ b/tests/components/metoffice/conftest.py @@ -1,15 +1,9 @@ """Fixtures for Met Office weather integration tests.""" from unittest.mock import patch +from datapoint.exceptions import APIException import pytest -# All tests are marked as disabled, as the integration is disabled in the -# integration manifest. `datapoint` isn't compatible with Python 3.12 -# -# from datapoint.exceptions import APIException -APIException = Exception -collect_ignore_glob = ["test_*.py"] - @pytest.fixture def mock_simple_manager_fail(): diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index af2f2ba5784..7ee534f91ce 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1352,7 +1352,7 @@ async def cover_fixture( suggested_object_id="position_shade", original_name="Position Shade", ) - cover_position_attributes = {cover.ATTR_POSITION: 50} + cover_position_attributes = {cover.ATTR_CURRENT_POSITION: 50} set_state_with_entry(hass, cover_position, STATE_OPEN, cover_position_attributes) data["cover_position"] = cover_position @@ -1363,7 +1363,7 @@ async def cover_fixture( suggested_object_id="tilt_position_shade", original_name="Tilt Position Shade", ) - cover_tilt_position_attributes = {cover.ATTR_TILT_POSITION: 50} + cover_tilt_position_attributes = {cover.ATTR_CURRENT_TILT_POSITION: 50} set_state_with_entry( hass, cover_tilt_position, STATE_OPEN, cover_tilt_position_attributes ) diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 5d1afaf8f84..7546e80b003 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -18,7 +18,7 @@ async def test_unload_entry( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert setup_entry.state is ConfigEntryState.LOADED with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.async_disconnect" + "homeassistant.components.roborock.coordinator.RoborockLocalClient.async_release" ) as mock_disconnect: assert await hass.config_entries.async_unload(setup_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index 8beeddbefdc..b1b06a378f7 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -1,4 +1,5 @@ """Test System Monitor sensor.""" + from datetime import timedelta import socket from unittest.mock import Mock, patch @@ -429,3 +430,37 @@ async def test_exception_handling_disk_sensor( assert disk_sensor is not None assert disk_sensor.state == "70.0" assert disk_sensor.attributes["unit_of_measurement"] == "%" + + +async def test_cpu_percentage_is_zero_returns_unknown( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + mock_psutil: Mock, + mock_added_config_entry: ConfigEntry, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the sensor.""" + cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") + assert cpu_sensor is not None + assert cpu_sensor.state == "10" + + mock_psutil.cpu_percent.return_value = 0.0 + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") + assert cpu_sensor is not None + assert cpu_sensor.state == STATE_UNKNOWN + + mock_psutil.cpu_percent.return_value = 15.0 + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") + assert cpu_sensor is not None + assert cpu_sensor.state == "15" diff --git a/tests/components/technove/fixtures/station_bad_status.json b/tests/components/technove/fixtures/station_bad_status.json new file mode 100644 index 00000000000..ad24ad43211 --- /dev/null +++ b/tests/components/technove/fixtures/station_bad_status.json @@ -0,0 +1,27 @@ +{ + "voltageIn": 238, + "voltageOut": 238, + "maxStationCurrent": 32, + "maxCurrent": 24, + "current": 23.75, + "network_ssid": "Connecting...", + "id": "AA:AA:AA:AA:AA:BB", + "auto_charge": true, + "highChargePeriodActive": false, + "normalPeriodActive": false, + "maxChargePourcentage": 0.9, + "isBatteryProtected": false, + "inSharingMode": true, + "energySession": 12.34, + "energyTotal": 1234, + "version": "1.82", + "rssi": -82, + "name": "TechnoVE Station", + "lastCharge": "1701072080,0,17.39\n", + "time": 1701000000, + "isUpToDate": true, + "isSessionActive": true, + "conflictInSharingConfig": false, + "isStaticIp": false, + "status": 12345 +} diff --git a/tests/components/technove/snapshots/test_sensor.ambr b/tests/components/technove/snapshots/test_sensor.ambr index d38b08631cc..cbaf8813604 100644 --- a/tests/components/technove/snapshots/test_sensor.ambr +++ b/tests/components/technove/snapshots/test_sensor.ambr @@ -297,6 +297,8 @@ 'unplugged', 'plugged_waiting', 'plugged_charging', + 'out_of_activation_period', + 'high_charge_period', ]), }), 'config_entry_id': , @@ -333,6 +335,8 @@ 'unplugged', 'plugged_waiting', 'plugged_charging', + 'out_of_activation_period', + 'high_charge_period', ]), }), 'context': , diff --git a/tests/components/technove/test_sensor.py b/tests/components/technove/test_sensor.py index 5215f62c517..c44aab8ecc4 100644 --- a/tests/components/technove/test_sensor.py +++ b/tests/components/technove/test_sensor.py @@ -5,15 +5,20 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion -from technove import Status, TechnoVEError +from technove import Station, Status, TechnoVEError +from homeassistant.components.technove.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_with_selected_platforms -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_object_fixture, +) @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_technove") @@ -93,3 +98,27 @@ async def test_sensor_update_failure( await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("init_integration") +async def test_sensor_unknown_status( + hass: HomeAssistant, + mock_technove: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test coordinator update failure.""" + entity_id = "sensor.technove_station_status" + + assert hass.states.get(entity_id).state == Status.PLUGGED_CHARGING.value + + mock_technove.update.return_value = Station( + load_json_object_fixture("station_bad_status.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5, seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNKNOWN + # Other sensors should still be available + assert hass.states.get("sensor.technove_station_total_energy_usage").state == "1234" diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 30e59014bbf..4c188fcddcc 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -36,6 +36,7 @@ IP_ADDRESS2 = "127.0.0.2" ALIAS = "My Bulb" MODEL = "HS100" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" CREDENTIALS_HASH_LEGACY = "" diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index f5b0ba6c41f..edb19a93207 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -33,6 +33,7 @@ from . import ( DEFAULT_ENTRY_TITLE, DEVICE_CONFIG_DICT_AUTH, DEVICE_CONFIG_DICT_LEGACY, + DHCP_FORMATTED_MAC_ADDRESS, IP_ADDRESS, MAC_ADDRESS, MAC_ADDRESS2, @@ -144,6 +145,7 @@ async def test_discovery_auth( assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_ENTRY_TITLE assert result2["data"] == CREATE_ENTRY_DATA_AUTH + assert result2["context"]["unique_id"] == MAC_ADDRESS @pytest.mark.parametrize( @@ -206,6 +208,7 @@ async def test_discovery_auth_errors( ) assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["context"]["unique_id"] == MAC_ADDRESS async def test_discovery_new_credentials( @@ -254,6 +257,7 @@ async def test_discovery_new_credentials( ) assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["context"]["unique_id"] == MAC_ADDRESS async def test_discovery_new_credentials_invalid( @@ -309,6 +313,7 @@ async def test_discovery_new_credentials_invalid( ) assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["context"]["unique_id"] == MAC_ADDRESS async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> None: @@ -365,6 +370,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == CREATE_ENTRY_DATA_LEGACY + assert result3["context"]["unique_id"] == MAC_ADDRESS await hass.async_block_till_done() mock_setup_entry.assert_called_once() @@ -432,6 +438,7 @@ async def test_manual(hass: HomeAssistant) -> None: assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == DEFAULT_ENTRY_TITLE assert result4["data"] == CREATE_ENTRY_DATA_LEGACY + assert result4["context"]["unique_id"] == MAC_ADDRESS # Duplicate result = await hass.config_entries.flow.async_init( @@ -470,6 +477,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CREATE_ENTRY_DATA_LEGACY + assert result["context"]["unique_id"] == MAC_ADDRESS async def test_manual_auth( @@ -510,6 +518,7 @@ async def test_manual_auth( assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["context"]["unique_id"] == MAC_ADDRESS @pytest.mark.parametrize( @@ -572,6 +581,7 @@ async def test_manual_auth_errors( ) assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"] == CREATE_ENTRY_DATA_AUTH + assert result4["context"]["unique_id"] == MAC_ADDRESS await hass.async_block_till_done() @@ -599,7 +609,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( - ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS + ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS ), ) await hass.async_block_till_done() @@ -611,7 +621,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( - ip=IP_ADDRESS, macaddress="00:00:00:00:00:00", hostname="mock_hostname" + ip=IP_ADDRESS, macaddress="000000000000", hostname="mock_hostname" ), ) await hass.async_block_till_done() @@ -625,7 +635,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( - ip="1.2.3.5", macaddress="00:00:00:00:00:01", hostname="mock_hostname" + ip="1.2.3.5", macaddress="000000000001", hostname="mock_hostname" ), ) await hass.async_block_till_done() @@ -638,7 +648,9 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS), + dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS + ), ), ( config_entries.SOURCE_INTEGRATION_DISCOVERY, @@ -675,6 +687,8 @@ async def test_discovered_by_dhcp_or_discovery( assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == CREATE_ENTRY_DATA_LEGACY + assert result2["context"]["unique_id"] == MAC_ADDRESS + assert mock_async_setup.called assert mock_async_setup_entry.called @@ -684,7 +698,9 @@ async def test_discovered_by_dhcp_or_discovery( [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS), + dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS + ), ), ( config_entries.SOURCE_INTEGRATION_DISCOVERY, @@ -713,7 +729,7 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device( assert result["reason"] == "cannot_connect" -async def test_discovery_with_ip_change( +async def test_integration_discovery_with_ip_change( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_discovery: AsyncMock, @@ -764,6 +780,36 @@ async def test_discovery_with_ip_change( mock_connect["connect"].assert_awaited_once_with(config=config) +async def test_dhcp_discovery_with_ip_change( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test dhcp discovery with an IP change.""" + mock_connect["connect"].side_effect = SmartDeviceException() + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_RETRY + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY + assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1" + + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="127.0.0.2", macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS + ), + ) + assert discovery_result["type"] is FlowResultType.ABORT + assert discovery_result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + + async def test_reauth( hass: HomeAssistant, mock_added_config_entry: MockConfigEntry, @@ -1022,6 +1068,7 @@ async def test_pick_device_errors( }, ) assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["context"]["unique_id"] == MAC_ADDRESS async def test_discovery_timeout_connect( @@ -1046,6 +1093,7 @@ async def test_discovery_timeout_connect( ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["context"]["unique_id"] == MAC_ADDRESS assert mock_connect["connect"].call_count == 1 diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 70a399d27a4..2924e6654e2 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -228,6 +228,46 @@ async def test_in_zone_works_for_passive_zones(hass: HomeAssistant) -> None: assert zone.in_zone(hass.states.get("zone.passive_zone"), latitude, longitude) +async def test_async_active_zone_with_non_zero_radius( + hass: HomeAssistant, +) -> None: + """Test async_active_zone with a non-zero radius.""" + latitude = 32.880600 + longitude = -117.237561 + + assert await setup.async_setup_component( + hass, + zone.DOMAIN, + { + "zone": [ + { + "name": "Small Zone", + "latitude": 32.980600, + "longitude": -117.137561, + "radius": 50000, + }, + { + "name": "Big Zone", + "latitude": 32.980600, + "longitude": -117.137561, + "radius": 100000, + }, + ] + }, + ) + + home_state = hass.states.get("zone.home") + assert home_state.attributes["radius"] == 100 + assert home_state.attributes["latitude"] == 32.87336 + assert home_state.attributes["longitude"] == -117.22743 + + active = zone.async_active_zone(hass, latitude, longitude, 5000) + assert active.entity_id == "zone.home" + + active = zone.async_active_zone(hass, latitude, longitude, 0) + assert active.entity_id == "zone.small_zone" + + async def test_core_config_update(hass: HomeAssistant) -> None: """Test updating core config will update home zone.""" assert await setup.async_setup_component(hass, "zone", {})