Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: "Set up Python"
uses: actions/setup-python@v6.1.0
with:
python-version: "3.11"
python-version: "3.13"
cache: "pip"

- name: "Install requirements"
Expand Down
4 changes: 3 additions & 1 deletion custom_components/iec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed

from .const import DOMAIN
from .coordinator import IecApiCoordinator
Expand All @@ -23,8 +24,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = iec_coordinator
try:
await hass.data[DOMAIN][entry.entry_id].async_config_entry_first_refresh()
except ConfigEntryAuthFailed:
raise
except Exception as err:
# Log the error but don't fail the setup
_LOGGER.error("Failed to fetch initial data: %s", err)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
Expand Down
9 changes: 8 additions & 1 deletion custom_components/iec/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,21 @@ async def async_setup_entry(
list(
filter(
lambda key: key not in (STATICS_DICT_NAME, JWT_DICT_NAME),
list(coordinator.data.keys()),
list(coordinator.data.keys()) if coordinator.data else [],
)
)
)
> 1
)

entities: list[BinarySensorEntity] = []

if not coordinator.data:
_LOGGER.warning(
"Coordinator data is not available yet, skipping binary sensor setup"
)
return

for contract_key in coordinator.data:
if contract_key in (STATICS_DICT_NAME, JWT_DICT_NAME):
continue
Expand Down
76 changes: 75 additions & 1 deletion custom_components/iec/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ class IecConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1

def __init__(self) -> None:
"""Initialize a new IECConfigFlow."""
"""Initialize a new IecConfigFlow."""
self.reauth_entry: config_entries.ConfigEntry | None = None
self.reconfigure_entry: config_entries.ConfigEntry | None = None
self.data: dict[str, Any] | None = None
self.client: IecClient | None = None

Expand Down Expand Up @@ -309,3 +310,76 @@ async def async_step_reauth_confirm(
data_schema=vol.Schema(schema),
errors=errors,
)

async def async_step_reconfigure(
self, entry_data: Mapping[str, Any] | None = None
) -> FlowResult:
"""Handle configuration by reconfigure."""
self.reconfigure_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
self.data = dict(self.reconfigure_entry.data)
# Clear TOTP secret for re-authentication
self.data.pop(CONF_TOTP_SECRET, None)
return await self.async_step_reconfigure_mfa()

