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 <goran.johansson@shiftit.se>
This commit is contained in:
Glenn Waters 2024-07-16 05:57:37 -04:00 committed by GitHub
parent bc7d2d2195
commit 6645932fb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 281 additions and 123 deletions

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["env_canada"], "loggers": ["env_canada"],
"requirements": ["env-canada==0.7.1"] "requirements": ["env-canada==0.7.2"]
} }

View file

@ -2,8 +2,6 @@
from __future__ import annotations from __future__ import annotations
import datetime
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY, ATTR_CONDITION_CLOUDY,
@ -37,7 +35,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from . import device_info from . import device_info
from .const import DOMAIN 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): if not (half_days := ec_data.daily_forecasts):
return None return None
today: Forecast = { def get_day_forecast(fcst: list[dict[str, str]]) -> Forecast:
ATTR_FORECAST_TIME: dt_util.now().isoformat(), high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None
ATTR_FORECAST_CONDITION: icon_code_to_condition( return {
int(half_days[0]["icon_code"]) ATTR_FORECAST_TIME: fcst[0]["timestamp"],
), ATTR_FORECAST_NATIVE_TEMP: high_temp,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]),
half_days[0]["precip_probability"] 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": i = 2 if half_days[0]["temperature_class"] == "high" else 1
today.update( forecast_array.append(get_day_forecast(half_days[0:i]))
{ for i in range(i, len(half_days) - 1, 2):
ATTR_FORECAST_NATIVE_TEMP: int(half_days[0]["temperature"]), forecast_array.append(get_day_forecast(half_days[i : i + 2])) # noqa: PERF401
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"]
),
}
)
else: else:
forecast_array.extend( forecast_array.extend(

View file

@ -813,7 +813,7 @@ enocean==0.50
enturclient==0.2.4 enturclient==0.2.4
# homeassistant.components.environment_canada # homeassistant.components.environment_canada
env-canada==0.7.1 env-canada==0.7.2
# homeassistant.components.season # homeassistant.components.season
ephem==4.1.5 ephem==4.1.5

View file

@ -682,7 +682,7 @@ energyzero==2.1.1
enocean==0.50 enocean==0.50
# homeassistant.components.environment_canada # homeassistant.components.environment_canada
env-canada==0.7.1 env-canada==0.7.2
# homeassistant.components.season # homeassistant.components.season
ephem==4.1.5 ephem==4.1.5

View file

@ -1 +1,67 @@
"""Tests for the Environment Canada integration.""" """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

View file

@ -135,7 +135,8 @@
"icon_code": "30", "icon_code": "30",
"temperature": -1, "temperature": -1,
"temperature_class": "low", "temperature_class": "low",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-03 15:00:00+00:00"
}, },
{ {
"period": "Tuesday", "period": "Tuesday",
@ -143,7 +144,8 @@
"icon_code": "00", "icon_code": "00",
"temperature": 18, "temperature": 18,
"temperature_class": "high", "temperature_class": "high",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-04 15:00:00+00:00"
}, },
{ {
"period": "Tuesday night", "period": "Tuesday night",
@ -151,7 +153,8 @@
"icon_code": "30", "icon_code": "30",
"temperature": 3, "temperature": 3,
"temperature_class": "low", "temperature_class": "low",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-04 15:00:00+00:00"
}, },
{ {
"period": "Wednesday", "period": "Wednesday",
@ -159,7 +162,8 @@
"icon_code": "00", "icon_code": "00",
"temperature": 20, "temperature": 20,
"temperature_class": "high", "temperature_class": "high",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-05 15:00:00+00:00"
}, },
{ {
"period": "Wednesday night", "period": "Wednesday night",
@ -167,7 +171,8 @@
"icon_code": "30", "icon_code": "30",
"temperature": 9, "temperature": 9,
"temperature_class": "low", "temperature_class": "low",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-05 15:00:00+00:00"
}, },
{ {
"period": "Thursday", "period": "Thursday",
@ -175,7 +180,8 @@
"icon_code": "02", "icon_code": "02",
"temperature": 20, "temperature": 20,
"temperature_class": "high", "temperature_class": "high",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-06 15:00:00+00:00"
}, },
{ {
"period": "Thursday night", "period": "Thursday night",
@ -183,7 +189,8 @@
"icon_code": "12", "icon_code": "12",
"temperature": 7, "temperature": 7,
"temperature_class": "low", "temperature_class": "low",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-06 15:00:00+00:00"
}, },
{ {
"period": "Friday", "period": "Friday",
@ -191,7 +198,8 @@
"icon_code": "12", "icon_code": "12",
"temperature": 13, "temperature": 13,
"temperature_class": "high", "temperature_class": "high",
"precip_probability": 40 "precip_probability": 40,
"timestamp": "2022-10-07 15:00:00+00:00"
}, },
{ {
"period": "Friday night", "period": "Friday night",
@ -199,7 +207,8 @@
"icon_code": "32", "icon_code": "32",
"temperature": 1, "temperature": 1,
"temperature_class": "low", "temperature_class": "low",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-07 15:00:00+00:00"
}, },
{ {
"period": "Saturday", "period": "Saturday",
@ -207,7 +216,8 @@
"icon_code": "02", "icon_code": "02",
"temperature": 10, "temperature": 10,
"temperature_class": "high", "temperature_class": "high",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-08 15:00:00+00:00"
}, },
{ {
"period": "Saturday night", "period": "Saturday night",
@ -215,7 +225,8 @@
"icon_code": "32", "icon_code": "32",
"temperature": 3, "temperature": 3,
"temperature_class": "low", "temperature_class": "low",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-08 15:00:00+00:00"
}, },
{ {
"period": "Sunday", "period": "Sunday",
@ -223,7 +234,8 @@
"icon_code": "02", "icon_code": "02",
"temperature": 12, "temperature": 12,
"temperature_class": "high", "temperature_class": "high",
"precip_probability": 0 "precip_probability": 0,
"timestamp": "2022-10-09 15:00:00+00:00"
} }
], ],
"metadata": { "metadata": {

View file

@ -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,
}),
]),
}),
})
# ---

View file

@ -1,16 +1,16 @@
"""Test Environment Canada diagnostics.""" """Test Environment Canada diagnostics."""
from datetime import UTC, datetime
import json import json
from unittest.mock import AsyncMock, MagicMock, patch
from syrupy import SnapshotAssertion 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.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant 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.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator 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( async def test_entry_diagnostics(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
@ -83,7 +29,11 @@ async def test_entry_diagnostics(
) -> None: ) -> None:
"""Test config entry diagnostics.""" """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( diagnostics = await get_diagnostics_for_config_entry(
hass, hass_client, config_entry hass, hass_client, config_entry
) )

View file

@ -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