Skip to content

Conversation

@firstof9
Copy link
Owner

@firstof9 firstof9 commented Jan 19, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Status and sensor fields now handle missing or malformed data safely, returning None/defaults instead of errors; more robust parsing for temperatures, times, voltages, and connection indicators.
  • API

    • Many public getters updated to reflect optional/absent data (more values may be None rather than raising).
  • Tests

    • Tests updated to expect None/falsey values instead of exceptions and ensure proper cleanup.
  • Chores

    • Package version bumped to 0.2.3

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

📝 Walkthrough

Walkthrough

Multiple OpenEVSE property getters were hardened: type hints broadened to Optional, direct dict indexing replaced with .get(...), parsing and fallbacks added for missing or malformed status/config fields; tests updated to expect None/boolean defaults; package version bumped.

Changes

Cohort / File(s) Summary
API robustness refactor
openevsehttp/__main__.py
Dozens of public property signatures changed to Optional[...] or safer boolean types; internal direct dict indexing replaced with .get(...); strict assertions on _config/_status removed; guarded parsing/coercion and safe fallbacks added across device info, status, telemetry, shaper, and vehicle-related properties.
Tests updated
tests/test_main.py
Tests adjusted to expect None or explicit boolean defaults instead of raising KeyError for missing fields; parametrized expectations updated; exception-context checks removed; websocket disconnect cleanup ensured.
Version bump
setup.py
VERSION incremented from "0.2.2" to "0.2.3"

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I nudged the keys with careful paw,

When data's missing, I saw no flaw;
A gentle None, a softer cheer,
Tests now steady, no KeyError fear,
Small hops of safety — clap your paw!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective: refactoring properties to return None when values are missing, which aligns with the extensive signature changes across 40+ properties.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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 docstrings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Pylint (4.0.4)
openevsehttp/__main__.py

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
openevsehttp/__main__.py (2)

1162-1168: Inconsistent pattern: assertion and direct indexing remain.

This property still uses assert self._config is not None and direct dictionary indexing self._config["protocol"], which is inconsistent with the PR's goal of graceful handling. If the protocol key is missing, this will raise KeyError.

Proposed fix
     `@property`
     def protocol_version(self) -> str | None:
         """Return the protocol version."""
-        assert self._config is not None
-        if self._config["protocol"] == "-":
+        protocol = self._config.get("protocol")
+        if protocol is None or protocol == "-":
             return None
-        return self._config["protocol"]
+        return protocol

1223-1233: Logic error: should use all() instead of any().

The condition checks if any of voltage or amp exists, but both are required for the calculation. If only one key exists, the direct indexing on line 1232 will raise KeyError.

Proposed fix
     `@property`
     def charging_power(self) -> float | None:
         """Return the charge power.

         Calculate Watts base on V*I
         """
