From 6645932fb7a98bd89208e1810df4071c7f743ff8 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Tue, 16 Jul 2024 05:57:37 -0400 Subject: [PATCH] Fix for Environment Canada date being wrong after midnight (#121850) * Use async_connect in newly bumped 0.5.8 UPB library. * Fix tests. * Fix date being wrong after midnight for Environment Canada * Fix typing. * Add test. * Formatting. * Remove tests until can be added properly. * Add weather tests back. * Fix tests * Change of tactic for determining previous day's data. --------- Co-authored-by: G Johansson --- .../environment_canada/manifest.json | 2 +- .../components/environment_canada/weather.py | 66 ++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/environment_canada/__init__.py | 66 +++++++++++++ .../fixtures/current_conditions_data.json | 36 ++++--- .../snapshots/test_weather.ambr | 94 +++++++++++++++++++ .../environment_canada/test_diagnostics.py | 68 ++------------ .../environment_canada/test_weather.py | 68 ++++++++++++++ 9 files changed, 281 insertions(+), 123 deletions(-) create mode 100644 tests/components/environment_canada/snapshots/test_weather.ambr create mode 100644 tests/components/environment_canada/test_weather.py diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index c77d35b1769..76534662ff7 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.7.1"] + "requirements": ["env-canada==0.7.2"] } diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index a3036c55659..2d54a313dde 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -2,8 +2,6 @@ from __future__ import annotations -import datetime - from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -37,7 +35,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import dt as dt_util from . import device_info from .const import DOMAIN @@ -193,53 +190,24 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None: if not (half_days := ec_data.daily_forecasts): return None - today: Forecast = { - ATTR_FORECAST_TIME: dt_util.now().isoformat(), - ATTR_FORECAST_CONDITION: icon_code_to_condition( - int(half_days[0]["icon_code"]) - ), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( - half_days[0]["precip_probability"] - ), - } + def get_day_forecast(fcst: list[dict[str, str]]) -> Forecast: + high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None + return { + ATTR_FORECAST_TIME: fcst[0]["timestamp"], + ATTR_FORECAST_NATIVE_TEMP: high_temp, + ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]), + ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( + fcst[0]["precip_probability"] + ), + ATTR_FORECAST_CONDITION: icon_code_to_condition( + int(fcst[0]["icon_code"]) + ), + } - if half_days[0]["temperature_class"] == "high": - today.update( - { - ATTR_FORECAST_NATIVE_TEMP: int(half_days[0]["temperature"]), - ATTR_FORECAST_NATIVE_TEMP_LOW: int(half_days[1]["temperature"]), - } - ) - half_days = half_days[2:] - else: - today.update( - { - ATTR_FORECAST_NATIVE_TEMP: None, - ATTR_FORECAST_NATIVE_TEMP_LOW: int(half_days[0]["temperature"]), - } - ) - half_days = half_days[1:] - - forecast_array.append(today) - - for day, high, low in zip( - range(1, 6), range(0, 9, 2), range(1, 10, 2), strict=False - ): - forecast_array.append( - { - ATTR_FORECAST_TIME: ( - dt_util.now() + datetime.timedelta(days=day) - ).isoformat(), - ATTR_FORECAST_NATIVE_TEMP: int(half_days[high]["temperature"]), - ATTR_FORECAST_NATIVE_TEMP_LOW: int(half_days[low]["temperature"]), - ATTR_FORECAST_CONDITION: icon_code_to_condition( - int(half_days[high]["icon_code"]) - ), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( - half_days[high]["precip_probability"] - ), - } - ) + i = 2 if half_days[0]["temperature_class"] == "high" else 1 + forecast_array.append(get_day_forecast(half_days[0:i])) + for i in range(i, len(half_days) - 1, 2): + forecast_array.append(get_day_forecast(half_days[i : i + 2])) # noqa: PERF401 else: forecast_array.extend( diff --git a/requirements_all.txt b/requirements_all.txt index 70dc90eed6f..c3d9d0f72d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -813,7 +813,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.7.1 +env-canada==0.7.2 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 568528951d5..2b16427cb0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -682,7 +682,7 @@ energyzero==2.1.1 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.7.1 +env-canada==0.7.2 # homeassistant.components.season ephem==4.1.5 diff --git a/tests/components/environment_canada/__init__.py b/tests/components/environment_canada/__init__.py index 65b0ed16207..92c28e09b74 100644 --- a/tests/components/environment_canada/__init__.py +++ b/tests/components/environment_canada/__init__.py @@ -1 +1,67 @@ """Tests for the Environment Canada integration.""" + +from datetime import UTC, datetime +from unittest.mock import AsyncMock, MagicMock, patch + +from homeassistant.components.environment_canada.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +FIXTURE_USER_INPUT = { + CONF_LATITUDE: 55.55, + CONF_LONGITUDE: 42.42, + CONF_STATION: "XX/1234567", + CONF_LANGUAGE: "Gibberish", +} + + +async def init_integration(hass: HomeAssistant, ec_data) -> MockConfigEntry: + """Set up the Environment Canada integration in Home Assistant.""" + + def mock_ec(): + ec_mock = MagicMock() + ec_mock.station_id = FIXTURE_USER_INPUT[CONF_STATION] + ec_mock.lat = FIXTURE_USER_INPUT[CONF_LATITUDE] + ec_mock.lon = FIXTURE_USER_INPUT[CONF_LONGITUDE] + ec_mock.language = FIXTURE_USER_INPUT[CONF_LANGUAGE] + ec_mock.update = AsyncMock() + return ec_mock + + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT, title="Home") + config_entry.add_to_hass(hass) + + weather_mock = mock_ec() + ec_data["metadata"]["timestamp"] = datetime(2022, 10, 4, tzinfo=UTC) + weather_mock.conditions = ec_data["conditions"] + weather_mock.alerts = ec_data["alerts"] + weather_mock.daily_forecasts = ec_data["daily_forecasts"] + weather_mock.metadata = ec_data["metadata"] + + radar_mock = mock_ec() + radar_mock.image = b"GIF..." + radar_mock.timestamp = datetime(2022, 10, 4, tzinfo=UTC) + + with ( + patch( + "homeassistant.components.environment_canada.ECWeather", + return_value=weather_mock, + ), + patch( + "homeassistant.components.environment_canada.ECAirQuality", + return_value=mock_ec(), + ), + patch( + "homeassistant.components.environment_canada.ECRadar", + return_value=radar_mock, + ), + patch( + "homeassistant.components.environment_canada.config_flow.ECWeather", + return_value=weather_mock, + ), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/environment_canada/fixtures/current_conditions_data.json b/tests/components/environment_canada/fixtures/current_conditions_data.json index f3a18869940..ceb00028f95 100644 --- a/tests/components/environment_canada/fixtures/current_conditions_data.json +++ b/tests/components/environment_canada/fixtures/current_conditions_data.json @@ -135,7 +135,8 @@ "icon_code": "30", "temperature": -1, "temperature_class": "low", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-03 15:00:00+00:00" }, { "period": "Tuesday", @@ -143,7 +144,8 @@ "icon_code": "00", "temperature": 18, "temperature_class": "high", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-04 15:00:00+00:00" }, { "period": "Tuesday night", @@ -151,7 +153,8 @@ "icon_code": "30", "temperature": 3, "temperature_class": "low", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-04 15:00:00+00:00" }, { "period": "Wednesday", @@ -159,7 +162,8 @@ "icon_code": "00", "temperature": 20, "temperature_class": "high", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-05 15:00:00+00:00" }, { "period": "Wednesday night", @@ -167,7 +171,8 @@ "icon_code": "30", "temperature": 9, "temperature_class": "low", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-05 15:00:00+00:00" }, { "period": "Thursday", @@ -175,7 +180,8 @@ "icon_code": "02", "temperature": 20, "temperature_class": "high", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-06 15:00:00+00:00" }, { "period": "Thursday night", @@ -183,7 +189,8 @@ "icon_code": "12", "temperature": 7, "temperature_class": "low", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-06 15:00:00+00:00" }, { "period": "Friday", @@ -191,7 +198,8 @@ "icon_code": "12", "temperature": 13, "temperature_class": "high", - "precip_probability": 40 + "precip_probability": 40, + "timestamp": "2022-10-07 15:00:00+00:00" }, { "period": "Friday night", @@ -199,7 +207,8 @@ "icon_code": "32", "temperature": 1, "temperature_class": "low", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-07 15:00:00+00:00" }, { "period": "Saturday", @@ -207,7 +216,8 @@ "icon_code": "02", "temperature": 10, "temperature_class": "high", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-08 15:00:00+00:00" }, { "period": "Saturday night", @@ -215,7 +225,8 @@ "icon_code": "32", "temperature": 3, "temperature_class": "low", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-08 15:00:00+00:00" }, { "period": "Sunday", @@ -223,7 +234,8 @@ "icon_code": "02", "temperature": 12, "temperature_class": "high", - "precip_probability": 0 + "precip_probability": 0, + "timestamp": "2022-10-09 15:00:00+00:00" } ], "metadata": { diff --git a/tests/components/environment_canada/snapshots/test_weather.ambr b/tests/components/environment_canada/snapshots/test_weather.ambr new file mode 100644 index 00000000000..7ba37110c2a --- /dev/null +++ b/tests/components/environment_canada/snapshots/test_weather.ambr @@ -0,0 +1,94 @@ +# serializer version: 1 +# name: test_forecast_daily + dict({ + 'weather.home_forecast': dict({ + 'forecast': list([ + dict({ + 'condition': 'sunny', + 'datetime': '2022-10-04 15:00:00+00:00', + 'precipitation_probability': 0, + 'temperature': 18.0, + 'templow': 3.0, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2022-10-05 15:00:00+00:00', + 'precipitation_probability': 0, + 'temperature': 20.0, + 'templow': 9.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2022-10-06 15:00:00+00:00', + 'precipitation_probability': 0, + 'temperature': 20.0, + 'templow': 7.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2022-10-07 15:00:00+00:00', + 'precipitation_probability': 40, + 'temperature': 13.0, + 'templow': 1.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2022-10-08 15:00:00+00:00', + 'precipitation_probability': 0, + 'temperature': 10.0, + 'templow': 3.0, + }), + ]), + }), + }) +# --- +# name: test_forecast_daily_with_some_previous_days_data + dict({ + 'weather.home_forecast': dict({ + 'forecast': list([ + dict({ + 'condition': 'clear-night', + 'datetime': '2022-10-03 15:00:00+00:00', + 'precipitation_probability': 0, + 'temperature': None, + 'templow': -1.0, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2022-10-04 15:00:00+00:00', + 'precipitation_probability': 0, + 'temperature': 18.0, + 'templow': 3.0, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2022-10-05 15:00:00+00:00', + 'precipitation_probability': 0, + 'temperature': 20.0, + 'templow': 9.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2022-10-06 15:00:00+00:00', + 'precipitation_probability': 0, + 'temperature': 20.0, + 'templow': 7.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2022-10-07 15:00:00+00:00', + 'precipitation_probability': 40, + 'temperature': 13.0, + 'templow': 1.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2022-10-08 15:00:00+00:00', + 'precipitation_probability': 0, + 'temperature': 10.0, + 'templow': 3.0, + }), + ]), + }), + }) +# --- diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py index 8f800111d39..7e9c8691f90 100644 --- a/tests/components/environment_canada/test_diagnostics.py +++ b/tests/components/environment_canada/test_diagnostics.py @@ -1,16 +1,16 @@ """Test Environment Canada diagnostics.""" -from datetime import UTC, datetime import json -from unittest.mock import AsyncMock, MagicMock, patch from syrupy import SnapshotAssertion -from homeassistant.components.environment_canada.const import CONF_STATION, DOMAIN +from homeassistant.components.environment_canada.const import CONF_STATION from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from . import init_integration + +from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -22,60 +22,6 @@ FIXTURE_USER_INPUT = { } -async def init_integration(hass: HomeAssistant) -> MockConfigEntry: - """Set up the Environment Canada integration in Home Assistant.""" - - def mock_ec(): - ec_mock = MagicMock() - ec_mock.station_id = FIXTURE_USER_INPUT[CONF_STATION] - ec_mock.lat = FIXTURE_USER_INPUT[CONF_LATITUDE] - ec_mock.lon = FIXTURE_USER_INPUT[CONF_LONGITUDE] - ec_mock.language = FIXTURE_USER_INPUT[CONF_LANGUAGE] - ec_mock.update = AsyncMock() - return ec_mock - - config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) - config_entry.add_to_hass(hass) - - ec_data = json.loads( - load_fixture("environment_canada/current_conditions_data.json") - ) - - weather_mock = mock_ec() - ec_data["metadata"]["timestamp"] = datetime(2022, 10, 4, tzinfo=UTC) - weather_mock.conditions = ec_data["conditions"] - weather_mock.alerts = ec_data["alerts"] - weather_mock.daily_forecasts = ec_data["daily_forecasts"] - weather_mock.metadata = ec_data["metadata"] - - radar_mock = mock_ec() - radar_mock.image = b"GIF..." - radar_mock.timestamp = datetime(2022, 10, 4, tzinfo=UTC) - - with ( - patch( - "homeassistant.components.environment_canada.ECWeather", - return_value=weather_mock, - ), - patch( - "homeassistant.components.environment_canada.ECAirQuality", - return_value=mock_ec(), - ), - patch( - "homeassistant.components.environment_canada.ECRadar", - return_value=radar_mock, - ), - patch( - "homeassistant.components.environment_canada.config_flow.ECWeather", - return_value=weather_mock, - ), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -83,7 +29,11 @@ async def test_entry_diagnostics( ) -> None: """Test config entry diagnostics.""" - config_entry = await init_integration(hass) + ec_data = json.loads( + load_fixture("environment_canada/current_conditions_data.json") + ) + + config_entry = await init_integration(hass, ec_data) diagnostics = await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) diff --git a/tests/components/environment_canada/test_weather.py b/tests/components/environment_canada/test_weather.py new file mode 100644 index 00000000000..e8c21e2dc06 --- /dev/null +++ b/tests/components/environment_canada/test_weather.py @@ -0,0 +1,68 @@ +"""Test weather.""" + +import json + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.weather import ( + DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECASTS, +) +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import load_fixture + + +async def test_forecast_daily( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test basic forecast.""" + + ec_data = json.loads( + load_fixture("environment_canada/current_conditions_data.json") + ) + + # First entry in test data is a half day; we don't want that for this test + del ec_data["daily_forecasts"][0] + + await init_integration(hass, ec_data) + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECASTS, + { + "entity_id": "weather.home_forecast", + "type": "daily", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + + +async def test_forecast_daily_with_some_previous_days_data( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test forecast with half day at start.""" + + ec_data = json.loads( + load_fixture("environment_canada/current_conditions_data.json") + ) + + await init_integration(hass, ec_data) + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECASTS, + { + "entity_id": "weather.home_forecast", + "type": "daily", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot