Skip to content

feat: add update_statistics_date service to unstick partial hour statistics - #369

Open
GuyKh wants to merge 1 commit into
mainfrom
feature/update-statistics-date
Open

feat: add update_statistics_date service to unstick partial hour statistics#369
GuyKh wants to merge 1 commit into
mainfrom
feature/update-statistics-date

Conversation

@GuyKh

@GuyKh GuyKh commented Mar 15, 2026

Copy link
Copy Markdown
Owner

Summary

When the IEC API returns partial data for an hour (e.g., 23:00 exists but 23:15/23:30/23:45 are missing), the statistics fetch gets stuck because _insert_statistics skips hours with fewer than 4 readings. The next refresh then re-attempts the same hour indefinitely.

This PR adds an update_statistics_date service that inserts a zero-value statistic record at a target hour, naturally advancing the DB's "last statistics" point so the next refresh continues from there.

Changes

  • coordinator.py: Added asyncio.Lock (_statistics_lock) to protect concurrent statistics operations, wrapped _insert_statistics with the lock, added set_statistics_from_date method with full validation, normalized device_number by stripping leading zeros
  • init.py: Registered update_statistics_date service
  • services.yaml: Added service definition
  • translations/en.json: Added English translations
  • translations/he.json: Added Hebrew translations

Validation

The service validates:

  1. Device number format (strips leading zeros)
  2. Datetime format (ISO)
  3. Not in the future
  4. Not older than 30 days
  5. Hour-aligned (minute/second must be 0)
  6. Device belongs to a known contract
  7. Target date is after last statistics
  8. No valid statistics already exist at target hour

Test Plan

  1. Call service: hass.services.async_call("iec", "update_statistics_date", {"datetime": "2024-01-16T01:00:00", "device_number": "12345"})
  2. Verify validation rejects dates before last statistics
  3. Verify validation rejects hours that already have valid statistics
  4. Verify the next _insert_statistics cycle picks up from the new hour

🤖 Generated with Claude Code

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add update_statistics_date service to unstick partial hour statistics

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add update_statistics_date service to manually advance statistics fetch point
• Implement thread-safe statistics operations with asyncio.Lock
• Normalize device numbers by stripping leading zeros in statistics processing
• Comprehensive validation for target datetime and device existence
• Register service with English and Hebrew translations
Diagram
flowchart LR
  A["Service Call<br/>update_statistics_date"] --> B["Validate Input<br/>datetime, device_number"]
  B --> C["Check Device<br/>Exists in Contract"]
  C --> D["Acquire Lock<br/>_statistics_lock"]
  D --> E["Verify Target Date<br/>After Last Stats"]
  E --> F["Insert Zero-Value<br/>Statistics Record"]
  F --> G["Return Success<br/>with Details"]
Loading

Grey Divider

File Changes

1. custom_components/iec/__init__.py ✨ Enhancement +11/-0

Register update_statistics_date service handler

• Register new update_statistics_date service handler
• Extract datetime and device_number from service call data
• Delegate to coordinator's set_statistics_from_date method
• Fire event iec_statistics_date_updated with result

custom_components/iec/init.py


2. custom_components/iec/coordinator.py ✨ Enhancement +344/-198

Add thread-safe statistics locking and set_statistics_from_date method

• Add _statistics_lock asyncio.Lock for thread-safe statistics operations
• Wrap entire _insert_statistics method body with lock acquisition
• Normalize device_number by converting to int then back to string to strip leading zeros
• Implement new set_statistics_from_date method with comprehensive validation
• Validate datetime format, future dates, age limit, hour alignment, device existence
• Check that target date is after last statistics and no valid stats exist at target hour
• Insert zero-value statistics records to advance fetch point while preserving cumulative sums

custom_components/iec/coordinator.py


3. custom_components/iec/services.yaml ⚙️ Configuration changes +15/-1

Define update_statistics_date service schema

• Add update_statistics_date service definition with description
• Define two required fields: datetime (ISO format) and device_number
• Add datetime selector for UI field validation
• Add text selector for device number field

custom_components/iec/services.yaml


View more (2)
4. custom_components/iec/translations/en.json 📝 Documentation +14/-0

Add English translations for service

