Skip to content

Commit

Permalink
fix: back off for 6 hours after rate limit error (#84)
Browse files Browse the repository at this point in the history
* fix: back off for 6 hours after rate limit error

* use new library, remove options flow

* linting

* more linting

* update tests to py3.13

* udpate test

* update test
  • Loading branch information
firstof9 authored Dec 6, 2024
1 parent f3ee39b commit 281bfa9
Show file tree
Hide file tree
Showing 14 changed files with 711 additions and 620 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
python-version:
# - "3.10"
# - "3.11"
- "3.12"
- "3.13"

steps:
- name: 📥 Checkout the repository
Expand Down
6 changes: 6 additions & 0 deletions .tox/py310/.tox-info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ToxEnv": {
"name": "py310",
"type": "VirtualEnvRunner"
}
}
6 changes: 6 additions & 0 deletions .tox/py311/.tox-info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ToxEnv": {
"name": "py311",
"type": "VirtualEnvRunner"
}
}
121 changes: 66 additions & 55 deletions custom_components/openei/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
self.hass = hass
self.interval = timedelta(seconds=30)
self._data = {}
self._rate_limit_count = 0

_LOGGER.debug("Data will be updated at the top of every hour.")

Expand All @@ -99,12 +100,11 @@ async def _async_update_data(self) -> dict:
_LOGGER.debug("Next update in %s seconds.", wait_seconds)
async_call_later(self.hass, wait_seconds, self._async_refresh_data)
try:
self._data = await self.hass.async_add_executor_job(
get_sensors, self.hass, self._config
)
await self.get_sensors()
except openeihttp.RateLimit:
_LOGGER.error("API Rate limit exceded, retrying later.")
self._data = {}
pass
except AssertionError:
pass
except Exception as exception:
raise UpdateFailed() from exception
return self._data
Expand All @@ -120,60 +120,71 @@ async def _async_refresh_data(self, data=None) -> None:
_LOGGER.debug("Next update in %s seconds.", wait_seconds)
async_call_later(self.hass, wait_seconds, self._async_refresh_data)
try:
self._data = await self.hass.async_add_executor_job(
get_sensors, self.hass, self._config
)
await self.get_sensors()
except openeihttp.RateLimit:
_LOGGER.error("API Rate limit exceded, retrying later.")
self._data = {}
pass
except AssertionError:
pass
except Exception as exception:
raise UpdateFailed() from exception


def get_sensors(hass, config) -> dict:
"""Update sensor data."""
api = config.data.get(CONF_API_KEY)
plan = config.data.get(CONF_PLAN)
meter = config.data.get(CONF_SENSOR)
reading = None

if config.data.get(CONF_MANUAL_PLAN):
plan = config.data.get(CONF_MANUAL_PLAN)

if meter:
_LOGGER.debug("Using meter data from sensor: %s", meter)
reading = hass.states.get(meter)
if not reading:
reading = None
_LOGGER.warning("Sensor: %s is not valid.", meter)
else:
reading = reading.state

rate = openeihttp.Rates(
api=api,
plan=plan,
reading=reading,
)
rate.update()
data = {}

for sensor in SENSOR_TYPES: # pylint: disable=consider-using-dict-items
_sensor = {}
value = getattr(rate, SENSOR_TYPES[sensor].key)
if isinstance(value, tuple):
_sensor[sensor] = value[0]
_sensor[f"{sensor}_uom"] = value[1]
else:
_sensor[sensor] = getattr(rate, SENSOR_TYPES[sensor].key)
data.update(_sensor)

for sensor in BINARY_SENSORS: # pylint: disable=consider-using-dict-items
_sensor = {}
_sensor[sensor] = getattr(rate, sensor)
data.update(_sensor)

_LOGGER.debug("DEBUG: %s", data)
return data
async def get_sensors(self) -> dict:
"""Update sensor data."""
api = self._config.data.get(CONF_API_KEY)
plan = self._config.data.get(CONF_PLAN)
meter = self._config.data.get(CONF_SENSOR)
reading = None

if self._config.data.get(CONF_MANUAL_PLAN):
plan = self._config.data.get(CONF_MANUAL_PLAN)

if meter:
_LOGGER.debug("Using meter data from sensor: %s", meter)
reading = self.hass.states.get(meter)
if not reading:
reading = None
_LOGGER.warning("Sensor: %s is not valid.", meter)
else:
reading = reading.state

