Skip to content

fix: price unit API support, ChargeState nullable fields, and legacy model cleanup#101

Merged
Veldkornet merged 3 commits into
HiDiHo01:mainfrom
Veldkornet:fix/price-unit-enode-sensors-legacy-cleanup
Jun 17, 2026
Merged

fix: price unit API support, ChargeState nullable fields, and legacy model cleanup#101
Veldkornet merged 3 commits into
HiDiHo01:mainfrom
Veldkornet:fix/price-unit-enode-sensors-legacy-cleanup

Conversation

@Veldkornet

@Veldkornet Veldkornet commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR addresses three related issues in the library layer: API-provided gas unit support, robustness fixes for EV charger data, and removal of legacy/duplicate model code.

Key Changes

  • ** on ** — parses the API's field (e.g. KWH, M3) and exposes it as price.per_unit so the HA integration can select the correct unit without hardcoding it
  • Nullable fieldsbattery_capacity, battery_level, charge_limit, charge_rate, charge_time_remaining, is_plugged_in, power_delivery_state are now | None with None guards in from_dict, preventing a silent TypeError when a wall charger has no connected EV
  • Removed Old_PriceData — legacy class, dead helper methods (old_avg, older_avg, old_elec_previoushour, old_elec_nexthour), and unused PriceData.from_dict() removed, leaving one canonical implementation
  • Ruff complianceResolution migrated from (str, Enum) to StrEnum; SIM102/SIM103/SIM108/SIM117/D401/E402/B018/F541 violations resolved across all touched files
  • Tests — regression tests added for nullable ChargeState; snapshot and fixture files updated to reflect model changes

Why This Is Needed

The perUnit field is the only reliable way to distinguish Dutch (kWh) from Belgian (GJ) gas contracts at runtime. Without it, the HA integration was forced to hardcode the unit, which broke Belgian users.

The ChargeState TypeError was silently swallowing charger data for users whose EV was not connected, making all charger sensors unavailable.

The Old_PriceData cleanup reduces maintenance surface and eliminates a potential double-conversion bug where Price objects were passed to a constructor expecting list[dict].

Closes

Summary by CodeRabbit

Release Notes

  • New Features

    • Added diagnostic helpers for schema introspection and system troubleshooting.
  • Bug Fixes

    • Improved token expiration detection and validation logic.
    • Enhanced robustness of smart battery data retrieval with better error handling.
    • Refined error messages for API validation failures.
    • Better handling of missing or incomplete API responses.
    • Strengthened date validation across pricing and usage queries.
  • Documentation

    • Deprecated authentication property added for backward compatibility.

…cleanup

