|
10 | 10 |
|
11 | 11 | from datetime import datetime |
12 | 12 |
|
| 13 | +import pytz |
| 14 | + |
| 15 | +from utils import local_midnight |
| 16 | + |
13 | 17 |
|
14 | 18 | def test_fetch_octopus_rates(my_predbat): |
15 | 19 | """ |
@@ -244,6 +248,149 @@ def test_fetch_octopus_rates(my_predbat): |
244 | 248 | else: |
245 | 249 | print("Test 8 passed - sensor with no attributes returns empty dict") |
246 | 250 |
|
| 251 | + # Test 9: DST transition day - mixed timezone offsets in rate data |
| 252 | + print("*** Test 9: DST spring-forward day with mixed +00:00 / +01:00 offsets") |
| 253 | + |
| 254 | + # On UK spring-forward day (last Sunday in March), clocks go from GMT (+00:00) |
| 255 | + # to BST (+01:00) at 01:00 GMT. The Octopus integration delivers rates before |
| 256 | + # the transition with +00:00 and rates after with +01:00. |
| 257 | + # midnight_utc must carry the correct offset (+00:00) so that minute indices |
| 258 | + # are computed correctly for both sets of rates. |
| 259 | + # |
| 260 | + # We simulate "now" at 14:00 BST and derive midnight_utc via |
| 261 | + # local_midnight() to exercise the actual DST-offset fix (midnight was |
| 262 | + # still in GMT +00:00 even though 14:00 is in BST +01:00). |
| 263 | + |
| 264 | + london_tz = pytz.timezone("Europe/London") |
| 265 | + # Simulate "now" at 14:00 BST on spring-forward day (13:00 UTC) |
| 266 | + now_bst = london_tz.localize(datetime(2026, 3, 29, 14, 0, 0)) |
| 267 | + my_predbat.local_tz = london_tz |
| 268 | + my_predbat.midnight_utc = local_midnight(now_bst) |
| 269 | + |
| 270 | + # Verify local_midnight() picked the correct GMT offset for midnight |
| 271 | + assert my_predbat.midnight_utc.utcoffset().total_seconds() == 0, "midnight_utc should have +00:00 offset on spring-forward day, got {}".format(my_predbat.midnight_utc.utcoffset()) |
| 272 | + assert my_predbat.midnight_utc.hour == 0 and my_predbat.midnight_utc.minute == 0, "midnight_utc should be 00:00, got {}".format(my_predbat.midnight_utc) |
| 273 | + |
| 274 | + my_predbat.forecast_days = 2 |
| 275 | + |
| 276 | + entity_id_dst = "sensor.metric_octopus_import_dst" |
| 277 | + dst_rates = [ |
| 278 | + # Pre-DST rates (GMT, +00:00) |
| 279 | + {"start": "2026-03-29T00:00:00+00:00", "end": "2026-03-29T00:30:00+00:00", "value": 0.04138}, |
| 280 | + {"start": "2026-03-29T00:30:00+00:00", "end": "2026-03-29T01:00:00+00:00", "value": 0.04135}, |
| 281 | + # Post-DST rates (BST, +01:00) — 02:00 BST = 01:00 UTC |
| 282 | + {"start": "2026-03-29T02:00:00+01:00", "end": "2026-03-29T02:30:00+01:00", "value": 0.04000}, |
| 283 | + {"start": "2026-03-29T02:30:00+01:00", "end": "2026-03-29T03:00:00+01:00", "value": 0.03991}, |
| 284 | + ] |
| 285 | + |
| 286 | + my_predbat.ha_interface.dummy_items[entity_id_dst] = { |
| 287 | + "state": "0.04", |
| 288 | + "raw_today": dst_rates, |
| 289 | + } |
| 290 | + |
| 291 | + rate_data = my_predbat.fetch_octopus_rates(entity_id_dst) |
| 292 | + |
| 293 | + if not rate_data: |
| 294 | + print("ERROR: No rate data returned for DST test") |
| 295 | + failed = True |
| 296 | + else: |
| 297 | + # 00:00+00:00 is minute 0 from midnight |
| 298 | + expected_min0 = 0.04138 * 100 # scale=100 for start/end format |
| 299 | + if 0 not in rate_data: |
| 300 | + print("ERROR: DST test - missing rate at minute 0") |
| 301 | + failed = True |
| 302 | + elif abs(rate_data[0] - expected_min0) > 0.01: |
| 303 | + print("ERROR: DST test - minute 0 expected {}, got {}".format(expected_min0, rate_data[0])) |
| 304 | + failed = True |
| 305 | + |
| 306 | + # 02:00+01:00 = 01:00 UTC = minute 60 from midnight (+00:00) |
| 307 | + expected_min60 = 0.04000 * 100 |
| 308 | + if 60 not in rate_data: |
| 309 | + print("ERROR: DST test - missing rate at minute 60 (02:00 BST = 01:00 UTC)") |
| 310 | + failed = True |
| 311 | + elif abs(rate_data[60] - expected_min60) > 0.01: |
| 312 | + print("ERROR: DST test - minute 60 expected {}, got {}".format(expected_min60, rate_data[60])) |
| 313 | + failed = True |
| 314 | + |
| 315 | + # 02:30+01:00 = 01:30 UTC = minute 90 from midnight (+00:00) |
| 316 | + expected_min90 = 0.03991 * 100 |
| 317 | + if 90 not in rate_data: |
| 318 | + print("ERROR: DST test - missing rate at minute 90 (02:30 BST = 01:30 UTC)") |
| 319 | + failed = True |
| 320 | + elif abs(rate_data[90] - expected_min90) > 0.01: |
| 321 | + print("ERROR: DST test - minute 90 expected {}, got {}".format(expected_min90, rate_data[90])) |
| 322 | + failed = True |
| 323 | + |
| 324 | + if 0 in rate_data and 60 in rate_data and 90 in rate_data: |
| 325 | + print("Test 9 passed - DST mixed-offset rates placed at correct minutes") |
| 326 | + |
| 327 | + # Test 10: DST autumn fallback day - mixed timezone offsets in rate data |
| 328 | + print("*** Test 10: DST fall-back day with mixed +01:00 / +00:00 offsets") |
| 329 | + |
| 330 | + # On UK fall-back day (last Sunday in October), clocks go from BST (+01:00) |
| 331 | + # to GMT (+00:00). Midnight is still in BST (+01:00), but later daytime is |
| 332 | + # in GMT (+00:00). |
| 333 | + # |
| 334 | + # We simulate "now" at 14:00 GMT and derive midnight_utc via |
| 335 | + # local_midnight() to verify it picks +01:00 for local midnight. |
| 336 | + now_gmt = london_tz.localize(datetime(2026, 10, 25, 14, 0, 0)) |
| 337 | + my_predbat.midnight_utc = local_midnight(now_gmt) |
| 338 | + |
| 339 | + # Verify local_midnight() picked the correct BST offset for midnight |
| 340 | + assert my_predbat.midnight_utc.utcoffset().total_seconds() == 3600, "midnight_utc should have +01:00 offset on fall-back day, got {}".format(my_predbat.midnight_utc.utcoffset()) |
| 341 | + assert my_predbat.midnight_utc.hour == 0 and my_predbat.midnight_utc.minute == 0, "midnight_utc should be 00:00, got {}".format(my_predbat.midnight_utc) |
| 342 | + |
| 343 | + entity_id_dst_fall = "sensor.metric_octopus_import_dst_fall" |
| 344 | + dst_fall_rates = [ |
| 345 | + # Pre-fallback rates (BST, +01:00) |
| 346 | + {"start": "2026-10-25T00:00:00+01:00", "end": "2026-10-25T00:30:00+01:00", "value": 0.05000}, |
| 347 | + {"start": "2026-10-25T00:30:00+01:00", "end": "2026-10-25T01:00:00+01:00", "value": 0.04900}, |
| 348 | + # Post-fallback rates (GMT, +00:00) — 01:00 GMT = minute 120 from midnight (+01:00) |
| 349 | + {"start": "2026-10-25T01:00:00+00:00", "end": "2026-10-25T01:30:00+00:00", "value": 0.04800}, |
| 350 | + {"start": "2026-10-25T01:30:00+00:00", "end": "2026-10-25T02:00:00+00:00", "value": 0.04700}, |
| 351 | + ] |
| 352 | + |
| 353 | + my_predbat.ha_interface.dummy_items[entity_id_dst_fall] = { |
| 354 | + "state": "0.05", |
| 355 | + "raw_today": dst_fall_rates, |
| 356 | + } |
| 357 | + |
| 358 | + rate_data = my_predbat.fetch_octopus_rates(entity_id_dst_fall) |
| 359 | + |
| 360 | + if not rate_data: |
| 361 | + print("ERROR: No rate data returned for DST fall-back test") |
| 362 | + failed = True |
| 363 | + else: |
| 364 | + # 00:00+01:00 is minute 0 from midnight |
| 365 | + expected_min0 = 0.05000 * 100 |
| 366 | + if 0 not in rate_data: |
| 367 | + print("ERROR: DST fall-back test - missing rate at minute 0") |
| 368 | + failed = True |
| 369 | + elif abs(rate_data[0] - expected_min0) > 0.01: |
| 370 | + print("ERROR: DST fall-back test - minute 0 expected {}, got {}".format(expected_min0, rate_data[0])) |
| 371 | + failed = True |
| 372 | + |
| 373 | + # 01:00+00:00 = 01:00 UTC = minute 120 from midnight (+01:00) |
| 374 | + expected_min120 = 0.04800 * 100 |
| 375 | + if 120 not in rate_data: |
| 376 | + print("ERROR: DST fall-back test - missing rate at minute 120 (01:00 GMT = 120 mins from 00:00 BST)") |
| 377 | + failed = True |
| 378 | + elif abs(rate_data[120] - expected_min120) > 0.01: |
| 379 | + print("ERROR: DST fall-back test - minute 120 expected {}, got {}".format(expected_min120, rate_data[120])) |
| 380 | + failed = True |
| 381 | + |
| 382 | + # 01:30+00:00 = 01:30 UTC = minute 150 from midnight (+01:00) |
| 383 | + expected_min150 = 0.04700 * 100 |
| 384 | + if 150 not in rate_data: |
| 385 | + print("ERROR: DST fall-back test - missing rate at minute 150 (01:30 GMT = 150 mins from 00:00 BST)") |
| 386 | + failed = True |
| 387 | + elif abs(rate_data[150] - expected_min150) > 0.01: |
| 388 | + print("ERROR: DST fall-back test - minute 150 expected {}, got {}".format(expected_min150, rate_data[150])) |
| 389 | + failed = True |
| 390 | + |
| 391 | + if 0 in rate_data and 120 in rate_data and 150 in rate_data: |
| 392 | + print("Test 10 passed - DST fall-back mixed-offset rates placed at correct minutes") |
| 393 | + |
247 | 394 | # Restore original values |
248 | 395 | my_predbat.forecast_days = old_forecast_days |
249 | 396 | my_predbat.midnight_utc = old_midnight_utc |
|
0 commit comments