-        if self._status is not None and any(
+        if self._status is not None and all(
             key in self._status for key in ["voltage", "amp"]
         ):
             return round(self._status["voltage"] * self._status["amp"], 2)
         return None
🤖 Fix all issues with AI agents
In `@openevsehttp/__main__.py`:
- Around line 898-904: The led_brightness property's return type annotation is
wrong: update the annotation on the led_brightness property from "str | None" to
"int | None" and ensure callers still read the value from
self._config.get("led_brightness") (no other logic change); verify
_version_check("4.1.0") and the UnsupportedFeature raise remain intact and that
any downstream uses expect an int or None.
- Around line 1127-1135: The usage_session property risks a KeyError by indexing
self._status["wattsec"]; update usage_session to safely access keys (use
self._status.get("session_energy") first, then wattsec =
self._status.get("wattsec")) and if wattsec is None return None, otherwise
compute and return float(round(wattsec / 3600, 2)); reference the usage_session
property and the "session_energy" and "wattsec" keys when making the change.
- Around line 951-954: The service_level property is annotated as str | None but
tests expect an int | None; update the annotation to int | None and convert the
stored value from self._config.get("service") to an int before returning (e.g.,
fetch value = self._config.get("service"); if value is None return None;
otherwise return int(value)), and guard the conversion with a try/except
ValueError to return None (or handle/log) if the config contains a non-integer
string; reference the service_level property and the use of
self._config.get("service") when making the change.
🧹 Nitpick comments (1)
openevsehttp/__main__.py (1)

1079-1085: Consider simplifying the key check (static analysis hint).

The "temp" in self._status check before using .get() is flagged by Ruff (RUF019). While the current logic works, you could simplify by relying on .get() with a falsy check:

     `@property`
     def ambient_temperature(self) -> float | None:
         """Return the temperature of the ambient sensor, in degrees Celsius."""
-        if "temp" in self._status and self._status["temp"]:
-            return self._status.get("temp", 0) / 10
-        else:
-            return self._status.get("temp1", 0) / 10
+        temp = self._status.get("temp")
+        if temp:
+            return temp / 10
+        return self._status.get("temp1", 0) / 10

Note: If both temp and temp1 are missing, this returns 0.0 rather than None. Consider whether returning None would be more consistent with other temperature properties.

@firstof9 firstof9 added the code quality Improves code quality label Jan 19, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@openevsehttp/__main__.py`:
- Around line 1035-1043: The status() method and state property call
int(self._status.get("state", 0)) which will raise on None or non-numeric
strings; update both to safely coerce state by retrieving raw =
self._status.get("state", 0) then attempting to parse to int with a try/except
(or use str.isdigit-like check) and fall back to 0 on TypeError/ValueError, then
use that safe_index when indexing states in status() and state property
(referencing the status() method, state property, and self._status).
- Around line 1080-1084: The ambient_temperature method currently defaults
missing sensor values to 0; change it to return None when the ambient
temperature is absent instead of using 0. Update ambient_temperature to first
check for the presence of the "temp" key in self._status (use key-in-dict, not
truthiness) and if present and not None return self._status["temp"] / 10;
otherwise check "temp1" similarly and return its value/10 if present, and return
None if neither key exists or both are None. This preserves 0 as a valid reading
and avoids defaulting to 0 when sensors are missing.
♻️ Duplicate comments (1)
openevsehttp/__main__.py (1)

952-954: Service level should be numeric (not str | None).

Returning a string here is inconsistent with the API/tests and can break callers expecting an int. Consider coercing to int | None with a safe fallback.

💡 Proposed fix
 `@property`
-def service_level(self) -> str | None:
+def service_level(self) -> int | None:
     """Return the service level."""
-    return self._config.get("service")
+    value = self._config.get("service")
+    if value is None:
+        return None
+    try:
+        return int(value)
+    except (TypeError, ValueError):
+        _LOGGER.debug("Invalid service level: %s", value)
+        return None

@firstof9
Copy link
Owner Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@openevsehttp/__main__.py`:
- Around line 1088-1114: The three accessors rtc_temperature, ir_temperature,
and esp_temperature currently treat falsy 0 as missing and will raise on
non-numeric strings; update each to explicitly check for None and the string
"false" (i.e., if temp is None or temp == "false" return None), then attempt to
convert temp to a numeric (int/float) inside a try/except (or use a safe parse)
and on success return numeric_value / 10, otherwise return None if parsing
fails; ensure esp_temperature still handles the absent "temp4" key the same way
before parsing.
♻️ Duplicate comments (3)
openevsehttp/__main__.py (3)

951-960: service_level should be int | None with safe parsing.
Same concern as earlier review: the value is numeric but annotated as str | None.

🛠️ Proposed fix
-    def service_level(self) -> str | None:
+    def service_level(self) -> int | None:
         """Return the service level."""
-        return self._config.get("service")
+        value = self._config.get("service")
+        if value is None:
+            return None
+        try:
+            return int(value)
+        except (TypeError, ValueError):
+            return None

1035-1042: Guard state parsing to avoid TypeError/ValueError.
Same issue as earlier review: int(self._status.get("state", 0)) can fail on None or non-numeric strings.

🛠️ Proposed fix
     def status(self) -> str:
         """Return charger's state."""
-        return self._status.get("status", states[int(self._status.get("state", 0))])
+        state = self._status.get("state")
+        try:
+            state_int = int(state) if state is not None else 0
+        except (TypeError, ValueError):
+            state_int = 0
+        return self._status.get("status", states[state_int])

     def state(self) -> str:
         """Return charger's state."""
-        return states[int(self._status.get("state", 0))]
+        state = self._status.get("state")
+        try:
+            state_int = int(state) if state is not None else 0
+        except (TypeError, ValueError):
+            state_int = 0
+        return states[state_int]

1073-1085: Return None when ambient temperature is missing (don’t coerce to 0).
Same issue as earlier review: current logic treats 0 as missing and returns 0 when both keys are absent.

🌡️ Proposed fix
     def ambient_temperature(self) -> float | None:
         """Return the temperature of the ambient sensor, in degrees Celsius."""
-        temp = self._status.get("temp")
-        if temp:
-            return temp / 10
-        return self._status.get("temp1", 0) / 10
+        temp = self._status.get("temp")
+        if temp is not None and temp != "false":
+            return temp / 10
+        temp1 = self._status.get("temp1")
+        if temp1 is not None and temp1 != "false":
+            return temp1 / 10
+        return None

@firstof9
Copy link
Owner Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@firstof9 firstof9 merged commit cdbca62 into main Jan 19, 2026
5 checks passed
@firstof9 firstof9 deleted the cleanup branch January 19, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code quality Improves code quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants