diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index bbc763d7ec3d5..cf453c75773b9 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -45,7 +45,7 @@ {vol.Optional(CONF_FORCE, default=False): cv.boolean} ) -PLATFORMS = [Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.UPDATE] type AdGuardConfigEntry = ConfigEntry[AdGuardData] diff --git a/homeassistant/components/adguard/update.py b/homeassistant/components/adguard/update.py new file mode 100644 index 0000000000000..74d427e973fc0 --- /dev/null +++ b/homeassistant/components/adguard/update.py @@ -0,0 +1,71 @@ +"""AdGuard Home Update platform.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from adguardhome import AdGuardHomeError + +from homeassistant.components.update import UpdateEntity, UpdateEntityFeature +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import AdGuardConfigEntry, AdGuardData +from .const import DOMAIN +from .entity import AdGuardHomeEntity + +SCAN_INTERVAL = timedelta(seconds=300) +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AdGuardConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up AdGuard Home update entity based on a config entry.""" + data = entry.runtime_data + + if (await data.client.update.update_available()).disabled: + return + + async_add_entities([AdGuardHomeUpdate(data, entry)], True) + + +class AdGuardHomeUpdate(AdGuardHomeEntity, UpdateEntity): + """Defines an AdGuard Home update.""" + + _attr_supported_features = UpdateEntityFeature.INSTALL + _attr_name = None + + def __init__( + self, + data: AdGuardData, + entry: AdGuardConfigEntry, + ) -> None: + """Initialize AdGuard Home update.""" + super().__init__(data, entry) + + self._attr_unique_id = "_".join( + [DOMAIN, self.adguard.host, str(self.adguard.port), "update"] + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + value = await self.adguard.update.update_available() + self._attr_installed_version = self.data.version + self._attr_latest_version = value.new_version + self._attr_release_summary = value.announcement + self._attr_release_url = value.announcement_url + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install latest update.""" + try: + await self.adguard.update.begin_update() + except AdGuardHomeError as err: + raise HomeAssistantError(f"Failed to install update: {err}") from err + self.hass.config_entries.async_schedule_reload(self._entry.entry_id) diff --git a/homeassistant/components/anthropic/entity.py b/homeassistant/components/anthropic/entity.py index fcefcbb50a52e..1714ab7b1ac4c 100644 --- a/homeassistant/components/anthropic/entity.py +++ b/homeassistant/components/anthropic/entity.py @@ -392,7 +392,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have type="tool_use", id=response.content_block.id, name=response.content_block.name, - input="", + input={}, ) current_tool_args = "" if response.content_block.name == output_tool: @@ -459,7 +459,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have type="server_tool_use", id=response.content_block.id, name=response.content_block.name, - input="", + input={}, ) current_tool_args = "" elif isinstance(response.content_block, WebSearchToolResultBlock): diff --git a/homeassistant/components/anthropic/manifest.json b/homeassistant/components/anthropic/manifest.json index a0991f42fdbf1..bb24dfbd397ca 100644 --- a/homeassistant/components/anthropic/manifest.json +++ b/homeassistant/components/anthropic/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/anthropic", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["anthropic==0.69.0"] + "requirements": ["anthropic==0.73.0"] } diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 50a8d674f771e..81900bed57f35 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,7 +20,7 @@ "bluetooth-adapters==2.1.0", "bluetooth-auto-recovery==1.5.3", "bluetooth-data-tools==1.28.4", - "dbus-fast==2.45.1", + "dbus-fast==3.0.0", "habluetooth==5.7.0" ] } diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 586e9c6582802..b66cee2bce73d 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -4237,7 +4237,8 @@ async def async_step_entity( return self.async_show_form( step_id="entity", data_schema=data_schema, - description_placeholders={ + description_placeholders=TRANSLATION_DESCRIPTION_PLACEHOLDERS + | { "mqtt_device": device_name, "entity_name_label": entity_name_label, "platform_label": platform_label, diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index d93835333002a..176d7330fd19e 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -19,5 +19,5 @@ "documentation": "https://www.home-assistant.io/integrations/nest", "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], - "requirements": ["google-nest-sdm==7.1.4"] + "requirements": ["google-nest-sdm==9.0.1"] } diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index c83c71ee9bc9a..d539243d28739 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -37,6 +37,7 @@ class PlugwiseSelectEntityDescription(SelectEntityDescription): PlugwiseSelectEntityDescription( key=SELECT_SCHEDULE, translation_key=SELECT_SCHEDULE, + entity_category=EntityCategory.CONFIG, options_key="available_schedules", ), PlugwiseSelectEntityDescription( diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 7bd93e2ff84c5..aa417d2eeeb60 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -48,7 +48,6 @@ class PlugwiseSensorEntityDescription(SensorEntityDescription): native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseSensorEntityDescription( key="setpoint_high", @@ -56,7 +55,6 @@ class PlugwiseSensorEntityDescription(SensorEntityDescription): native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseSensorEntityDescription( key="setpoint_low", @@ -64,13 +62,11 @@ class PlugwiseSensorEntityDescription(SensorEntityDescription): native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseSensorEntityDescription( key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, - entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), PlugwiseSensorEntityDescription( @@ -94,6 +90,7 @@ class PlugwiseSensorEntityDescription(SensorEntityDescription): translation_key="outdoor_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), PlugwiseSensorEntityDescription( @@ -352,8 +349,8 @@ class PlugwiseSensorEntityDescription(SensorEntityDescription): key="illuminance", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, - state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, ), PlugwiseSensorEntityDescription( key="modulation_level", @@ -365,8 +362,8 @@ class PlugwiseSensorEntityDescription(SensorEntityDescription): PlugwiseSensorEntityDescription( key="valve_position", translation_key="valve_position", - entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), PlugwiseSensorEntityDescription( diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 0f64dc059020d..4c597b0c0f44e 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -222,17 +222,13 @@ async def async_init(self) -> None: enable_onvif = None enable_rtmp = None - if not self._api.rtsp_enabled and not self._api.baichuan_only: + if not self._api.rtsp_enabled and self._api.supported(None, "RTSP"): _LOGGER.debug( "RTSP is disabled on %s, trying to enable it", self._api.nvr_name ) enable_rtsp = True - if ( - not self._api.onvif_enabled - and onvif_supported - and not self._api.baichuan_only - ): + if not self._api.onvif_enabled and onvif_supported: _LOGGER.debug( "ONVIF is disabled on %s, trying to enable it", self._api.nvr_name ) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 81e633717176b..bf51fc2692d61 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -10,6 +10,7 @@ import aiohttp from aiohttp import hdrs import voluptuous as vol +from yarl import URL from homeassistant.const import ( CONF_AUTHENTICATION, @@ -51,6 +52,7 @@ CONF_CONTENT_TYPE = "content_type" CONF_INSECURE_CIPHER = "insecure_cipher" +CONF_SKIP_URL_ENCODING = "skip_url_encoding" COMMAND_SCHEMA = vol.Schema( { @@ -69,6 +71,7 @@ vol.Optional(CONF_CONTENT_TYPE): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_INSECURE_CIPHER, default=False): cv.boolean, + vol.Optional(CONF_SKIP_URL_ENCODING, default=False): cv.boolean, } ) @@ -113,6 +116,7 @@ def async_register_rest_command(name: str, command_config: dict[str, Any]) -> No method = command_config[CONF_METHOD] template_url = command_config[CONF_URL] + skip_url_encoding = command_config[CONF_SKIP_URL_ENCODING] auth = None digest_middleware = None @@ -179,7 +183,7 @@ async def async_service_handler(service: ServiceCall) -> ServiceResponse: request_kwargs["middlewares"] = (digest_middleware,) async with getattr(websession, method)( - request_url, + URL(request_url, encoded=skip_url_encoding), **request_kwargs, ) as response: if response.status < HTTPStatus.BAD_REQUEST: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d2fba4c40fd67..792b576c9fd37 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ certifi>=2021.5.30 ciso8601==2.3.3 cronsim==2.7 cryptography==46.0.2 -dbus-fast==2.45.1 +dbus-fast==3.0.0 file-read-backwards==2.0.0 fnv-hash-fast==1.6.0 go2rtc-client==0.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9732f92d64536..2b791dd85c982 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -500,7 +500,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.69.0 +anthropic==0.73.0 # homeassistant.components.mcp_server anyio==4.10.0 @@ -772,7 +772,7 @@ datadog==0.52.0 datapoint==0.12.1 # homeassistant.components.bluetooth -dbus-fast==2.45.1 +dbus-fast==3.0.0 # homeassistant.components.debugpy debugpy==1.8.16 @@ -1076,7 +1076,7 @@ google-genai==1.38.0 google-maps-routing==0.6.15 # homeassistant.components.nest -google-nest-sdm==7.1.4 +google-nest-sdm==9.0.1 # homeassistant.components.google_photos google-photos-library-api==0.12.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f0e5e8f0ce3d..cb4f8e97b337b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -473,7 +473,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.69.0 +anthropic==0.73.0 # homeassistant.components.mcp_server anyio==4.10.0 @@ -675,7 +675,7 @@ datadog==0.52.0 datapoint==0.12.1 # homeassistant.components.bluetooth -dbus-fast==2.45.1 +dbus-fast==3.0.0 # homeassistant.components.debugpy debugpy==1.8.16 @@ -943,7 +943,7 @@ google-genai==1.38.0 google-maps-routing==0.6.15 # homeassistant.components.nest -google-nest-sdm==7.1.4 +google-nest-sdm==9.0.1 # homeassistant.components.google_photos google-photos-library-api==0.12.1 diff --git a/tests/components/adguard/__init__.py b/tests/components/adguard/__init__.py index 4d8ae091dc533..a44c30e4f2068 100644 --- a/tests/components/adguard/__init__.py +++ b/tests/components/adguard/__init__.py @@ -1 +1,25 @@ """Tests for the AdGuard Home integration.""" + +from homeassistant.const import CONTENT_TYPE_JSON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def setup_integration( + hass: HomeAssistant, + config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + aioclient_mock.get( + "https://127.0.0.1:3000/control/status", + json={"version": "v0.107.50"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/adguard/conftest.py b/tests/components/adguard/conftest.py new file mode 100644 index 0000000000000..5245e3aef5cdf --- /dev/null +++ b/tests/components/adguard/conftest.py @@ -0,0 +1,32 @@ +"""Common fixtures for the adguard tests.""" + +import pytest + +from homeassistant.components.adguard import DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 3000, + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_SSL: True, + CONF_VERIFY_SSL: True, + }, + title="AdGuard Home", + ) diff --git a/tests/components/adguard/snapshots/test_update.ambr b/tests/components/adguard/snapshots/test_update.ambr new file mode 100644 index 0000000000000..fc6af1b61ee25 --- /dev/null +++ b/tests/components/adguard/snapshots/test_update.ambr @@ -0,0 +1,61 @@ +# serializer version: 1 +# name: test_update[update.adguard_home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.adguard_home', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'adguard', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'adguard_127.0.0.1_3000_update', + 'unit_of_measurement': None, + }) +# --- +# name: test_update[update.adguard_home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'display_precision': 0, + 'entity_picture': 'https://brands.home-assistant.io/_/adguard/icon.png', + 'friendly_name': 'AdGuard Home', + 'in_progress': False, + 'installed_version': 'v0.107.50', + 'latest_version': 'v0.107.59', + 'release_summary': 'AdGuard Home v0.107.59 is now available!', + 'release_url': 'https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.107.59', + 'skipped_version': None, + 'supported_features': , + 'title': None, + 'update_percentage': None, + }), + 'context': , + 'entity_id': 'update.adguard_home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/adguard/test_update.py b/tests/components/adguard/test_update.py new file mode 100644 index 0000000000000..deb37c1684473 --- /dev/null +++ b/tests/components/adguard/test_update.py @@ -0,0 +1,138 @@ +"""Tests for the AdGuard Home update entity.""" + +from unittest.mock import patch + +from adguardhome import AdGuardHomeError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import CONTENT_TYPE_JSON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the adguard update platform.""" + aioclient_mock.post( + "https://127.0.0.1:3000/control/version.json", + json={ + "new_version": "v0.107.59", + "announcement": "AdGuard Home v0.107.59 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.107.59", + "can_autoupdate": True, + "disabled": False, + }, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry, aioclient_mock) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_update_disabled( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the adguard update is disabled.""" + aioclient_mock.post( + "https://127.0.0.1:3000/control/version.json", + json={"disabled": True}, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry, aioclient_mock) + + assert not hass.states.async_all() + + +async def test_update_install( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the adguard update installation.""" + aioclient_mock.post( + "https://127.0.0.1:3000/control/version.json", + json={ + "new_version": "v0.107.59", + "announcement": "AdGuard Home v0.107.59 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.107.59", + "can_autoupdate": True, + "disabled": False, + }, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.post("https://127.0.0.1:3000/control/update") + + with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry, aioclient_mock) + + aioclient_mock.mock_calls.clear() + + await hass.services.async_call( + "update", + "install", + {"entity_id": "update.adguard_home"}, + blocking=True, + ) + + assert aioclient_mock.mock_calls[0][0] == "POST" + assert ( + str(aioclient_mock.mock_calls[0][1]) == "https://127.0.0.1:3000/control/update" + ) + + +async def test_update_install_failed( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the adguard update install failed.""" + aioclient_mock.post( + "https://127.0.0.1:3000/control/version.json", + json={ + "new_version": "v0.107.59", + "announcement": "AdGuard Home v0.107.59 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.107.59", + "can_autoupdate": True, + "disabled": False, + }, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.post( + "https://127.0.0.1:3000/control/update", exc=AdGuardHomeError("boom") + ) + + with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry, aioclient_mock) + + aioclient_mock.mock_calls.clear() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "update", + "install", + {"entity_id": "update.adguard_home"}, + blocking=True, + ) + + assert aioclient_mock.mock_calls[0][0] == "POST" + assert ( + str(aioclient_mock.mock_calls[0][1]) == "https://127.0.0.1:3000/control/update" + ) diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index a4dc0a39be602..d443bb6ef52ab 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -1,74 +1,16 @@ """Tests for GIOS.""" -from unittest.mock import patch - -from homeassistant.components.gios.const import DOMAIN from homeassistant.core import HomeAssistant -from tests.common import ( - MockConfigEntry, - async_load_json_array_fixture, - async_load_json_object_fixture, -) - -STATIONS = [ - { - "Identyfikator stacji": 123, - "Nazwa stacji": "Test Name 1", - "WGS84 φ N": "99.99", - "WGS84 λ E": "88.88", - }, - { - "Identyfikator stacji": 321, - "Nazwa stacji": "Test Name 2", - "WGS84 φ N": "77.77", - "WGS84 λ E": "66.66", - }, -] - - -async def init_integration( - hass: HomeAssistant, incomplete_data=False, invalid_indexes=False -) -> MockConfigEntry: - """Set up the GIOS integration in Home Assistant.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Home", - unique_id="123", - data={"station_id": 123, "name": "Home"}, - entry_id="86129426118ae32020417a53712d6eef", - ) +from tests.common import MockConfigEntry - indexes = await async_load_json_object_fixture(hass, "indexes.json", DOMAIN) - station = await async_load_json_array_fixture(hass, "station.json", DOMAIN) - sensors = await async_load_json_object_fixture(hass, "sensors.json", DOMAIN) - if incomplete_data: - indexes["AqIndex"] = "foo" - sensors["pm10"]["Lista danych pomiarowych"][0]["Wartość"] = None - sensors["pm10"]["Lista danych pomiarowych"][1]["Wartość"] = None - if invalid_indexes: - indexes = {} - with ( - patch( - "homeassistant.components.gios.coordinator.Gios._get_stations", - return_value=STATIONS, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_station", - return_value=station, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_all_sensors", - return_value=sensors, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_indexes", - return_value=indexes, - ), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() +async def setup_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Set up the GIOS integration for testing.""" + mock_config_entry.add_to_hass(hass) - return entry + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/gios/conftest.py b/tests/components/gios/conftest.py new file mode 100644 index 0000000000000..3ab1a70ed796e --- /dev/null +++ b/tests/components/gios/conftest.py @@ -0,0 +1,84 @@ +"""Fixtures for GIOS integration tests.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, MagicMock, patch + +from gios.model import GiosSensors, GiosStation, Sensor as GiosSensor +import pytest + +from homeassistant.components.gios.const import DOMAIN +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="123", + data={"station_id": 123, "name": "Home"}, + entry_id="86129426118ae32020417a53712d6eef", + ) + + +@pytest.fixture +def mock_gios_sensors() -> GiosSensors: + """Return the default mocked gios sensors.""" + return GiosSensors( + aqi=GiosSensor(name="AQI", id=None, index=None, value="good"), + c6h6=GiosSensor(name="benzene", id=658, index="very_good", value=0.23789), + co=GiosSensor(name="carbon monoxide", id=660, index="good", value=251.874), + no=GiosSensor(name="nitrogen monoxide", id=664, index=None, value=5.1), + no2=GiosSensor(name="nitrogen dioxide", id=665, index="good", value=7.13411), + nox=GiosSensor(name="nitrogen oxides", id=666, index=None, value=5.5), + o3=GiosSensor(name="ozone", id=667, index="good", value=95.7768), + pm10=GiosSensor( + name="particulate matter 10", id=14395, index="good", value=16.8344 + ), + pm25=GiosSensor(name="particulate matter 2.5", id=670, index="good", value=4), + so2=GiosSensor(name="sulfur dioxide", id=672, index="very_good", value=4.35478), + ) + + +@pytest.fixture +def mock_gios_stations() -> dict[int, GiosStation]: + """Return the default mocked gios stations.""" + return { + 123: GiosStation(id=123, name="Test Name 1", latitude=99.99, longitude=88.88), + 321: GiosStation(id=321, name="Test Name 2", latitude=77.77, longitude=66.66), + } + + +@pytest.fixture +async def mock_gios( + hass: HomeAssistant, + mock_gios_stations: dict[int, GiosStation], + mock_gios_sensors: GiosSensors, +) -> AsyncGenerator[MagicMock]: + """Yield a mocked GIOS client.""" + with ( + patch("homeassistant.components.gios.Gios", autospec=True) as mock_gios, + patch("homeassistant.components.gios.config_flow.Gios", new=mock_gios), + ): + mock_gios.create = AsyncMock(return_value=mock_gios) + mock_gios.async_update = AsyncMock(return_value=mock_gios_sensors) + mock_gios.measurement_stations = mock_gios_stations + mock_gios.station_id = 123 + mock_gios.station_name = mock_gios_stations[mock_gios.station_id].name + + yield mock_gios + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_gios: MagicMock, +) -> None: + """Set up the GIOS integration for testing.""" + await setup_integration(hass, mock_config_entry) diff --git a/tests/components/gios/fixtures/indexes.json b/tests/components/gios/fixtures/indexes.json deleted file mode 100644 index 1fb46e9a4d86e..0000000000000 --- a/tests/components/gios/fixtures/indexes.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "AqIndex": { - "Identyfikator stacji pomiarowej": 123, - "Data wykonania obliczeń indeksu": "2020-07-31 15:10:17", - "Nazwa kategorii indeksu": "Dobry", - "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika st": "2020-07-31 14:00:00", - "Data wykonania obliczeń indeksu dla wskaźnika SO2": "2020-07-31 15:10:17", - "Wartość indeksu dla wskaźnika SO2": 0, - "Nazwa kategorii indeksu dla wskażnika SO2": "Bardzo dobry", - "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika SO2": "2020-07-31 14:00:00", - "Data wykonania obliczeń indeksu dla wskaźnika NO2": "2020-07-31 14:00:00", - "Wartość indeksu dla wskaźnika NO2": 0, - "Nazwa kategorii indeksu dla wskażnika NO2": "Dobry", - "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika NO2": "2020-07-31 14:00:00", - "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika CO": "2020-07-31 15:10:17", - "Wartość indeksu dla wskaźnika CO": 0, - "Nazwa kategorii indeksu dla wskażnika CO": "Dobry", - "Data wykonania obliczeń indeksu dla wskaźnika CO": "2020-07-31 14:00:00", - "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika PM10": "2020-07-31 15:10:17", - "Wartość indeksu dla wskaźnika PM10": 0, - "Nazwa kategorii indeksu dla wskażnika PM10": "Dobry", - "Data wykonania obliczeń indeksu dla wskaźnika PM10": "2020-07-31 14:00:00", - "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika PM2.5": "2020-07-31 15:10:17", - "Wartość indeksu dla wskaźnika PM2.5": 0, - "Nazwa kategorii indeksu dla wskażnika PM2.5": "Dobry", - "Data wykonania obliczeń indeksu dla wskaźnika PM2.5": "2020-07-31 14:00:00", - "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika O3": "2020-07-31 15:10:17", - "Wartość indeksu dla wskaźnika O3": 1, - "Nazwa kategorii indeksu dla wskażnika O3": "Dobry", - "Data wykonania obliczeń indeksu dla wskaźnika O3": "2020-07-31 14:00:00", - "Data danych źródłowych, z których policzono wartość indeksu dla wskaźnika C6H6": "2020-07-31 15:10:17", - "Wartość indeksu dla wskaźnika C6H6": 0, - "Nazwa kategorii indeksu dla wskażnika C6H6": "Bardzo dobry", - "Data wykonania obliczeń indeksu dla wskaźnika C6H6": "2020-07-31 14:00:00", - "Status indeksu ogólnego dla stacji pomiarowej": true, - "Kod zanieczyszczenia krytycznego": "OZON" - } -} diff --git a/tests/components/gios/fixtures/sensors.json b/tests/components/gios/fixtures/sensors.json deleted file mode 100644 index 64cb9685f97d6..0000000000000 --- a/tests/components/gios/fixtures/sensors.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "so2": { - "Lista danych pomiarowych": [ - { "Data": "2020-07-31 15:00:00", "Wartość": 4.35478 }, - { "Data": "2020-07-31 14:00:00", "Wartość": 4.25478 }, - { "Data": "2020-07-31 13:00:00", "Wartość": 4.34309 } - ] - }, - "c6h6": { - "Lista danych pomiarowych": [ - { "Data": "2020-07-31 15:00:00", "Wartość": 0.23789 }, - { "Data": "2020-07-31 14:00:00", "Wartość": 0.22789 }, - { "Data": "2020-07-31 13:00:00", "Wartość": 0.21315 } - ] - }, - "co": { - "Lista danych pomiarowych": [ - { "Data": "2020-07-31 15:00:00", "Wartość": 251.874 }, - { "Data": "2020-07-31 14:00:00", "Wartość": 250.874 }, - { "Data": "2020-07-31 13:00:00", "Wartość": 251.097 } - ] - }, - "no": { - "Lista danych pomiarowych": [ - { "Data": "2020-07-31 15:00:00", "Wartość": 5.1 }, - { "Data": "2020-07-31 14:00:00", "Wartość": 4.0 }, - { "Data": "2020-07-31 13:00:00", "Wartość": 5.2 } - ] - }, - "no2": { - "Lista danych pomiarowych": [ - { "Data": "2020-07-31 15:00:00", "Wartość": 7.13411 }, - { "Data": "2020-07-31 14:00:00", "Wartość": 7.33411 }, - { "Data": "2020-07-31 13:00:00", "Wartość": 9.32578 } - ] - }, - "nox": { - "Lista danych pomiarowych": [ - { "Data": "2020-07-31 15:00:00", "Wartość": 5.5 }, - { "Data": "2020-07-31 14:00:00", "Wartość": 6.3 }, - { "Data": "2020-07-31 13:00:00", "Wartość": 4.9 } - ] - }, - "o3": { - "Lista danych pomiarowych": [ - { "Data": "2020-07-31 15:00:00", "Wartość": 95.7768 }, - { "Data": "2020-07-31 14:00:00", "Wartość": 93.7768 }, - { "Data": "2020-07-31 13:00:00", "Wartość": 89.4232 } - ] - }, - "pm2.5": { - "Lista danych pomiarowych": [ - { "Data": "2020-07-31 15:00:00", "Wartość": 4 }, - { "Data": "2020-07-31 14:00:00", "Wartość": 4 }, - { "Data": "2020-07-31 13:00:00", "Wartość": 5 } - ] - }, - "pm10": { - "Lista danych pomiarowych": [ - { "Data": "2020-07-31 15:00:00", "Wartość": 16.8344 }, - { "Data": "2020-07-31 14:00:00", "Wartość": 17.8344 }, - { "Data": "2020-07-31 13:00:00", "Wartość": 20.8094 } - ] - } -} diff --git a/tests/components/gios/fixtures/station.json b/tests/components/gios/fixtures/station.json deleted file mode 100644 index 1d112c0947b71..0000000000000 --- a/tests/components/gios/fixtures/station.json +++ /dev/null @@ -1,74 +0,0 @@ -[ - { - "Identyfikator stanowiska": 672, - "Identyfikator stacji": 117, - "Wskaźnik": "dwutlenek siarki", - "Wskaźnik - wzór": "SO2", - "Wskaźnik - kod": "SO2", - "Id wskaźnika": 1 - }, - { - "Identyfikator stanowiska": 658, - "Identyfikator stacji": 117, - "Wskaźnik": "benzen", - "Wskaźnik - wzór": "C6H6", - "Wskaźnik - kod": "C6H6", - "Id wskaźnika": 10 - }, - { - "Identyfikator stanowiska": 660, - "Identyfikator stacji": 117, - "Wskaźnik": "tlenek węgla", - "Wskaźnik - wzór": "CO", - "Wskaźnik - kod": "CO", - "Id wskaźnika": 8 - }, - { - "Identyfikator stanowiska": 664, - "Identyfikator stacji": 117, - "Wskaźnik": "tlenek azotu", - "Wskaźnik - wzór": "NO", - "Wskaźnik - kod": "NO", - "Id wskaźnika": 16 - }, - { - "Identyfikator stanowiska": 665, - "Identyfikator stacji": 117, - "Wskaźnik": "dwutlenek azotu", - "Wskaźnik - wzór": "NO2", - "Wskaźnik - kod": "NO2", - "Id wskaźnika": 6 - }, - { - "Identyfikator stanowiska": 666, - "Identyfikator stacji": 117, - "Wskaźnik": "tlenki azotu", - "Wskaźnik - wzór": "NOx", - "Wskaźnik - kod": "NOx", - "Id wskaźnika": 7 - }, - { - "Identyfikator stanowiska": 667, - "Identyfikator stacji": 117, - "Wskaźnik": "ozon", - "Wskaźnik - wzór": "O3", - "Wskaźnik - kod": "O3", - "Id wskaźnika": 5 - }, - { - "Identyfikator stanowiska": 670, - "Identyfikator stacji": 117, - "Wskaźnik": "pył zawieszony PM2.5", - "Wskaźnik - wzór": "PM2.5", - "Wskaźnik - kod": "PM2.5", - "Id wskaźnika": 69 - }, - { - "Identyfikator stanowiska": 14395, - "Identyfikator stacji": 117, - "Wskaźnik": "pył zawieszony PM10", - "Wskaźnik - wzór": "PM10", - "Wskaźnik - kod": "PM10", - "Id wskaźnika": 3 - } -] diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index ee783ba57e343..715e15318f8f6 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -1,138 +1,93 @@ """Define tests for the GIOS config flow.""" -import json -from unittest.mock import patch +from unittest.mock import MagicMock -from gios import ApiError +from gios import ApiError, InvalidSensorsDataError +import pytest -from homeassistant.components.gios import config_flow from homeassistant.components.gios.const import CONF_STATION_ID, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import STATIONS - -from tests.common import async_load_fixture - CONFIG = { CONF_NAME: "Foo", CONF_STATION_ID: "123", } +pytestmark = pytest.mark.usefixtures("mock_gios") + async def test_show_form(hass: HomeAssistant) -> None: """Test that the form is served with no input.""" - with patch( - "homeassistant.components.gios.coordinator.Gios._get_stations", - return_value=STATIONS, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" + assert len(result["data_schema"].schema[CONF_STATION_ID].config["options"]) == 2 -async def test_form_with_api_error(hass: HomeAssistant) -> None: +async def test_form_with_api_error(hass: HomeAssistant, mock_gios: MagicMock) -> None: """Test the form is aborted because of API error.""" - with patch( - "homeassistant.components.gios.coordinator.Gios._get_stations", - side_effect=ApiError("error"), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - assert result["type"] is FlowResultType.ABORT - - -async def test_invalid_sensor_data(hass: HomeAssistant) -> None: - """Test that errors are shown when sensor data is invalid.""" - with ( - patch( - "homeassistant.components.gios.coordinator.Gios._get_stations", - return_value=STATIONS, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_station", - return_value=json.loads( - await async_load_fixture(hass, "station.json", DOMAIN) - ), - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_sensor", - return_value={}, - ), - ): - flow = config_flow.GiosFlowHandler() - flow.hass = hass - flow.context = {} + mock_gios.create.side_effect = ApiError("error") - result = await flow.async_step_user(user_input=CONFIG) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - assert result["errors"] == {CONF_STATION_ID: "invalid_sensors_data"} + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" -async def test_cannot_connect(hass: HomeAssistant) -> None: - """Test that errors are shown when cannot connect to GIOS server.""" - with ( - patch( - "homeassistant.components.gios.coordinator.Gios._get_stations", - return_value=STATIONS, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._async_get", - side_effect=ApiError("error"), +@pytest.mark.parametrize( + ("exception", "errors"), + [ + ( + InvalidSensorsDataError("Invalid data"), + {CONF_STATION_ID: "invalid_sensors_data"}, ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG - ) - await hass.async_block_till_done() + (ApiError("error"), {"base": "cannot_connect"}), + ], +) +async def test_form_submission_errors( + hass: HomeAssistant, mock_gios: MagicMock, exception, errors +) -> None: + """Test errors during form submission.""" + mock_gios.async_update.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONFIG + ) - assert result["errors"] == {"base": "cannot_connect"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == errors + mock_gios.async_update.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONFIG + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Test Name 1" async def test_create_entry(hass: HomeAssistant) -> None: """Test that the user step works.""" - with ( - patch( - "homeassistant.components.gios.coordinator.Gios._get_stations", - return_value=STATIONS, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_station", - return_value=json.loads( - await async_load_fixture(hass, "station.json", DOMAIN) - ), - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_all_sensors", - return_value=json.loads( - await async_load_fixture(hass, "sensors.json", DOMAIN) - ), - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_indexes", - return_value=json.loads( - await async_load_fixture(hass, "indexes.json", DOMAIN) - ), - ), - ): - flow = config_flow.GiosFlowHandler() - flow.hass = hass - flow.context = {} - - result = await flow.async_step_user(user_input=CONFIG) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Test Name 1" - assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] - - assert flow.context["unique_id"] == "123" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONFIG + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Test Name 1" + assert result["data"][CONF_STATION_ID] == 123 + + assert result["result"].unique_id == "123" diff --git a/tests/components/gios/test_diagnostics.py b/tests/components/gios/test_diagnostics.py index cc3df9e359374..11e051fef0adf 100644 --- a/tests/components/gios/test_diagnostics.py +++ b/tests/components/gios/test_diagnostics.py @@ -1,24 +1,24 @@ """Test GIOS diagnostics.""" +import pytest from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant -from . import init_integration - +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("init_integration") async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: """Test config entry diagnostics.""" - entry = await init_integration(hass) - - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( - exclude=props("created_at", "modified_at") - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index 9c7f7270ca4f7..20944ea44276f 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -1,7 +1,8 @@ """Test init of GIOS integration.""" -import json -from unittest.mock import patch +from unittest.mock import MagicMock + +import pytest from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.gios.const import DOMAIN @@ -10,108 +11,98 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from . import STATIONS, init_integration +from . import setup_integration -from tests.common import MockConfigEntry, async_load_fixture +from tests.common import MockConfigEntry -async def test_async_setup_entry(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("init_integration") +async def test_async_setup_entry( + hass: HomeAssistant, +) -> None: """Test a successful setup entry.""" - await init_integration(hass) - state = hass.states.get("sensor.home_pm2_5") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == "4" -async def test_config_not_ready(hass: HomeAssistant) -> None: +async def test_config_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_gios: MagicMock, +) -> None: """Test for setup failure if connection to GIOS is missing.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Home", - unique_id=123, - data={"station_id": 123, "name": "Home"}, - ) + mock_gios.create.side_effect = ConnectionError() - with patch( - "homeassistant.components.gios.coordinator.Gios._get_stations", - side_effect=ConnectionError(), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.SETUP_RETRY + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("init_integration") +async def test_unload_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: """Test successful unload of entry.""" - entry = await init_integration(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state is ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED - assert await hass.config_entries.async_unload(entry.entry_id) + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() - assert entry.state is ConfigEntryState.NOT_LOADED + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) async def test_migrate_device_and_config_entry( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + mock_gios: MagicMock, ) -> None: """Test device_info identifiers and config entry migration.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - title="Home", - unique_id=123, - data={ - "station_id": 123, - "name": "Home", - }, + mock_config_entry.add_to_hass(hass) + + device_entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, identifiers={(DOMAIN, 123)} ) - indexes = json.loads(await async_load_fixture(hass, "indexes.json", DOMAIN)) - station = json.loads(await async_load_fixture(hass, "station.json", DOMAIN)) - sensors = json.loads(await async_load_fixture(hass, "sensors.json", DOMAIN)) - - with ( - patch( - "homeassistant.components.gios.coordinator.Gios._get_stations", - return_value=STATIONS, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_station", - return_value=station, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_all_sensors", - return_value=sensors, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_indexes", - return_value=indexes, - ), - ): - config_entry.add_to_hass(hass) - - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, 123)} - ) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - migrated_device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123")} - ) - assert device_entry.id == migrated_device_entry.id + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + migrated_device_entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, identifiers={(DOMAIN, "123")} + ) + assert device_entry.id == migrated_device_entry.id + + +async def test_migrate_unique_id_to_str( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_gios: MagicMock, +) -> None: + """Test device_info identifiers and config entry migration.""" + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, + unique_id=int(mock_config_entry.unique_id), # type: ignore[misc] + ) + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.unique_id == "123" async def test_remove_air_quality_entities( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_gios: MagicMock, ) -> None: """Test remove air_quality entities from registry.""" + mock_config_entry.add_to_hass(hass) entity_registry.async_get_or_create( AIR_QUALITY_PLATFORM, DOMAIN, @@ -120,7 +111,8 @@ async def test_remove_air_quality_entities( disabled_by=None, ) - await init_integration(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() entry = entity_registry.async_get("air_quality.home") assert entry is None diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index b4e03dd748823..b668de99a4e7f 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -1,42 +1,45 @@ """Test sensor of GIOS integration.""" -from copy import deepcopy -from datetime import timedelta -import json -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory from gios import ApiError +from gios.model import GiosSensors +import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.gios.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as PLATFORM +from homeassistant.components.gios.const import DOMAIN, SCAN_INTERVAL from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow -from . import init_integration +from . import setup_integration -from tests.common import async_fire_time_changed, async_load_fixture, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.gios.PLATFORMS", [Platform.SENSOR]): + yield + + +@pytest.mark.usefixtures("init_integration") async def test_sensor( - hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: """Test states of the sensor.""" - with patch("homeassistant.components.gios.PLATFORMS", [Platform.SENSOR]): - entry = await init_integration(hass) - - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.mark.usefixtures("init_integration") async def test_availability(hass: HomeAssistant) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" - indexes = json.loads(await async_load_fixture(hass, "indexes.json", DOMAIN)) - sensors = json.loads(await async_load_fixture(hass, "sensors.json", DOMAIN)) - - await init_integration(hass) - state = hass.states.get("sensor.home_pm2_5") assert state assert state.state == "4" @@ -49,13 +52,22 @@ async def test_availability(hass: HomeAssistant) -> None: assert state assert state.state == "good" - future = utcnow() + timedelta(minutes=60) - with patch( - "homeassistant.components.gios.coordinator.Gios._get_all_sensors", - side_effect=ApiError("Unexpected error"), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + +@pytest.mark.usefixtures("init_integration") +async def test_availability_api_error( + hass: HomeAssistant, + mock_gios: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state == "4" + + mock_gios.async_update.side_effect = ApiError("Unexpected error") + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() state = hass.states.get("sensor.home_pm2_5") assert state @@ -69,21 +81,16 @@ async def test_availability(hass: HomeAssistant) -> None: assert state assert state.state == STATE_UNAVAILABLE - incomplete_sensors = deepcopy(sensors) - incomplete_sensors["pm2.5"] = {} - future = utcnow() + timedelta(minutes=120) - with ( - patch( - "homeassistant.components.gios.coordinator.Gios._get_all_sensors", - return_value=incomplete_sensors, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_indexes", - return_value={}, - ), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + mock_gios.async_update.side_effect = None + gios_sensors: GiosSensors = mock_gios.async_update.return_value + old_pm25 = gios_sensors.pm25 + old_aqi = gios_sensors.aqi + gios_sensors.pm25 = None + gios_sensors.aqi = None + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() # There is no PM2.5 data so the state should be unavailable state = hass.states.get("sensor.home_pm2_5") @@ -100,19 +107,12 @@ async def test_availability(hass: HomeAssistant) -> None: assert state assert state.state == STATE_UNAVAILABLE - future = utcnow() + timedelta(minutes=180) - with ( - patch( - "homeassistant.components.gios.coordinator.Gios._get_all_sensors", - return_value=sensors, - ), - patch( - "homeassistant.components.gios.coordinator.Gios._get_indexes", - return_value=indexes, - ), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + gios_sensors.pm25 = old_pm25 + gios_sensors.aqi = old_aqi + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() state = hass.states.get("sensor.home_pm2_5") assert state @@ -127,9 +127,46 @@ async def test_availability(hass: HomeAssistant) -> None: assert state.state == "good" -async def test_invalid_indexes(hass: HomeAssistant) -> None: +async def test_dont_create_entities_when_data_missing_for_station( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_gios: MagicMock, + freezer: FrozenDateTimeFactory, + entity_registry: er.EntityRegistry, + mock_gios_sensors: GiosSensors, +) -> None: + """Test that no entities are created when data is missing for the station.""" + mock_gios_sensors.co = None + mock_gios_sensors.no = None + mock_gios_sensors.no2 = None + mock_gios_sensors.nox = None + mock_gios_sensors.o3 = None + mock_gios_sensors.pm10 = None + mock_gios_sensors.pm25 = None + mock_gios_sensors.so2 = None + mock_gios_sensors.aqi = None + mock_gios_sensors.c6h6 = None + + await setup_integration(hass, mock_config_entry) + + assert hass.states.async_entity_ids() == [] + + +async def test_missing_index_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_gios: MagicMock, + mock_gios_sensors: GiosSensors, +) -> None: """Test states of the sensor when API returns invalid indexes.""" - await init_integration(hass, invalid_indexes=True) + mock_gios_sensors.no2.index = None + mock_gios_sensors.o3.index = None + mock_gios_sensors.pm10.index = None + mock_gios_sensors.pm25.index = None + mock_gios_sensors.so2.index = None + mock_gios_sensors.aqi = None + + await setup_integration(hass, mock_config_entry) state = hass.states.get("sensor.home_nitrogen_dioxide_index") assert state @@ -156,18 +193,21 @@ async def test_invalid_indexes(hass: HomeAssistant) -> None: async def test_unique_id_migration( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_gios: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test states of the unique_id migration.""" entity_registry.async_get_or_create( - PLATFORM, + Platform.SENSOR, DOMAIN, "123-pm2.5", suggested_object_id="home_pm2_5", disabled_by=None, ) - await init_integration(hass) + await setup_integration(hass, mock_config_entry) entry = entity_registry.async_get("sensor.home_pm2_5") assert entry diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 03d669c2d38c0..4b889e4bda410 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -3770,6 +3770,10 @@ async def test_subentry_configflow( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "entity" assert result["errors"] == {} + assert "description_placeholders" in result + for placeholder, translation in TRANSLATION_DESCRIPTION_PLACEHOLDERS.items(): + assert placeholder in result["description_placeholders"] + assert result["description_placeholders"][placeholder] == translation # Process entity flow (initial step) diff --git a/tests/components/plugwise/snapshots/test_select.ambr b/tests/components/plugwise/snapshots/test_select.ambr index 90ace520e2d93..c2680f7bcea4a 100644 --- a/tests/components/plugwise/snapshots/test_select.ambr +++ b/tests/components/plugwise/snapshots/test_select.ambr @@ -141,7 +141,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.bathroom_thermostat_schedule', 'has_entity_name': True, 'hidden_by': None, @@ -263,7 +263,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.living_room_thermostat_schedule', 'has_entity_name': True, 'hidden_by': None, @@ -386,7 +386,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.badkamer_thermostat_schedule', 'has_entity_name': True, 'hidden_by': None, @@ -451,7 +451,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.bios_thermostat_schedule', 'has_entity_name': True, 'hidden_by': None, @@ -516,7 +516,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.jessie_thermostat_schedule', 'has_entity_name': True, 'hidden_by': None, @@ -581,7 +581,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.woonkamer_thermostat_schedule', 'has_entity_name': True, 'hidden_by': None, diff --git a/tests/components/plugwise/snapshots/test_sensor.ambr b/tests/components/plugwise/snapshots/test_sensor.ambr index aec9da7f95f2f..7c7715dfd2834 100644 --- a/tests/components/plugwise/snapshots/test_sensor.ambr +++ b/tests/components/plugwise/snapshots/test_sensor.ambr @@ -13,7 +13,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.adam_outdoor_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -69,7 +69,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.anna_setpoint', 'has_entity_name': True, 'hidden_by': None, @@ -125,7 +125,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.anna_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -293,7 +293,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.bathroom_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -455,7 +455,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.emma_setpoint', 'has_entity_name': True, 'hidden_by': None, @@ -511,7 +511,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.emma_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -673,7 +673,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.jip_setpoint', 'has_entity_name': True, 'hidden_by': None, @@ -729,7 +729,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.jip_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -838,7 +838,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.lisa_badkamer_setpoint', 'has_entity_name': True, 'hidden_by': None, @@ -894,7 +894,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.lisa_badkamer_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -1062,7 +1062,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.living_room_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -1283,7 +1283,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.tom_badkamer_setpoint', 'has_entity_name': True, 'hidden_by': None, @@ -1339,7 +1339,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.tom_badkamer_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -1556,7 +1556,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.anna_setpoint', 'has_entity_name': True, 'hidden_by': None, @@ -1612,7 +1612,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.anna_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -2948,7 +2948,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.smile_anna_p1_outdoor_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -3004,7 +3004,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.anna_cooling_setpoint', 'has_entity_name': True, 'hidden_by': None, @@ -3060,7 +3060,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.anna_heating_setpoint', 'has_entity_name': True, 'hidden_by': None, @@ -3169,7 +3169,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , + 'entity_category': None, 'entity_id': 'sensor.anna_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -3613,7 +3613,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.smile_anna_outdoor_temperature', 'has_entity_name': True, 'hidden_by': None, diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 0c8f8a93f65bb..fdb7c1dd74707 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -6,6 +6,7 @@ import aiohttp import pytest +from yarl import URL from homeassistant.components.rest_command import DOMAIN from homeassistant.const import ( @@ -455,3 +456,34 @@ async def test_rest_command_response_iter_chunked( # Verify iter_chunked was called with a chunk size assert mock_iter_chunked.called + + +async def test_rest_command_skip_url_encoding( + hass: HomeAssistant, + setup_component: ComponentSetup, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Check URL encoding.""" + config = { + "skip_url_encoding_test": { + "url": "0%2C", + "method": "get", + "skip_url_encoding": True, + }, + "with_url_encoding_test": { + "url": "1,", + "method": "get", + }, + } + + await setup_component(config) + + aioclient_mock.get(URL("0%2C", encoded=True), content=b"success") + aioclient_mock.get(URL("1,"), content=b"success") + + await hass.services.async_call(DOMAIN, "skip_url_encoding_test", {}, blocking=True) + await hass.services.async_call(DOMAIN, "with_url_encoding_test", {}, blocking=True) + + assert len(aioclient_mock.mock_calls) == 2 + assert str(aioclient_mock.mock_calls[0][1]) == "0%2C" + assert str(aioclient_mock.mock_calls[1][1]) == "1," diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index c3a8be77b7708..687de2ece0b16 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -221,8 +221,8 @@ def match_request(self, method, url, params=None): if ( self._url.scheme != url.scheme - or self._url.host != url.host - or self._url.path != url.path + or self._url.raw_host != url.raw_host + or self._url.raw_path != url.raw_path ): return False