• Add English translations for update_statistics_date service
• Translate service name and description
• Translate field names and descriptions for datetime and device_number

custom_components/iec/translations/en.json


5. custom_components/iec/translations/he.json 📝 Documentation +14/-0

Add Hebrew translations for service

• Add Hebrew translations for update_statistics_date service
• Translate service name and description to Hebrew
• Translate field names and descriptions for datetime and device_number

custom_components/iec/translations/he.json


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Mar 15, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Untyped call in service 📘 Rule violation ✓ Correctness
Description
The new handle_update_statistics_date service handler leaves the call parameter untyped
(suppressed via # noqa: ANN001), which breaks the requirement for full type hints under strict
mypy. This can hide real typing issues for service data access (call.data.get(...)).
Code

custom_components/iec/init.py[R43-46]

+    async def handle_update_statistics_date(call) -> None:  # noqa: ANN001
+        datetime_str = call.data.get("datetime")
+        device_number = call.data.get("device_number")
+        result = await iec_coordinator.set_statistics_from_date(datetime_str, device_number)
Evidence
PR Compliance ID 3 requires full type hints compatible with mypy strict; the added handler omits the
type for call and explicitly suppresses the annotation requirement.

AGENTS.md
custom_components/iec/init.py[43-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`handle_update_statistics_date` defines an untyped `call` parameter and suppresses the missing annotation with `# noqa: ANN001`, which violates strict typing requirements.

## Issue Context
This is a Home Assistant service handler; `call` should be typed (typically `homeassistant.core.ServiceCall`) so mypy can type-check service payload access.

## Fix Focus Areas
- custom_components/iec/__init__.py[43-46]

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


2. filter() typed as list 📘 Rule violation ✓ Correctness
Description
new_readings is annotated as list[PeriodConsumption] but is assigned the result of
filter(...), which is an iterator, not a list. This is a strict mypy incompatibility and can lead
to incorrect assumptions about the variable's behavior.
Code

custom_components/iec/coordinator.py[R1259-1265]

+                new_readings: list[PeriodConsumption] = filter(
+                    lambda reading: (
+                        reading.interval
+                        >= localize_datetime(datetime.fromtimestamp(last_stat_time))
+                    ),
+                    readings.meter_list[0].period_consumptions,
+                )
Evidence
PR Compliance ID 3 requires strict type correctness; the code declares a list[...] but assigns a
filter object, which mypy will flag as an incompatible assignment.

AGENTS.md
custom_components/iec/coordinator.py[1259-1265]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`new_readings` is annotated as `list[PeriodConsumption]` but `filter(...)` returns an iterator, causing a mypy strict type error.

## Issue Context
`itertools.groupby(...)` works with any iterable, so `new_readings` does not need to be a list unless later logic depends on list operations.

## Fix Focus Areas
- custom_components/iec/coordinator.py[1259-1269]

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


3. f-strings used in logging 📘 Rule violation ⛯ Reliability
Description
New _LOGGER.debug(...) calls use f-strings, which commonly violates Home Assistant’s Ruff
logging-format rules (and will be flagged by ruff check under HA defaults). This can block CI and
formatting checks.
Code

custom_components/iec/coordinator.py[R1252-1257]

+                _LOGGER.debug(
+                    f"[IEC Statistics] Last Consumption Sum for C[{contract_id}] D[{device.device_number}]: {consumption_sum}"
+                )
                _LOGGER.debug(
-                    f"[IEC Statistics] Last statistics are from {from_date.strftime('%Y-%m-%d %H:%M:%S')}"
+                    f"[IEC Statistics] Last Estimated Cost Sum for C[{contract_id}] D[{device.device_number}]: {cost_sum}"
                )
Evidence
PR Compliance ID 2 requires the PR to pass Ruff; the added debug logging uses f-strings rather than
%s-style lazy formatting, which is typically disallowed by Ruff’s logging rules in Home Assistant
configs.

AGENTS.md
custom_components/iec/coordinator.py[1252-1257]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several new `_LOGGER.debug(...)` calls use f-strings, which can violate Ruff logging rules used by Home Assistant.

## Issue Context
Use lazy formatting: `_LOGGER.debug(&quot;... %s&quot;, value)` to avoid eager string interpolation and satisfy lint rules.

## Fix Focus Areas
- custom_components/iec/coordinator.py[1252-1257]
- custom_components/iec/coordinator.py[1282-1290]
- custom_components/iec/coordinator.py[1331-1340]

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


View more (2)
4. Service call can crash 🐞 Bug ⛯ Reliability
Description
update_statistics_date passes call.data.get(...) values directly into
set_statistics_from_date; missing or non-string fields cause int(None) /
datetime.fromisoformat(None) to raise TypeError, which is not caught and will fail the service
call.
Code

custom_components/iec/init.py[R43-47]

+    async def handle_update_statistics_date(call) -> None:  # noqa: ANN001
+        datetime_str = call.data.get("datetime")
+        device_number = call.data.get("device_number")
+        result = await iec_coordinator.set_statistics_from_date(datetime_str, device_number)
+        _LOGGER.info("update_statistics_date result: %s", result)
Evidence
The service handler pulls inputs with dict.get, which yields None when absent, and forwards them
unvalidated. In the coordinator, parsing/normalization only catches ValueError, so a None input
triggers an uncaught TypeError.

custom_components/iec/init.py[43-47]
custom_components/iec/coordinator.py[1348-1362]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`update_statistics_date` can crash with an uncaught `TypeError` when `datetime` or `device_number` are missing or not strings, because the handler passes `None` through and `set_statistics_from_date` only catches `ValueError`.

### Issue Context
The service is registered without a validation schema, and the handler uses `dict.get`, which returns `None` for missing keys.

### Fix Focus Areas
- custom_components/iec/__init__.py[43-52]
- custom_components/iec/coordinator.py[1348-1362]

### Implementation notes
- Add explicit `if not datetime_str or not device_number: ...` checks (and ensure they are strings).
- Update exception handling to `except (TypeError, ValueError):` for both the `int(...)` and `fromisoformat(...)` conversions.
- Consider registering the service with a schema (e.g., `vol.Schema`) to enforce required fields and types.

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


5. Device list None crash 🐞 Bug ⛯ Reliability
Description
set_statistics_from_date iterates for device in devices without checking devices is iterable;
_get_devices_by_contract_id can return None on IEC API errors, causing a TypeError and
aborting the service.
Code

custom_components/iec/coordinator.py[R1384-1387]

+        for contract_id in self._contract_ids:
+            devices = await self._get_devices_by_contract_id(contract_id)
+            for device in devices:
+                if str(int(device.device_number)) == device_number:
Evidence
On IEC errors, _get_devices_by_contract_id logs but returns the devices variable, which may
still be None. The new service method assumes it is a list and iterates, which will raise at
runtime when the API call failed and no cache exists.

custom_components/iec/coordinator.py[312-322]
custom_components/iec/coordinator.py[1384-1387]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`set_statistics_from_date` assumes `_get_devices_by_contract_id` always returns an iterable list, but it can return `None` when the IEC API call fails, causing `TypeError: &#x27;NoneType&#x27; object is not iterable`.

### Issue Context
`_get_devices_by_contract_id` currently logs on `IECError` but does not set a fallback value, so it may return `None`.

### Fix Focus Areas
- custom_components/iec/coordinator.py[312-323]
- custom_components/iec/coordinator.py[1384-1392]

### Implementation notes
- Option A (preferred): In `_get_devices_by_contract_id`, return `[]` on exception / when no cached devices exist.
- Option B: In `set_statistics_from_date`, add `if not devices: continue` or return a structured error indicating device list could not be fetched.
- Ensure type annotations match behavior (`list[Device]` should not return `None`).

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



Remediation recommended

6. Unsorted groupby mis-aggregates 🐞 Bug ✓ Correctness
Description
_insert_statistics uses itertools.groupby on period_consumptions without sorting first, so if
readings are out of order the same hour can be split into multiple groups and produce incorrect
hourly sums/partial-hour detection.
Code

custom_components/iec/coordinator.py[R1259-1272]

+                new_readings: list[PeriodConsumption] = filter(
+                    lambda reading: (
+                        reading.interval
+                        >= localize_datetime(datetime.fromtimestamp(last_stat_time))
+                    ),
+                    readings.meter_list[0].period_consumptions,
+                )

-                if localized_today.date() == from_date.date():
-                    _LOGGER.debug(
-                        "[IEC Statistics] The date to fetch is today or later, replacing it with Today at 01:00:00"
-                    )
-                    from_date = localized_today.replace(
-                        hour=1, minute=0, second=0, microsecond=0
+                grouped_new_readings_by_hour = itertools.groupby(
+                    new_readings,
+                    key=lambda reading: reading.interval.replace(
+                        minute=0, second=0, microsecond=0
+                    ),
+                )
Evidence
itertools.groupby only groups consecutive items, so the input must be ordered by the grouping key.
The new code groups the API-provided period_consumptions directly (only filtered), while other
integration code explicitly deduplicates and sorts period_consumptions before
processing—indicating ordering is not assumed elsewhere.

custom_components/iec/coordinator.py[1259-1272]
custom_components/iec/coordinator.py[695-711]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`itertools.groupby` requires the input to be sorted by the grouping key; otherwise the same hour can appear in multiple groups and be summed incorrectly.

### Issue Context
The code currently filters `period_consumptions` and immediately calls `groupby` without sorting.

### Fix Focus Areas
- custom_components/iec/coordinator.py[1259-1273]

### Implementation notes
- Convert to a list and sort before grouping, e.g.:
 - `new_readings = [r for r in readings... if ...]`
 - `new_readings.sort(key=lambda r: r.interval)`
 - then `groupby(new_readings, ...)`
- Consider deduping by interval if duplicates are possible (elsewhere you already dedupe).

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread custom_components/iec/__init__.py Outdated
Comment on lines +43 to +46
async def handle_update_statistics_date(call) -> None: # noqa: ANN001
datetime_str = call.data.get("datetime")
device_number = call.data.get("device_number")
result = await iec_coordinator.set_statistics_from_date(datetime_str, device_number)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Untyped call in service 📘 Rule violation ✓ Correctness

The new handle_update_statistics_date service handler leaves the call parameter untyped
(suppressed via # noqa: ANN001), which breaks the requirement for full type hints under strict
mypy. This can hide real typing issues for service data access (call.data.get(...)).
Agent Prompt
## Issue description
`handle_update_statistics_date` defines an untyped `call` parameter and suppresses the missing annotation with `# noqa: ANN001`, which violates strict typing requirements.

## Issue Context
This is a Home Assistant service handler; `call` should be typed (typically `homeassistant.core.ServiceCall`) so mypy can type-check service payload access.

## Fix Focus Areas
- custom_components/iec/__init__.py[43-46]

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

Comment thread custom_components/iec/coordinator.py Outdated
Comment on lines +1259 to +1265
new_readings: list[PeriodConsumption] = filter(
lambda reading: (
reading.interval
>= localize_datetime(datetime.fromtimestamp(last_stat_time))
),
readings.meter_list[0].period_consumptions,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. filter() typed as list 📘 Rule violation ✓ Correctness

new_readings is annotated as list[PeriodConsumption] but is assigned the result of
filter(...), which is an iterator, not a list. This is a strict mypy incompatibility and can lead
to incorrect assumptions about the variable's behavior.
Agent Prompt
## Issue description
`new_readings` is annotated as `list[PeriodConsumption]` but `filter(...)` returns an iterator, causing a mypy strict type error.

## Issue Context
`itertools.groupby(...)` works with any iterable, so `new_readings` does not need to be a list unless later logic depends on list operations.

## Fix Focus Areas
- custom_components/iec/coordinator.py[1259-1269]

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

Comment on lines +1252 to 1257
_LOGGER.debug(
f"[IEC Statistics] Last Consumption Sum for C[{contract_id}] D[{device.device_number}]: {consumption_sum}"
)
_LOGGER.debug(
f"[IEC Statistics] Last statistics are from {from_date.strftime('%Y-%m-%d %H:%M:%S')}"
f"[IEC Statistics] Last Estimated Cost Sum for C[{contract_id}] D[{device.device_number}]: {cost_sum}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. F-strings used in logging 📘 Rule violation ⛯ Reliability

New _LOGGER.debug(...) calls use f-strings, which commonly violates Home Assistant’s Ruff
logging-format rules (and will be flagged by ruff check under HA defaults). This can block CI and
formatting checks.
Agent Prompt
## Issue description
Several new `_LOGGER.debug(...)` calls use f-strings, which can violate Ruff logging rules used by Home Assistant.

## Issue Context
Use lazy formatting: `_LOGGER.debug("... %s", value)` to avoid eager string interpolation and satisfy lint rules.

## Fix Focus Areas
- custom_components/iec/coordinator.py[1252-1257]
- custom_components/iec/coordinator.py[1282-1290]
- custom_components/iec/coordinator.py[1331-1340]

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

Comment thread custom_components/iec/__init__.py Outdated
Comment on lines +43 to +47
async def handle_update_statistics_date(call) -> None: # noqa: ANN001
datetime_str = call.data.get("datetime")
device_number = call.data.get("device_number")
result = await iec_coordinator.set_statistics_from_date(datetime_str, device_number)
_LOGGER.info("update_statistics_date result: %s", result)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Service call can crash 🐞 Bug ⛯ Reliability

update_statistics_date passes call.data.get(...) values directly into
set_statistics_from_date; missing or non-string fields cause int(None) /
datetime.fromisoformat(None) to raise TypeError, which is not caught and will fail the service
call.
Agent Prompt
### Issue description
`update_statistics_date` can crash with an uncaught `TypeError` when `datetime` or `device_number` are missing or not strings, because the handler passes `None` through and `set_statistics_from_date` only catches `ValueError`.

### Issue Context
The service is registered without a validation schema, and the handler uses `dict.get`, which returns `None` for missing keys.

### Fix Focus Areas
- custom_components/iec/__init__.py[43-52]
- custom_components/iec/coordinator.py[1348-1362]

### Implementation notes
- Add explicit `if not datetime_str or not device_number: ...` checks (and ensure they are strings).
- Update exception handling to `except (TypeError, ValueError):` for both the `int(...)` and `fromisoformat(...)` conversions.
- Consider registering the service with a schema (e.g., `vol.Schema`) to enforce required fields and types.

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

Comment on lines +1384 to +1387
for contract_id in self._contract_ids:
devices = await self._get_devices_by_contract_id(contract_id)
for device in devices:
if str(int(device.device_number)) == device_number:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Device list none crash 🐞 Bug ⛯ Reliability

set_statistics_from_date iterates for device in devices without checking devices is iterable;
_get_devices_by_contract_id can return None on IEC API errors, causing a TypeError and
aborting the service.
Agent Prompt
### Issue description
`set_statistics_from_date` assumes `_get_devices_by_contract_id` always returns an iterable list, but it can return `None` when the IEC API call fails, causing `TypeError: 'NoneType' object is not iterable`.

### Issue Context
`_get_devices_by_contract_id` currently logs on `IECError` but does not set a fallback value, so it may return `None`.

### Fix Focus Areas
- custom_components/iec/coordinator.py[312-323]
- custom_components/iec/coordinator.py[1384-1392]

### Implementation notes
- Option A (preferred): In `_get_devices_by_contract_id`, return `[]` on exception / when no cached devices exist.
- Option B: In `set_statistics_from_date`, add `if not devices: continue` or return a structured error indicating device list could not be fetched.
- Ensure type annotations match behavior (`list[Device]` should not return `None`).

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

@GuyKh
GuyKh force-pushed the feature/update-statistics-date branch 2 times, most recently from 04b7e27 to 90ae86d Compare March 15, 2026 13:58
…istics

When the IEC API returns partial data for an hour (e.g., 23:00 exists but
23:15/23:30/23:45 are missing), the statistics fetch gets stuck because
_insert_statistics skips hours with fewer than 4 readings.

This commit adds an `update_statistics_date` service that inserts a zero-value
statistic record at a target hour, naturally advancing the DB's "last statistics"
point so the next refresh continues from there.

- Add asyncio.Lock to protect concurrent statistics operations
- Wrap _insert_statistics with the lock
- Add set_statistics_from_date method with full validation
- Normalize device_number by stripping leading zeros
- Register service, services.yaml, and translations (en/he)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@GuyKh
GuyKh force-pushed the feature/update-statistics-date branch from 90ae86d to f212945 Compare April 6, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant