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",
"iot_class": "cloud_polling",
"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
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(

View file

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

View file

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

View file

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

View file

@ -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": {

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."""
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
)

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