Skip to content
Closed
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
3 changes: 3 additions & 0 deletions iec_api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
GET_DEVICE_BY_DEVICE_ID_URL = GET_DEVICES_URL + "/{device_id}"
GET_DEVICE_TYPE_URL = IEC_API_BASE_URL + "Device/type/{bp_number}/{contract_id}/false"
GET_BILLING_INVOICES_URL = IEC_API_BASE_URL + "BillingCollection/invoices/{contract_id}/{bp_number}"
# Deprecated: as of 2025 this endpoint returns HTTP 400 ("Token should be provide")
# unless a reCAPTCHA token is supplied via the `RecaptchToken` header. data.get_device_in
# now sources the device list from GET_DEVICES_URL (Device/{contract_id}) instead.
GET_DEVICE_IN_URL = IEC_API_BASE_URL + "DeviceIn/{contract_id}"
GET_INVOICE_PDF_URL = IEC_API_BASE_URL + "BillingCollection/pdf"
GET_KWH_TARIFF_URL = IEC_API_BASE_URL + "content/he-IL/content/tariffs/contentpages/homeelectricitytariff"
Expand Down
35 changes: 28 additions & 7 deletions iec_api/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
GET_CUSTOMER_MOBILE_URL,
GET_DEFAULT_CONTRACT_URL,
GET_DEVICE_BY_DEVICE_ID_URL,
GET_DEVICE_IN_URL,
GET_DEVICE_TYPE_URL,
GET_DEVICES_URL,
GET_EFS_MESSAGES_URL,
Expand Down Expand Up @@ -51,7 +50,7 @@
from iec_api.models.device import decoder as devices_decoder
from iec_api.models.device_identity import DeviceDetails
from iec_api.models.device_identity import decoder as device_identity_decoder
from iec_api.models.device_in import DeviceInResponse
from iec_api.models.device_in import DEFAULT_METER_KIND, DeviceInDevice, DeviceInResponse
from iec_api.models.device_type import DeviceType
from iec_api.models.device_type import decoder as device_type_decoder
from iec_api.models.efs import EfsMessage, EfsRequestAllServices, EfsRequestSingleService
Expand Down Expand Up @@ -426,12 +425,34 @@ async def get_social_discount(session: ClientSession, token: JWT, bp_number: str


async def get_device_in(session: ClientSession, token: JWT, contract_id: str) -> Optional[DeviceInResponse]:
"""Get device information from DeviceIn endpoint."""
headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token)
response = await commons.send_get_request(
session=session, url=GET_DEVICE_IN_URL.format(contract_id=contract_id), headers=headers
"""Get the list of devices (meters) for a contract.

The legacy ``GET /api/DeviceIn/{contract_id}`` endpoint now rejects calls with
HTTP 400 ("Token should be provide") unless a reCAPTCHA token is supplied via
the ``RecaptchToken`` header, which is not feasible for headless clients.

Instead, this fetches the same device list from the reCAPTCHA-free
``GET /api/Device/{contract_id}`` endpoint and adapts it to
:class:`DeviceInResponse`, so callers keep the same return shape. That
endpoint returns everything the old one did except ``meterKind``, which
defaults to ``"Consumption"`` (the value RemoteReadingRange expects).
"""
devices = await get_devices(session, token, contract_id)
device_in_devices = [
DeviceInDevice(
is_active=device.is_active,
device_type=device.device_type,
device_number=device.device_number,
device_code=device.device_code,
meter_kind=DEFAULT_METER_KIND,
)
for device in (devices or [])
]
Comment on lines +440 to +450

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Inactive devices returned 🐞 Bug ≡ Correctness

data.get_device_in() now adapts the full /api/Device/{contract_id} list without filtering
inactive devices, even though IecClient.get_device_in() is documented as returning active devices.
Callers that select the first returned device (as in example.py) may start using an inactive meter
and fail downstream operations (e.g., remote reading).
Agent Prompt
### Issue description
`data.get_device_in()` returns all devices from `get_devices()` (including inactive). This conflicts with the documented expectation that `get_device_in()` provides “active devices”, and can break callers that choose the first device.

### Issue Context
- `IecClient.get_device_in()` states it returns active devices.
- `example.py` selects `device_in.devices[0]` without checking `is_active`.

### Fix Focus Areas
- Filter returned devices to active only **or** sort active devices first (preserving the old behavior expectation).
- If you choose to keep inactive devices in the response for completeness, ensure the ordering (or documentation) prevents common “pick first” callers from selecting an inactive meter.

- iec_api/data.py[427-455]
- example.py[70-85]
- iec_api/iec_client.py[670-678]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

return DeviceInResponse(
status=0,
is_active=any(device.is_active for device in device_in_devices),
devices=device_in_devices,
)
return DeviceInResponse.from_dict(response)


async def get_touz_compatibility(
Expand Down
35 changes: 23 additions & 12 deletions iec_api/models/device_in.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from dataclasses import dataclass, field
from typing import Optional

from mashumaro import DataClassDictMixin, field_options

# GET https://iecapi.iec.co.il/api/DeviceIn/{contract_id}
# Historically this data came from:
# GET https://iecapi.iec.co.il/api/DeviceIn/{contract_id}
#
# Return format is:
# {
# "status": 0,
# "isActive": true,
Expand All @@ -18,23 +19,33 @@
# }
# ]
# }
#
# As of 2025 that endpoint returns HTTP 400 ("Token should be provide") unless a
# reCAPTCHA token (`RecaptchToken` header) is supplied, which isn't feasible for
# headless clients. The equivalent device list is now sourced from the
# reCAPTCHA-free `GET /api/Device/{contract_id}` endpoint (see data.get_device_in).
# That endpoint returns the same fields except `meterKind`, so it defaults to
# "Consumption" (the value the RemoteReadingRange request expects anyway).

# Default meter kind used when the source endpoint does not report one.
DEFAULT_METER_KIND = "Consumption"


@dataclass
class DeviceInDevice(DataClassDictMixin):
"""Device information from DeviceIn endpoint."""
"""Device information (device list entry for a contract)."""

is_active: bool = field(metadata=field_options(alias="isActive"))
device_type: int = field(metadata=field_options(alias="deviceType"))
device_number: str = field(metadata=field_options(alias="deviceNumber"))
device_code: str = field(metadata=field_options(alias="deviceCode"))
meter_kind: str = field(metadata=field_options(alias="meterKind"))
is_active: bool = field(default=True, metadata=field_options(alias="isActive"))
device_type: Optional[int] = field(default=None, metadata=field_options(alias="deviceType"))
device_number: Optional[str] = field(default=None, metadata=field_options(alias="deviceNumber"))
device_code: Optional[str] = field(default=None, metadata=field_options(alias="deviceCode"))
meter_kind: str = field(default=DEFAULT_METER_KIND, metadata=field_options(alias="meterKind"))


@dataclass
class DeviceInResponse(DataClassDictMixin):
"""DeviceIn endpoint response."""
"""Device list response (compatible with the legacy DeviceIn payload)."""

status: int
is_active: bool = field(metadata=field_options(alias="isActive"))
devices: list[DeviceInDevice]
status: int = 0
is_active: bool = field(default=True, metadata=field_options(alias="isActive"))
devices: list[DeviceInDevice] = field(default_factory=list)
Loading