- feat(models): add per_unit field to Price, parsed from API perUnit field (closes HiDiHo01#99)
- fix(models): make ChargeState fields nullable to handle wall chargers without a connected EV
- refactor(models): remove Old_PriceData, dead helper methods, and legacy duplicate implementations (closes HiDiHo01#100)
- refactor(models): migrate Resolution enum from (str, Enum) to StrEnum
- fix(models): simplify auth token checks and error handling (ruff SIM102, SIM103, SIM108)
- fix(auth): inline JWT-structure guard in is_expired (ruff SIM103)
- fix(frank_energie): remove extraneous f-string prefix (ruff F541)
- test(models): add regression tests for nullable ChargeState fields
- test: fix import ordering and useless-expression violations (ruff E402, B018)
- test: merge nested with-statements (ruff SIM117)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @Veldkornet, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Veldkornet, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 21 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0739fbb0-73fa-400f-9583-08b6b53a3b48

📥 Commits

Reviewing files that changed from the base of the PR and between 1eac4f1 and dad6a30.

📒 Files selected for processing (4)
  • python_frank_energie/frank_energie.py
  • python_frank_energie/models.py
  • tests/conftest.py
  • tests/test_frank_energie.py
📝 Walkthrough

Walkthrough

Authentication.is_expired gains a JWT structure check; AuthenticationResult is removed. FrankEnergie client method signatures are made more permissive, GraphQL error handling now raises typed exceptions, and two diagnostic methods are added. models.py introduces parse_date(), widens nullable fields on ChargeState/EnodeCharger/SmartBatterySessions, rewrites MarketPrices, and adds per_unit support on price aggregates. Tests, fixtures, and snapshots are aligned throughout.

Changes

API client and model overhaul

Layer / File(s) Summary
Authentication token expiry and AuthenticationResult removal
python_frank_energie/authentication.py, python_frank_energie/models.py
Authentication.is_expired now checks JWT segment count when expires_at is None instead of returning True unconditionally. AuthenticationResult dataclass is removed. Authentication.from_dict error handling uses a walrus guard pattern.
FrankEnergie client method signatures and error handling
python_frank_energie/frank_energie.py
Exception imports expanded; deprecated auth property added; is_authenticated tightened to authToken; _query validates to_dict(); GraphQL error branches raise SmartTradingNotEnabledException, SmartChargingNotEnabledException, and FrankEnergieException for country-restricted errors; login returns Authentication|None; enode_chargers returns {} on missing auth; me/user/be_prices gain optional parameters; smart_batteries returns SmartBatteries([]); smart_battery_details raises on incomplete data.
introspect_schema and get_diagnostic_data
python_frank_energie/frank_energie.py
Two new public methods added: introspect_schema() performs live synchronous GraphQL schema introspection via requests.post, and get_diagnostic_data() returns a constant string.
Shared model helpers and enum update
python_frank_energie/models.py
Resolution switched to StrEnum. Top-level parse_date() helper added; nested local parse_date in ContractPriceResolutionState.from_dict removed. ContractPriceResolutionChangeResult.from_dict gains flexible success/reason parsing. Invoices.calculate_average_costs_per_month simplified.
ChargeState and EnodeCharger nullability widening
python_frank_energie/models.py
ChargeState fields widened to nullable types; from_dict uses _parse_iso_datetime and conditional conversions. EnodeCharger.last_seen becomes datetime|None; from_dict uses nullable charge-state construction.
Price per_unit optional and PriceData/PriceDataAvg per_unit properties
python_frank_energie/models.py
Price.per_unit changed to str|None via data.get(). PriceDataAvg.per_unit and PriceData.per_unit properties added returning the first non-null per_unit. Legacy electricity helper methods and old PriceData.from_dict removed.
MarketPrices dataclass rewrite
python_frank_energie/models.py, tests/fixtures/market_prices.json
Legacy pricing section replaced with new MarketPrices dataclass with defensive from_dict/from_be_dict parsing. Fixture keys renamed to electricityPrices/gasPrices.
SmartBatterySessions nullable field widening
python_frank_energie/models.py
Period boundary types changed to datetime|None and numeric fields to float|None. from_dict uses _safe_float and _parse_iso_datetime. UserSmartFeedInStatus.from_dict returns None for null payload.
Test fixtures updated
tests/fixtures/invoices.json, tests/fixtures/month_summary.json, tests/fixtures/smart_battery_sessions.json
invoices.json field names changed from PascalCase to camelCase. month_summary.json adds _id and __typename. smart_battery_sessions.json adds cumulativeResult/result/status fields.
Test conftest socket fixtures
tests/conftest.py
Adds aggressive pytest_socket enabling at module load, a pytest_runtest_setup hook, and an autouse fixture restoring socket.socket from pytest_socket._true_socket.
Snapshots updated
tests/__snapshots__/test_frank_energie.ambr, tests/__snapshots__/test_models.ambr, tests/__snapshots__/test_smart_batteries.ambr
Snapshots updated to reflect richer model shapes, timezone-aware datetimes, snake_case field names, and new metadata fields.
test_models.py assertion and coverage updates
tests/test_models.py
Error message expectations updated; invoice attributes switched to snake_case; test_price_and_pricedata_per_unit added; ChargeState/EnodeCharger regression tests added for no-car-attached and car-attached scenarios.
test_frank_energie.py signature and behavior updates
tests/test_frank_energie.py
Authentication keyword args updated to snake_case; user_prices calls gain country argument; _ensure_session patching corrected; logging assertions tightened; Windows event-loop mock updated; default-date tests use new fixture shape.
test_renew_token and test_contract_price_resolution minor updates
tests/test_renew_token.py, tests/test_contract_price_resolution_mutations.py
Authentication constructor uses snake_case kwargs. Cancellation error test refactored to combined context manager.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #99 (Support API-provided gas units perUnit for Dutch and Belgian contracts): This PR makes Price.per_unit optional (str | None), populates it via data.get("perUnit"), and adds per_unit properties to both PriceData and PriceDataAvg, directly delivering the model-layer foundation requested in the issue.
  • #100 (Remove legacy and duplicate PriceData implementations): This PR removes the legacy PriceData.from_dict implementation, the old_elec_* helper methods, and the older_avg block, fulfilling the cleanup requested in the issue.

Possibly related PRs

  • HiDiHo01/python-frank-energie#80: Touches the same is_authenticated / auth-gating logic in frank_energie.py that this PR also tightens around authToken presence.
  • HiDiHo01/python-frank-energie#89: The Authentication.is_expired JWT check added here directly complements the RenewToken mutation's Authorization header omission logic from that PR.

Suggested labels

enhancement, python, tests, api, authentication, size/xl

Poem

🐇 Hoppity hop through the diff so wide,
Per-unit gas units now live inside!
Old legacy code? Gone without a trace,
Nullable fields now gracefully embrace.
The token checks JWTs with care,
And snapshots are fresh as morning air! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the three main changes: price unit API support, nullable ChargeState fields, and legacy model cleanup, which align with the changeset.
Linked Issues check ✅ Passed The PR addresses all coding requirements from #99 (perUnit parsing and exposure) and #100 (legacy code removal), plus additional robustness improvements in null-safety and dead code cleanup.
Out of Scope Changes check ✅ Passed All changes are in scope: parsing perUnit, nullability improvements for ChargeState, legacy code removal, Resolution enum migration, and snapshot updates align with the linked issues.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
python_frank_energie/frank_energie.py (1)

696-701: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Docstring/behavior mismatch: method returns {} but docstring claims it raises AuthRequiredException.

The test confirms returning {} is the intended behavior when not authenticated, but the docstring at lines 697-698 still documents raising AuthRequiredException.

📝 Suggested docstring fix
     Returns:
         The enode charger information.
 
-    Raises:
-        AuthRequiredException: If the client is not authenticated.
-        FrankEnergieException: If the request fails.
+    Returns an empty dict if the client is not authenticated.
+
+    Raises:
+        FrankEnergieException: If the request fails.
     """
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python_frank_energie/frank_energie.py` around lines 696 - 701, The docstring
for this method incorrectly documents that AuthRequiredException is raised when
the client is not authenticated (lines 697-698), but the actual code returns an
empty dictionary instead. Remove the AuthRequiredException from the Raises
section of the docstring to align with the actual behavior that returns {} when
not authenticated. If needed, document the empty dictionary return behavior in
the Returns section instead.
python_frank_energie/models.py (2)

3347-3359: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

all_avg should return PriceDataAvg so per_unit works on aggregate output.

At Line 3347, returning a dynamic type(...) object bypasses the new PriceDataAvg.per_unit property, so consumers of all_avg won’t see the propagated unit metadata.

💡 Suggested fix
@@
-        return type(
-            "PriceDataAvg",
-            (object,),
-            {
-                "values": all_prices,
-                "total": avg,
-                "market_price_with_tax_and_markup": market_price_with_tax_and_markup_avg,
-                "market_markup_price": market_price_markup_avg,
-                "market_price_with_tax": market_price_with_tax_avg,
-                "market_price_tax": market_price_tax_avg,
-                "market_price": market_price_avg,
-            },
-        )
+        return PriceDataAvg(
+            values=all_prices,
+            total=avg,
+            market_price_with_tax_and_markup=market_price_with_tax_and_markup_avg,
+            market_markup_price=market_price_markup_avg,
+            market_price_with_tax=market_price_with_tax_avg,
+            market_price_tax=market_price_tax_avg,
+            market_price=market_price_avg,
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python_frank_energie/models.py` around lines 3347 - 3359, The all_avg method
is currently returning a dynamically created type object via type(...) instead
of a proper PriceDataAvg instance, which prevents consumers from accessing the
per_unit property. Replace the dynamic type creation with an instantiation of
the PriceDataAvg class, passing the price data fields (all_prices, avg,
market_price_with_tax_and_markup_avg, market_price_markup_avg,
market_price_with_tax_avg, market_price_tax_avg, market_price_avg) as
constructor arguments or attributes to the PriceDataAvg class so that the
per_unit property becomes accessible on the returned aggregate output.

4214-4239: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden SmartBatterySessions.from_dict against malformed payloads and non-numeric values.

At Line 4219 and Line 4229, the code assumes payload["smartBatterySessions"] is dict-like and can crash with AttributeError if the API returns an unexpected shape. At Line 4226, _safe_float still raises on non-numeric strings, so it is not actually safe.

💡 Suggested fix
@@
-        payload = data.get("data")
-        if not payload:
+        payload = data.get("data")
+        if not payload:
             # return None
             raise RequestException("Unexpected response")
+        if not isinstance(payload, Mapping):
+            raise RequestException("Missing 'data' in SmartBatterySessions response")
 
         smart_battery_session_data = payload.get("smartBatterySessions")
+        if not isinstance(smart_battery_session_data, Mapping):
+            raise RequestException("Missing 'smartBatterySessions' in response")
@@
         def _safe_float(val: Any) -> float | None:
             if val is None or val == "":
                 return None
-            return float(val)
+            try:
+                return float(val)
+            except (TypeError, ValueError):
+                return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python_frank_energie/models.py` around lines 4214 - 4239, The
SmartBatterySessions.from_dict method lacks proper validation of the API
response structure. After retrieving smart_battery_session_data from the payload
using payload.get("smartBatterySessions"), add a check to ensure it is actually
a dictionary and is not None before attempting to call .get() methods on it;
raise RequestException if it is malformed. Additionally, the _safe_float helper
function is not truly safe because it does not handle exceptions when float(val)
fails on non-numeric strings. Wrap the float(val) conversion in a try-except
block to catch ValueError exceptions and return None for any values that cannot
be converted to float, ensuring the function is actually safe for all
non-numeric string inputs.
tests/test_frank_energie.py (1)

1159-1181: ⚠️ Potential issue | 🟠 Major

Restore the original Windows event-loop policy in teardown instead of unconditionally deleting it.

The test sets asyncio.WindowsSelectorEventLoopPolicy to a mock (line 1165) but the teardown unconditionally deletes the attribute (lines 1180-1181). On Windows, this removes the real stdlib symbol and breaks test isolation—subsequent tests in the same process will fail to access the real event loop policy.

Proposed fix
         import asyncio
         import sys
         from unittest.mock import MagicMock
 
         original_platform = sys.platform
+        original_policy_class = getattr(asyncio, "WindowsSelectorEventLoopPolicy", None)
         mock_policy_class = MagicMock()
         mock_policy = MagicMock()
         mock_policy_class.return_value = mock_policy
@@
         finally:
             sys.platform = original_platform
-            if hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
+            if original_policy_class is None and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
                 del asyncio.WindowsSelectorEventLoopPolicy
+            elif original_policy_class is not None:
+                asyncio.WindowsSelectorEventLoopPolicy = original_policy_class
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_frank_energie.py` around lines 1159 - 1181, The test
unconditionally deletes asyncio.WindowsSelectorEventLoopPolicy in the finally
block, which removes the real stdlib symbol on Windows systems and breaks test
isolation. Instead of unconditionally deleting the attribute in the teardown,
save the original value of asyncio.WindowsSelectorEventLoopPolicy before the try
block (checking if it exists first), and then restore it in the finally block to
its original state or delete it only if it did not exist originally.
🧹 Nitpick comments (1)
python_frank_energie/models.py (1)

2430-2448: ⚡ Quick win

Reuse ChargeState.from_dict in EnodeCharger.from_dict to prevent parser drift.

This block duplicates parsing logic and has already diverged (e.g., charge_time_remaining normalization behavior differs from ChargeState.from_dict). Delegating to one parser reduces drift risk.

♻️ Suggested refactor
@@
-        raw_battery_capacity = charge_state_data.get("batteryCapacity")
-        raw_battery_level = charge_state_data.get("batteryLevel")
-        raw_charge_limit = charge_state_data.get("chargeLimit")
-        raw_range = charge_state_data.get("range")
-        raw_is_fully_charged = charge_state_data.get("isFullyCharged")
-
-        charge_state = ChargeState(
-            battery_capacity=float(raw_battery_capacity) if raw_battery_capacity is not None else None,
-            battery_level=int(raw_battery_level) if raw_battery_level is not None else None,
-            charge_limit=int(raw_charge_limit) if raw_charge_limit is not None else None,
-            charge_rate=float(charge_state_data["chargeRate"]) if charge_state_data.get("chargeRate") is not None else None,
-            charge_time_remaining=charge_state_data.get("chargeTimeRemaining"),
-            is_charging=bool(charge_state_data["isCharging"]),
-            is_fully_charged=bool(raw_is_fully_charged) if raw_is_fully_charged is not None else None,
-            is_plugged_in=bool(charge_state_data["isPluggedIn"]),
-            last_updated=_parse_iso_datetime(charge_state_data.get("lastUpdated")),
-            power_delivery_state=charge_state_data["powerDeliveryState"],
-            range=int(raw_range) if raw_range is not None else None,
-        )
+        charge_state = ChargeState.from_dict(charge_state_data)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python_frank_energie/models.py` around lines 2430 - 2448, The parsing logic
in the EnodeCharger.from_dict method is duplicating the ChargeState.from_dict
logic, which has already caused inconsistencies (such as differing behavior for
charge_time_remaining normalization). Replace the entire inline ChargeState
construction block that manually extracts and converts fields like
battery_capacity, battery_level, charge_limit, charge_rate,
charge_time_remaining, is_charging, is_fully_charged, is_plugged_in,
last_updated, power_delivery_state, and range with a single delegation to
ChargeState.from_dict(charge_state_data) to ensure consistent parsing behavior
and eliminate future drift between the two parsers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python_frank_energie/frank_energie.py`:
- Around line 2333-2334: Replace the Dutch error message "Authenticatie is
vereist." in the AuthRequiredException raised when self.is_authenticated is
false with the English equivalent "Authentication is required." to maintain
consistency with the rest of the codebase where this same English message is
used throughout the file.
- Around line 2896-2914: The introspect_schema method incorrectly uses
requests.post() with a context manager (with statement), but requests.Response
does not implement the context manager protocol (__enter__/__exit__ methods),
which will cause an AttributeError at runtime. Remove the with statement and
instead directly assign the result of requests.post() to the response variable,
then call response.raise_for_status() and return response.json() on the assigned
variable as normal.

In `@python_frank_energie/models.py`:
- Around line 4690-4705: The from_dict classmethod in UserSmartFeedInStatus
silently maps GraphQL failures to None by only checking if the payload is None,
making it impossible to distinguish between a legitimate "no feed-in contract"
response (where userSmartFeedIn is explicitly null) and actual API errors or
malformed responses. Before extracting the userSmartFeedIn payload, check the
GraphQL response for an errors field; if errors are present, raise an
appropriate exception rather than returning None. Only return None when
userSmartFeedIn is explicitly null in a valid, error-free response.

In `@tests/conftest.py`:
- Around line 21-29: The force_enable_socket fixture at lines 21-29 uses the
private internal attribute pytest_socket._true_socket and mutates the global
socket.socket in an autouse fixture, causing cross-test state leakage and
fragile coupling to pytest-socket internals. Since the file already calls the
public pytest_socket.enable_socket() method at other locations and tests use the
proper `@pytest.mark.allow_socket` marker, this fixture is redundant. Either
remove the entire force_enable_socket fixture completely, or if it must be
retained, replace the private API usage by calling the public
pytest_socket.enable_socket() method instead of directly assigning
socket.socket.

---

Outside diff comments:
In `@python_frank_energie/frank_energie.py`:
- Around line 696-701: The docstring for this method incorrectly documents that
AuthRequiredException is raised when the client is not authenticated (lines
697-698), but the actual code returns an empty dictionary instead. Remove the
AuthRequiredException from the Raises section of the docstring to align with the
actual behavior that returns {} when not authenticated. If needed, document the
empty dictionary return behavior in the Returns section instead.

In `@python_frank_energie/models.py`:
- Around line 3347-3359: The all_avg method is currently returning a dynamically
created type object via type(...) instead of a proper PriceDataAvg instance,
which prevents consumers from accessing the per_unit property. Replace the
dynamic type creation with an instantiation of the PriceDataAvg class, passing
the price data fields (all_prices, avg, market_price_with_tax_and_markup_avg,
market_price_markup_avg, market_price_with_tax_avg, market_price_tax_avg,
market_price_avg) as constructor arguments or attributes to the PriceDataAvg
class so that the per_unit property becomes accessible on the returned aggregate
output.
- Around line 4214-4239: The SmartBatterySessions.from_dict method lacks proper
validation of the API response structure. After retrieving
smart_battery_session_data from the payload using
payload.get("smartBatterySessions"), add a check to ensure it is actually a
dictionary and is not None before attempting to call .get() methods on it; raise
RequestException if it is malformed. Additionally, the _safe_float helper
function is not truly safe because it does not handle exceptions when float(val)
fails on non-numeric strings. Wrap the float(val) conversion in a try-except
block to catch ValueError exceptions and return None for any values that cannot
be converted to float, ensuring the function is actually safe for all
non-numeric string inputs.

In `@tests/test_frank_energie.py`:
- Around line 1159-1181: The test unconditionally deletes
asyncio.WindowsSelectorEventLoopPolicy in the finally block, which removes the
real stdlib symbol on Windows systems and breaks test isolation. Instead of
unconditionally deleting the attribute in the teardown, save the original value
of asyncio.WindowsSelectorEventLoopPolicy before the try block (checking if it
exists first), and then restore it in the finally block to its original state or
delete it only if it did not exist originally.

---

Nitpick comments:
In `@python_frank_energie/models.py`:
- Around line 2430-2448: The parsing logic in the EnodeCharger.from_dict method
is duplicating the ChargeState.from_dict logic, which has already caused
inconsistencies (such as differing behavior for charge_time_remaining
normalization). Replace the entire inline ChargeState construction block that
manually extracts and converts fields like battery_capacity, battery_level,
charge_limit, charge_rate, charge_time_remaining, is_charging, is_fully_charged,
is_plugged_in, last_updated, power_delivery_state, and range with a single
delegation to ChargeState.from_dict(charge_state_data) to ensure consistent
parsing behavior and eliminate future drift between the two parsers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc5caf85-c7b3-4947-8c96-8016ef72205b

📥 Commits

Reviewing files that changed from the base of the PR and between dd16670 and 1eac4f1.

📒 Files selected for processing (15)
  • python_frank_energie/authentication.py
  • python_frank_energie/frank_energie.py
  • python_frank_energie/models.py
  • tests/__snapshots__/test_frank_energie.ambr
  • tests/__snapshots__/test_models.ambr
  • tests/__snapshots__/test_smart_batteries.ambr
  • tests/conftest.py
  • tests/fixtures/invoices.json
  • tests/fixtures/market_prices.json
  • tests/fixtures/month_summary.json
  • tests/fixtures/smart_battery_sessions.json
  • tests/test_contract_price_resolution_mutations.py
  • tests/test_frank_energie.py
  • tests/test_models.py
  • tests/test_renew_token.py

Comment thread python_frank_energie/frank_energie.py Outdated
Comment thread python_frank_energie/frank_energie.py Outdated
Comment thread python_frank_energie/models.py
Comment thread tests/conftest.py
@sonarqubecloud

Copy link
Copy Markdown

@Veldkornet Veldkornet merged commit a8ec917 into HiDiHo01:main Jun 17, 2026
7 checks passed
@Veldkornet Veldkornet deleted the fix/price-unit-enode-sensors-legacy-cleanup branch June 17, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove legacy and duplicate PriceData implementations Support API-provided gas units (perUnit) for Dutch and Belgian contracts

2 participants