async def async_step_reconfigure_mfa(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle MFA step for reconfigure."""
assert self.reconfigure_entry
errors: dict[str, str] = {}

client: IecClient = self.client

if user_input and user_input.get(CONF_TOTP_SECRET) is not None:
assert client
# Create data for validation, merging existing data with TOTP_SECRET
validation_data = {**self.reconfigure_entry.data, **user_input}
errors = await _validate_login(self.hass, validation_data, client)
if not errors:
# Only update the token, preserve all other existing data
updated_data = dict(self.reconfigure_entry.data)
updated_data[CONF_API_TOKEN] = client.get_token().to_dict()
# Ensure TOTP_SECRET is not persisted
updated_data.pop(CONF_TOTP_SECRET, None)

self.hass.config_entries.async_update_entry(
self.reconfigure_entry, data=updated_data
)
await self.hass.config_entries.async_reload(
self.reconfigure_entry.entry_id
)
return self.async_abort(reason="reconfigure_successful")

if not client:
self.client = IecClient(
self.data[CONF_USER_ID], async_create_clientsession(self.hass)
)
client = self.client

try:
otp_type = await client.login_with_id()
except asyncio.CancelledError:
errors["base"] = errors.get("base") or "cannot_connect"
otp_type = "OTP"
except IECError:
errors["base"] = errors.get("base") or "cannot_connect"
otp_type = "OTP"
except Exception as err: # noqa: BLE001
_LOGGER.exception(
"Unexpected error during reconfigure login_with_id: %s", err
)
errors["base"] = errors.get("base") or "cannot_connect"
otp_type = "OTP"

schema = {
vol.Required(CONF_TOTP_SECRET): str,
}

return self.async_show_form(
step_id="reconfigure_mfa",
description_placeholders={"otp_type": otp_type},
data_schema=vol.Schema(schema),
errors=errors,
)
61 changes: 54 additions & 7 deletions custom_components/iec/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,20 +781,67 @@ async def _async_update_data(
)

self._first_load = False
try:
_LOGGER.debug("Checking if API token needs to be refreshed")
# First thing first, check the token and refresh if needed.
old_token = self.api.get_token()
await self.api.check_token()
new_token = self.api.get_token()

def update_token_if_changed(new_token):
"""Update config entry if token has changed."""
if old_token != new_token:
_LOGGER.debug("Token refreshed")
new_data = {**self._entry_data, CONF_API_TOKEN: new_token.to_dict()}
self.hass.config_entries.async_update_entry(
entry=self._config_entry, data=new_data
)

try:
_LOGGER.debug("Checking if API token needs to be refreshed")
old_token = self.api.get_token()
await self.api.check_token()
new_token = self.api.get_token()
update_token_if_changed(new_token)

_LOGGER.debug("Validating token with get_customer API call")
await self.api.get_customer()

except IECError as err:
raise ConfigEntryAuthFailed from err
if err.status == 400:
_LOGGER.error(
"Token validation failed with 400 Bad Request. Attempting refresh and retry."
)
try:
await self.api.check_token()
new_token_after_retry = self.api.get_token()
update_token_if_changed(new_token_after_retry)
await self.api.get_customer()
except IECError as retry_err:
if retry_err.status == 400:
_LOGGER.error(
"Token refresh and retry failed with 400 Bad Request. Need to reconfigure integration."
)
entry = self.hass.config_entries.async_get_entry(
self._config_entry.entry_id
)
if entry:
try:
await self.hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": "reconfigure",
"entry_id": entry.entry_id,
},
)
except Exception as reconfig_err: # noqa: BLE001
_LOGGER.error(
"Failed to start reconfigure flow: %s", reconfig_err
)
raise ConfigEntryAuthFailed from retry_err
else:
raise ConfigEntryAuthFailed from retry_err
else:
_LOGGER.error(
"Authentication error during token validation (status %s): %s",
err.status,
err,
)
raise ConfigEntryAuthFailed from err

try:
return await self._update_data()
Expand Down
7 changes: 6 additions & 1 deletion custom_components/iec/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,13 +353,18 @@ async def async_setup_entry(
len(
list(
filter(
lambda key: key != STATICS_DICT_NAME, list(coordinator.data.keys())
lambda key: key != STATICS_DICT_NAME,
list(coordinator.data.keys()) if coordinator.data else [],
)
)
)
> 1
)

if not coordinator.data:
_LOGGER.warning("Coordinator data is not available yet, skipping sensor setup")
return

for contract_key in coordinator.data:
if contract_key == STATICS_DICT_NAME:
for sensor_desc in STATIC_SENSORS:
Expand Down
10 changes: 5 additions & 5 deletions custom_components/iec/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"description": "Select which contract to use"
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"title": "@common::config_flow::title::reauth",
"description": "Enter your One Time Password (OTP) send to your {otp_type}",
"data": {
"user_id": "User ID",
Expand All @@ -96,15 +96,15 @@
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "@common::config_flow::error::cannot_connect",
"invalid_auth": "@common::config_flow::error::invalid_auth",
"invalid_id": "Invalid Israeli ID",
"no_contracts": "You should select at least one contract",
"no_active_contracts": "No active contracts found"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"already_configured": "@common::config_flow::abort::already_configured_service",
"reauth_successful": "@common::config_flow::abort::reauth_successful"
}
}
}
10 changes: 5 additions & 5 deletions custom_components/iec/translations/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"description": "בחרו באיזה חשבון חוזה להשתמש"
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"title": "@common::config_flow::title::reauth",
"description": "הכניסו את הקוד החד-פעמי שנשלח אליכם (OTP) שנשלח ל-{otp_type}",
"data": {
"user_id": "מזהה לקוח",
Expand All @@ -99,15 +99,15 @@
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "@common::config_flow::error::cannot_connect",
"invalid_auth": "@common::config_flow::error::invalid_auth",
"invalid_id": "תעודת זהות לא תקנית",
"no_contracts": "נא לבחור לפחות חוזה אחד",
"no_active_contracts": "לא נמצאו חוזים פעילים"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"already_configured": "@common::config_flow::abort::already_configured_service",
"reauth_successful": "@common::config_flow::abort::reauth_successful"
}
}
}
2 changes: 1 addition & 1 deletion hacs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Israel Electric Corporation (IEC)",
"filename": "iec.zip",
"hide_default_branch": true,
"homeassistant": "2024.2.0",
"homeassistant": "2025.8.0",
"render_readme": true,
"zip_release": true
}
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
colorlog>=6.8.2
homeassistant==2024.2.0
homeassistant==2025.8.0
iec-api==0.4.12
pip>=21.0
ruff>=0.5.6