From b70c137138c579064ed0a6a894948727bf7070d4 Mon Sep 17 00:00:00 2001 From: Antonio Zaitoun Date: Sat, 4 Jul 2026 22:28:55 +0300 Subject: [PATCH] fix: source get_device_in from Device/{contract} endpoint The DeviceIn/{contract_id} endpoint now returns HTTP 400 ("Token should be provide") unless a reCAPTCHA token is supplied via the RecaptchToken header, which isn't feasible for headless clients (GuyKh/iec-custom-component#462). get_device_in now sources the device list from the reCAPTCHA-free Device/{contract_id} endpoint and adapts it to DeviceInResponse, keeping the same return shape (meter_kind defaults to "Consumption"). DeviceInResponse and DeviceInDevice fields are defaulted so they remain backward-compatible with the legacy DeviceIn payload. Co-authored-by: Cursor --- iec_api/const.py | 3 +++ iec_api/data.py | 35 ++++++++++++++++++++++++++++------- iec_api/models/device_in.py | 35 +++++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/iec_api/const.py b/iec_api/const.py index 4201c38..9612048 100644 --- a/iec_api/const.py +++ b/iec_api/const.py @@ -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" diff --git a/iec_api/data.py b/iec_api/data.py index 6a2ea8a..fb82a71 100644 --- a/iec_api/data.py +++ b/iec_api/data.py @@ -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, @@ -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 @@ -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 []) + ] + 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( diff --git a/iec_api/models/device_in.py b/iec_api/models/device_in.py index 8c480cd..07926b7 100644 --- a/iec_api/models/device_in.py +++ b/iec_api/models/device_in.py @@ -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, @@ -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)