rate = openeihttp.Rates(
api=api,
plan=plan,
reading=reading,
)
if self._rate_limit_count == 0:
try:
await rate.update()
except openeihttp.RateLimit:
_LOGGER.error("API Rate limit exceded, retrying later.")
if not self._data:
# 3 hour retry if we have no data
self._rate_limit_count = 3
else:
# 6 hour retry after rate limited
self._rate_limit_count = 6
elif self._rate_limit_count > 0:
self._rate_limit_count -= 1

data = {}

for sensor in SENSOR_TYPES: # pylint: disable=consider-using-dict-items
_sensor = {}
value = getattr(rate, SENSOR_TYPES[sensor].key)
if isinstance(value, tuple):
_sensor[sensor] = value[0]
_sensor[f"{sensor}_uom"] = value[1]
else:
_sensor[sensor] = getattr(rate, SENSOR_TYPES[sensor].key)
data.update(_sensor)

for sensor in BINARY_SENSORS: # pylint: disable=consider-using-dict-items
_sensor = {}
_sensor[sensor] = getattr(rate, sensor)
data.update(_sensor)

_LOGGER.debug("DEBUG: %s", data)
self._data = data


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Expand Down
160 changes: 80 additions & 80 deletions custom_components/openei/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.sensor import DOMAIN as SENSORS_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant

