Skip to content

Commit

Permalink
Improve handling of timezones and daylight savings
Browse files Browse the repository at this point in the history
  • Loading branch information
tronikos committed Mar 29, 2023
1 parent 76dd65d commit b3fdfc0
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 29 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pytest

# Run demo
python src/demo.py --help
# To output debug logs to a file:
# To output debug logs to a file, change DEBUG_LOG_RESPONSE to True in opower.py and run:
python src/demo.py --verbose 2> out.txt

# Build package
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "opower"
version = "0.0.1"
version = "0.0.2"
license = {text = "Apache-2.0"}
authors = [
{ name="tronikos", email="[email protected]" },
Expand All @@ -10,6 +10,7 @@ readme = "README.md"
requires-python = ">=3.7"
dependencies = [
"aiohttp>=3.8",
"arrow>=1.2",
"asyncio>=3.4.3",
]

Expand Down
49 changes: 22 additions & 27 deletions src/opower/opower.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Implementation of opower.com JSON API."""

import dataclasses
from datetime import date, datetime, timedelta
from datetime import date, datetime
from enum import Enum
import json
import logging
Expand All @@ -10,11 +10,13 @@
from urllib.parse import urlencode

import aiohttp
import arrow
from multidict import CIMultiDict

from .utilities import UtilityBase

_LOGGER = logging.getLogger(__file__)
DEBUG_LOG_RESPONSE = False


class MeterType(Enum):
Expand Down Expand Up @@ -128,10 +130,6 @@ def _get_form_action_url_and_hidden_inputs(html: str):
return action_url, inputs


def _strip_time(date_time: datetime):
return date_time.astimezone().replace(hour=0, minute=0, second=0, microsecond=0)


class Opower:
"""Class that can get historical and forecasted usage/cost from an utility."""

Expand Down Expand Up @@ -206,7 +204,8 @@ async def async_get_forecast(self) -> list[Forecast]:
_LOGGER.debug("Fetching: %s", url)
async with self.session.get(url) as resp:
result = await resp.json()
_LOGGER.debug("Fetched: %s", json.dumps(result, indent=2))
if DEBUG_LOG_RESPONSE:
_LOGGER.debug("Fetched: %s", json.dumps(result, indent=2))
forecasts = []
for forecast in result["accountForecasts"]:
forecasts.append(
Expand Down Expand Up @@ -244,7 +243,8 @@ async def _async_get_customer(self):
"/customers/current"
) as resp:
self.customer = await resp.json()
_LOGGER.debug("Fetched: %s", json.dumps(self.customer, indent=2))
if DEBUG_LOG_RESPONSE:
_LOGGER.debug("Fetched: %s", json.dumps(self.customer, indent=2))
assert self.customer
return self.customer

Expand Down Expand Up @@ -335,16 +335,12 @@ async def _async_get_dated_data(
return await self._async_fetch(
url, aggregate_type, start_date, end_date
)
raise ValueError("Missing start_date")

raise ValueError("start_date is required unless aggregate_type=BILL")
if end_date is None:
end_date = datetime.max
end_date = min(datetime.now(), end_date)
raise ValueError("end_date is required unless aggregate_type=BILL")

start_date = _strip_time(start_date)
end_date = _strip_time(end_date) + timedelta(days=1)
assert start_date
assert end_date
start = arrow.get(start_date.date(), self.utility.timezone())
end = arrow.get(end_date.date(), self.utility.timezone()).shift(days=1)

max_request_days = None
if aggregate_type == AggregateType.DAY:
Expand All @@ -353,32 +349,30 @@ async def _async_get_dated_data(
max_request_days = 26

# Fetch data in batches in reverse chronological order
# until we reach start_date or there is no fetched data
# until we reach start or there is no fetched data
# (non bill data are available up to 3 years ago).
result: list[Any] = []
req_end = end_date
req_end = end
while True:
req_start = start_date
if max_request_days:
req_start = max(start_date, req_end - timedelta(days=max_request_days))
req_start = start
if max_request_days is not None:
req_start = max(start, req_end.shift(days=-max_request_days))
if req_start >= req_end:
return result
reads = await self._async_fetch(url, aggregate_type, req_start, req_end)
if not reads:
return result
result = reads + result
req_end = req_start - timedelta(days=1)
req_end = req_start.shift(days=-1)

async def _async_fetch(
self,
url: str,
aggregate_type: AggregateType,
start_date: datetime | None = None,
end_date: datetime | None = None,
start_date: datetime | arrow.Arrow | None = None,
end_date: datetime | arrow.Arrow | None = None,
) -> list[Any]:
convert_to_date = not (
aggregate_type == AggregateType.DAY and "/cws/cost/" in url
)
convert_to_date = "/cws/utilities/" in url
params = {"aggregateType": aggregate_type.value}
if start_date:
params["startDate"] = (
Expand All @@ -391,5 +385,6 @@ async def _async_fetch(
_LOGGER.debug("Fetching: %s?%s", url, urlencode(params))
async with self.session.get(url, params=params) as resp:
result = await resp.json()
_LOGGER.debug("Fetched: %s", json.dumps(result, indent=2))
if DEBUG_LOG_RESPONSE:
_LOGGER.debug("Fetched: %s", json.dumps(result, indent=2))
return result["reads"]
8 changes: 8 additions & 0 deletions src/opower/utilities/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def subdomain() -> str:
"""Return the opower.com subdomain for this utility."""
raise NotImplementedError

@staticmethod
def timezone() -> str:
"""Return the timezone.
Should match the siteTimeZoneId of the API responses.
"""
raise NotImplementedError

@staticmethod
async def login(
session: aiohttp.ClientSession, username: str, password: str
Expand Down
5 changes: 5 additions & 0 deletions src/opower/utilities/pge.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ def subdomain() -> str:
"""Return the opower.com subdomain for this utility."""
return "pge"

@staticmethod
def timezone() -> str:
"""Return the timezone."""
return "America/Los_Angeles"

@staticmethod
async def login(
session: aiohttp.ClientSession, username: str, password: str
Expand Down

0 comments on commit b3fdfc0

Please sign in to comment.