Skip to content

Commit 972aae2

Browse files
authored
Merge pull request #229 from nickknissen/claude/fix-monta-rate-limit-011CUpJHhqxdU3P6QwzQmFTH
Configurable update Interval and options improvement
2 parents a79b109 + 8ce4e31 commit 972aae2

File tree

6 files changed

+183
-6
lines changed

6 files changed

+183
-6
lines changed

custom_components/monta/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from homeassistant.helpers.aiohttp_client import async_get_clientsession
1313

1414
from .api import MontaApiClient
15-
from .const import DOMAIN, STORAGE_KEY, STORAGE_VERSION
15+
from .const import CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, DOMAIN, STORAGE_KEY, STORAGE_VERSION
1616
from .coordinator import MontaDataUpdateCoordinator
1717
from .services import async_setup_services
1818

@@ -31,6 +31,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
3131
store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
3232
await store.async_remove()
3333

34+
# Get scan interval from options or data, with default fallback
35+
scan_interval = entry.options.get(
36+
CONF_SCAN_INTERVAL,
37+
entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL),
38+
)
39+
3440
hass.data[DOMAIN][entry.entry_id] = coordinator = MontaDataUpdateCoordinator(
3541
hass=hass,
3642
client=MontaApiClient(
@@ -39,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
3945
session=async_get_clientsession(hass),
4046
store=store,
4147
),
48+
scan_interval=scan_interval,
4249
)
4350
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
4451
await coordinator.async_config_entry_first_refresh()

custom_components/monta/config_flow.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
MontaApiClientCommunicationError,
1515
MontaApiClientError,
1616
)
17-
from .const import DOMAIN, LOGGER, STORAGE_KEY, STORAGE_VERSION
17+
from .const import CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, STORAGE_KEY, STORAGE_VERSION
1818

1919

2020
class MontaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -66,6 +66,129 @@ async def async_step_user(
6666
type=selector.TextSelectorType.PASSWORD
6767
),
6868
),
69+
vol.Optional(
70+
CONF_SCAN_INTERVAL,
71+
default=(user_input or {}).get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL),
72+
): selector.NumberSelector(
73+
selector.NumberSelectorConfig(
74+
min=30,
75+
max=3600,
76+
unit_of_measurement="seconds",
77+
mode=selector.NumberSelectorMode.BOX,
78+
),
79+
),
80+
}
81+
),
82+
errors=_errors,
83+
)
84+
85+
@staticmethod
86+
def async_get_options_flow(
87+
config_entry: config_entries.ConfigEntry,
88+
) -> config_entries.OptionsFlow:
89+
"""Get the options flow for this handler."""
90+
return MontaOptionsFlowHandler(config_entry)
91+
92+
async def _test_credentials(self, client_id: str, client_secret: str) -> any:
93+
"""Validate credentials."""
94+
client = MontaApiClient(
95+
client_id=client_id,
96+
client_secret=client_secret,
97+
session=async_create_clientsession(self.hass),
98+
store=Store(self.hass, STORAGE_VERSION, STORAGE_KEY),
99+
)
100+
return await client.async_request_token()
101+
102+
103+
class MontaOptionsFlowHandler(config_entries.OptionsFlow):
104+
"""Handle options flow for Monta."""
105+
106+
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
107+
"""Initialize options flow."""
108+
self.config_entry = config_entry
109+
110+
async def async_step_init(
111+
self, user_input: dict | None = None
112+
) -> config_entries.FlowResult:
113+
"""Manage the options."""
114+
_errors = {}
115+
if user_input is not None:
116+
# Only validate credentials if they were changed
117+
if (
118+
user_input.get(CONF_CLIENT_ID) != self.config_entry.data.get(CONF_CLIENT_ID)
119+
or user_input.get(CONF_CLIENT_SECRET) != self.config_entry.data.get(CONF_CLIENT_SECRET)
120+
):
121+
try:
122+
await self._test_credentials(
123+
client_id=user_input[CONF_CLIENT_ID],
124+
client_secret=user_input[CONF_CLIENT_SECRET],
125+
)
126+
except MontaApiClientAuthenticationError as exception:
127+
LOGGER.warning(exception)
128+
_errors["base"] = "auth"
129+
except MontaApiClientCommunicationError as exception:
130+
LOGGER.error(exception)
131+
_errors["base"] = "connection"
132+
except MontaApiClientError as exception:
133+
LOGGER.exception(exception)
134+
_errors["base"] = "unknown"
135+
136+
if not _errors:
137+
# Update the config entry data with new credentials if they changed
138+
if (
139+
user_input.get(CONF_CLIENT_ID) != self.config_entry.data.get(CONF_CLIENT_ID)
140+
or user_input.get(CONF_CLIENT_SECRET) != self.config_entry.data.get(CONF_CLIENT_SECRET)
141+
):
142+
self.hass.config_entries.async_update_entry(
143+
self.config_entry,
144+
data={
145+
CONF_CLIENT_ID: user_input[CONF_CLIENT_ID],
146+
CONF_CLIENT_SECRET: user_input[CONF_CLIENT_SECRET],
147+
CONF_SCAN_INTERVAL: user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL),
148+
},
149+
)
150+
return self.async_create_entry(title="", data=user_input)
151+
152+
return self.async_show_form(
153+
step_id="init",
154+
data_schema=vol.Schema(
155+
{
156+
vol.Required(
157+
CONF_CLIENT_ID,
158+
default=(user_input or {}).get(
159+
CONF_CLIENT_ID,
160+
self.config_entry.data.get(CONF_CLIENT_ID),
161+
),
162+
): selector.TextSelector(
163+
selector.TextSelectorConfig(
164+
type=selector.TextSelectorType.TEXT
165+
),
166+
),
167+
vol.Required(
168+
CONF_CLIENT_SECRET,
169+
default=(user_input or {}).get(
170+
CONF_CLIENT_SECRET,
171+
self.config_entry.data.get(CONF_CLIENT_SECRET),
172+
),
173+
): selector.TextSelector(
174+
selector.TextSelectorConfig(
175+
type=selector.TextSelectorType.PASSWORD
176+
),
177+
),
178+
vol.Optional(
179+
CONF_SCAN_INTERVAL,
180+
default=self.config_entry.options.get(
181+
CONF_SCAN_INTERVAL,
182+
self.config_entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL),
183+
),
184+
): selector.NumberSelector(
185+
selector.NumberSelectorConfig(
186+
min=30,
187+
max=3600,
188+
unit_of_measurement="seconds",
189+
mode=selector.NumberSelectorMode.BOX,
190+
),
191+
),
69192
}
70193
),
71194
errors=_errors,