from .const import (
CONF_API_KEY,
Expand Down Expand Up @@ -68,11 +68,11 @@ async def async_step_user_3(self, user_input=None):

return await self._show_config_form_3(user_input)

@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Enable option flow."""
return OpenEIOptionsFlowHandler(config_entry)
# @staticmethod
# @callback
# def async_get_options_flow(config_entry):
# """Enable option flow."""
# return OpenEIOptionsFlowHandler(config_entry)

async def _show_config_form(self, user_input): # pylint: disable=unused-argument
"""Show the configuration form to edit location data."""
Expand Down Expand Up @@ -104,76 +104,76 @@ async def _show_config_form_3(self, user_input): # pylint: disable=unused-argum
)


class OpenEIOptionsFlowHandler(config_entries.OptionsFlow):
"""Blueprint config flow options handler."""

def __init__(self, config_entry):
"""Initialize OpenEI options flow."""
self.config_entry = config_entry
self._data = dict(config_entry.data)
self._errors = {}

async def async_step_init(self, user_input=None): # pylint: disable=unused-argument
"""Manage the options."""
return await self.async_step_user()

async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
if user_input[CONF_LOCATION] == '""':
user_input[CONF_LOCATION] = ""
self._data.update(user_input)
return await self.async_step_user_2()

return await self._show_config_form(user_input)

async def async_step_user_2(self, user_input=None):
"""Handle a flow initialized by the user."""
_LOGGER.debug("data: %s", self._data)
if user_input is not None:
self._data.update(user_input)
return await self.async_step_user_3()

return await self._show_config_form_2(user_input)

async def async_step_user_3(self, user_input=None):
"""Handle a flow initialized by the user."""
_LOGGER.debug("data: %s", self._data)
if user_input is not None:
if user_input[CONF_MANUAL_PLAN] == '""':
user_input[CONF_MANUAL_PLAN] = ""
self._data.update(user_input)
return self.async_create_entry(title="", data=self._data)

return await self._show_config_form_3(user_input)

async def _show_config_form(self, user_input: Optional[Dict[str, Any]]):
"""Show the configuration form to edit location data."""
return self.async_show_form(
step_id="user",
data_schema=_get_schema_step_1(user_input, self._data),
errors=self._errors,
)

async def _show_config_form_2(self, user_input: Optional[Dict[str, Any]]):
"""Show the configuration form to edit location data."""
utility_list = await _get_utility_list(self.hass, self._data)
return self.async_show_form(
step_id="user_2",
data_schema=_get_schema_step_2(user_input, self._data, utility_list),
errors=self._errors,
)

async def _show_config_form_3(self, user_input: Optional[Dict[str, Any]]):
"""Show the configuration form to edit location data."""
plan_list = await _get_plan_list(self.hass, self._data)
return self.async_show_form(
step_id="user_3",
data_schema=_get_schema_step_3(
self.hass, user_input, self._data, plan_list
),
errors=self._errors,
)
# class OpenEIOptionsFlowHandler(config_entries.OptionsFlow):
# """Blueprint config flow options handler."""

# def __init__(self, config_entry):
# """Initialize OpenEI options flow."""
# self.config_entry = config_entry
# self._data = dict(config_entry.data)
# self._errors = {}

# async def async_step_init(self, user_input=None): # pylint: disable=unused-argument
# """Manage the options."""
# return await self.async_step_user()

# async def async_step_user(self, user_input=None):
# """Handle a flow initialized by the user."""
# if user_input is not None:
# if user_input[CONF_LOCATION] == '""':
# user_input[CONF_LOCATION] = ""
# self._data.update(user_input)
# return await self.async_step_user_2()

# return await self._show_config_form(user_input)

# async def async_step_user_2(self, user_input=None):
# """Handle a flow initialized by the user."""
# _LOGGER.debug("data: %s", self._data)
# if user_input is not None:
# self._data.update(user_input)
# return await self.async_step_user_3()

# return await self._show_config_form_2(user_input)

# async def async_step_user_3(self, user_input=None):
# """Handle a flow initialized by the user."""
# _LOGGER.debug("data: %s", self._data)
# if user_input is not None:
# if user_input[CONF_MANUAL_PLAN] == '""':
# user_input[CONF_MANUAL_PLAN] = ""
# self._data.update(user_input)
# return self.async_create_entry(title="", data=self._data)

# return await self._show_config_form_3(user_input)

# async def _show_config_form(self, user_input: Optional[Dict[str, Any]]):
# """Show the configuration form to edit location data."""
# return self.async_show_form(
# step_id="user",
# data_schema=_get_schema_step_1(user_input, self._data),
# errors=self._errors,
# )

# async def _show_config_form_2(self, user_input: Optional[Dict[str, Any]]):
# """Show the configuration form to edit location data."""
# utility_list = await _get_utility_list(self.hass, self._data)
# return self.async_show_form(
# step_id="user_2",
# data_schema=_get_schema_step_2(user_input, self._data, utility_list),
# errors=self._errors,
# )

# async def _show_config_form_3(self, user_input: Optional[Dict[str, Any]]):
# """Show the configuration form to edit location data."""
# plan_list = await _get_plan_list(self.hass, self._data)
# return self.async_show_form(
# step_id="user_3",
# data_schema=_get_schema_step_3(
# self.hass, user_input, self._data, plan_list
# ),
# errors=self._errors,
# )


def _get_schema_step_1(
Expand Down Expand Up @@ -271,7 +271,7 @@ async def _get_utility_list(hass, user_input) -> list | None:
address = None

plans = openeihttp.Rates(api=api, lat=lat, lon=lon, radius=radius, address=address)
plans = await hass.async_add_executor_job(_lookup_plans, plans)
plans = await _lookup_plans(plans)
utilities = []

for utility in plans:
Expand All @@ -296,7 +296,7 @@ async def _get_plan_list(hass, user_input) -> list | None:
address = None

plans = openeihttp.Rates(api=api, lat=lat, lon=lon, radius=radius, address=address)
plans = await hass.async_add_executor_job(_lookup_plans, plans)
plans = await _lookup_plans(plans)
value = {}

for plan in plans[utility]:
Expand All @@ -306,9 +306,9 @@ async def _get_plan_list(hass, user_input) -> list | None:
return value


def _lookup_plans(handler) -> list:
async def _lookup_plans(handler) -> list:
"""Return list of utilities and plans."""
response = handler.lookup_plans()
response = await handler.lookup_plans()
_LOGGER.debug("lookup_plans: %s", response)
return response

Expand Down
2 changes: 1 addition & 1 deletion custom_components/openei/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"documentation": "https://github.com/firstof9/ha-openei",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/firstof9/ha-openei/issues",
"requirements": ["python-openei==0.1.24"],
"requirements": ["python-openei==0.2.0"],
"version": "0.1.6"
}
3 changes: 2 additions & 1 deletion requirements_tests.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-r requirements_dev.txt
python-openei==0.1.24
python-openei==0.2.0
pytest
pytest-cov
pytest-homeassistant-custom-component
Expand All @@ -10,3 +10,4 @@ mypy
pydocstyle
isort
pylint
aioresponses
Loading

0 comments on commit 281bfa9

Please sign in to comment.