custom_components/monta/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
ATTR_WALLET = "wallet"
1515
ATTR_TRANSACTIONS = "transactions"
1616

17+
CONF_SCAN_INTERVAL = "scan_interval"
18+
DEFAULT_SCAN_INTERVAL = 120 # Default to 120 seconds to stay within rate limits
19+
1720
PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
1821
STORAGE_KEY = "monta_auth"
1922
STORAGE_VERSION = 1

custom_components/monta/coordinator.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ class MontaDataUpdateCoordinator(DataUpdateCoordinator):
1818

1919
config_entry: ConfigEntry
2020

21-
def __init__(self, hass: HomeAssistant, client: MontaApiClient) -> None:
21+
def __init__(
22+
self, hass: HomeAssistant, client: MontaApiClient, scan_interval: int
23+
) -> None:
2224
"""Initialize."""
2325
self.client = client
2426
super().__init__(
2527
hass=hass,
2628
logger=LOGGER,
2729
name=DOMAIN,
28-
update_interval=timedelta(seconds=30),
30+
update_interval=timedelta(seconds=scan_interval),
2931
)
3032

3133
async def _async_update_data(self):

custom_components/monta/translations/en.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
"description": "👉 Monta Public API is in BETA currently.\nIn order to obtain client id/Client secret to it, you might have to opt-in to their Beta program. This can be done in your user account settings in the app or in Monta's CPMS. \n\nClient id and secret are obtained from https://portal2.monta.app/applications.",
66
"data": {
77
"client_id": "Client id",
8-
"client_secret": "client secret"
8+
"client_secret": "client secret",
9+
"scan_interval": "Scan interval (seconds)"
10+
},
11+
"data_description": {
12+
"scan_interval": "How often to fetch data from Monta API. Monta has a rate limit of 10 requests per minute. Recommended: 120 seconds or higher for multiple chargers."
913
}
1014
}
1115
},
@@ -15,6 +19,23 @@
1519
"unknown": "Unknown error occurred."
1620
}
1721
},
22+
"options": {
23+
"step": {
24+
"init": {
25+
"description": "Configure Monta integration options. You can update your credentials or adjust the scan interval here.",
26+
"data": {
27+
"client_id": "Client id",
28+
"client_secret": "Client secret",
29+
"scan_interval": "Scan interval (seconds)"
30+
},
31+
"data_description": {
32+
"client_id": "Your Monta API client ID from https://portal2.monta.app/applications",
33+
"client_secret": "Your Monta API client secret from https://portal2.monta.app/applications",
34+
"scan_interval": "How often to fetch data from Monta API. Monta has a rate limit of 10 requests per minute. Recommended: 120 seconds or higher for multiple chargers."
35+
}
36+
}
37+
}
38+
},
1839
"services": {
1940
"start_charging": {
2041
"description": "Start charge on selected charger",

custom_components/monta/translations/pt.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
"description": "👉 A API Pública da Monta está atualmente em BETA.\nPara obter o ID do cliente/segredo do cliente, pode ser necessário aderir ao programa Beta. Isto pode ser feito nas configurações da sua conta de utilizador na aplicação ou no CPMS da Monta.\n\nO ID e o segredo do cliente são obtidos em https://portal2.monta.app/applications.",
66
"data": {
77
"client_id": "ID do cliente",
8-
"client_secret": "Segredo do cliente"
8+
"client_secret": "Segredo do cliente",
9+
"scan_interval": "Intervalo de varredura (segundos)"
10+
},
11+
"data_description": {
12+
"scan_interval": "Com que frequência buscar dados da API Monta. A Monta tem um limite de 10 solicitações por minuto. Recomendado: 120 segundos ou mais para vários carregadores."
913
}
1014
}
1115
},
@@ -15,6 +19,23 @@
1519
"unknown": "Ocorreu um erro desconhecido."
1620
}
1721
},
22+
"options": {
23+
"step": {
24+
"init": {
25+
"description": "Configurar opções de integração Monta. Você pode atualizar suas credenciais ou ajustar o intervalo de varredura aqui.",
26+
"data": {
27+
"client_id": "ID do cliente",
28+
"client_secret": "Segredo do cliente",
29+
"scan_interval": "Intervalo de varredura (segundos)"
30+
},
31+
"data_description": {
32+
"client_id": "Seu ID de cliente da API Monta de https://portal2.monta.app/applications",
33+
"client_secret": "Seu segredo de cliente da API Monta de https://portal2.monta.app/applications",
34+
"scan_interval": "Com que frequência buscar dados da API Monta. A Monta tem um limite de 10 solicitações por minuto. Recomendado: 120 segundos ou mais para vários carregadores."
35+
}
36+
}
37+
}
38+
},
1839
"services": {
1940
"start_charging": {
2041
"description": "Iniciar carregamento no carregador selecionado",

0 commit comments

Comments
 (0)