From 8f575f55cde96734b8bf536d850ff5c17b909085 Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Mon, 19 Jan 2026 10:11:08 -0700 Subject: [PATCH 1/7] refactor: optimize properties to return `None` if value missing --- openevsehttp/__main__.py | 204 ++++++++++++++++----------------------- setup.py | 2 +- tests/test_main.py | 27 +++--- 3 files changed, 95 insertions(+), 138 deletions(-) diff --git a/openevsehttp/__main__.py b/openevsehttp/__main__.py index ca6d36e..9ff2ff2 100644 --- a/openevsehttp/__main__.py +++ b/openevsehttp/__main__.py @@ -896,37 +896,32 @@ async def set_divert_mode(self, mode: str = "fast") -> None: raise UnknownError @property - def led_brightness(self) -> str: + def led_brightness(self) -> str | None: """Return charger led_brightness.""" if not self._version_check("4.1.0"): _LOGGER.debug("Feature not supported for older firmware.") raise UnsupportedFeature - assert self._config is not None - return self._config["led_brightness"] + return self._config.get("led_brightness") @property - def hostname(self) -> str: + def hostname(self) -> str | None: """Return charger hostname.""" - assert self._config is not None - return self._config["hostname"] + return self._config.get("hostname") @property - def wifi_ssid(self) -> str: + def wifi_ssid(self) -> str | None: """Return charger connected SSID.""" - assert self._config is not None - return self._config["ssid"] + return self._config.get("ssid") @property - def ammeter_offset(self) -> int: + def ammeter_offset(self) -> int | None: """Return ammeter's current offset.""" - assert self._config is not None - return self._config["offset"] + return self._config.get("offset") @property - def ammeter_scale_factor(self) -> int: + def ammeter_scale_factor(self) -> int | None: """Return ammeter's current scale factor.""" - assert self._config is not None - return self._config["scale"] + return self._config.get("scale") @property def temp_check_enabled(self) -> bool: @@ -954,23 +949,21 @@ def stuck_relay_check_enabled(self) -> bool: return bool(self._config.get("relayt", False)) @property - def service_level(self) -> str: + def service_level(self) -> str | None: """Return the service level.""" - assert self._config is not None - return self._config["service"] + return self._config.get("service") @property - def openevse_firmware(self) -> str: + def openevse_firmware(self) -> str | None: """Return the firmware version.""" - assert self._config is not None - return self._config["firmware"] + return self._config.get("firmware") @property def max_current_soft(self) -> int | None: """Return the max current soft.""" - if self._config is not None and "max_current_soft" in self._config: - return self._config["max_current_soft"] - return self._status["pilot"] + if "max_current_soft" in self._config: + return self._config.get("max_current_soft") + return self._status.get("pilot") @property async def async_charge_current(self) -> int | None: @@ -984,9 +977,9 @@ async def async_charge_current(self) -> int | None: return min( claims["properties"]["charge_current"], self._config["max_current_hard"] ) - if self._config is not None and "max_current_soft" in self._config: - return self._config["max_current_soft"] - return self._status["pilot"] + if "max_current_soft" in self._config: + return self._config.get("max_current_soft") + return self._status.get("pilot") @property def max_current(self) -> int | None: @@ -994,33 +987,29 @@ def max_current(self) -> int | None: return self._status.get("max_current", None) @property - def wifi_firmware(self) -> str: + def wifi_firmware(self) -> str | None: """Return the ESP firmware version.""" - assert self._config is not None - value = self._config["version"] - if "dev" in value: + value = self._config.get("version") + if value is not None and "dev" in value: _LOGGER.debug("Stripping 'dev' from version.") value = value.split(".") value = ".".join(value[0:3]) return value @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the ip address.""" - assert self._status is not None - return self._status["ipaddress"] + return self._status.get("ipaddress") @property - def charging_voltage(self) -> int: + def charging_voltage(self) -> int | None: """Return the charging voltage.""" - assert self._status is not None - return self._status["voltage"] + return self._status.get("voltage") @property - def mode(self) -> str: + def mode(self) -> str | None: """Return the mode.""" - assert self._status is not None - return self._status["mode"] + return self._status.get("mode") @property def using_ethernet(self) -> bool: @@ -1028,98 +1017,80 @@ def using_ethernet(self) -> bool: return bool(self._status.get("eth_connected", False)) @property - def stuck_relay_trip_count(self) -> int: + def stuck_relay_trip_count(self) -> int | None: """Return the stuck relay count.""" - assert self._status is not None - return self._status["stuckcount"] + return self._status.get("stuckcount") @property - def no_gnd_trip_count(self) -> int: + def no_gnd_trip_count(self) -> int | None: """Return the no ground count.""" - assert self._status is not None - return self._status["nogndcount"] + return self._status.get("nogndcount") @property - def gfi_trip_count(self) -> int: + def gfi_trip_count(self) -> int | None: """Return the GFCI count.""" - assert self._status is not None - return self._status["gfcicount"] + return self._status.get("gfcicount") @property def status(self) -> str: """Return charger's state.""" - state = self._status.get("status", states[int(self._status.get("state", 0))]) - return state + return self._status.get("status", states[int(self._status.get("state", 0))]) @property def state(self) -> str: """Return charger's state.""" - assert self._status is not None - return states[int(self._status["state"])] + return states[int(self._status.get("state", 0))] @property - def state_raw(self) -> int: + def state_raw(self) -> int | None: """Return charger's state int form.""" - assert self._status is not None - return self._status["state"] + return self._status.get("state") @property - def charge_time_elapsed(self) -> int: + def charge_time_elapsed(self) -> int | None: """Return elapsed charging time.""" - assert self._status is not None - return self._status["elapsed"] + return self._status.get("elapsed") @property - def wifi_signal(self) -> str: + def wifi_signal(self) -> str | None: """Return charger's wifi signal.""" - assert self._status is not None - return self._status["srssi"] + return self._status.get("srssi") @property - def charging_current(self) -> float: + def charging_current(self) -> float | None: """Return the charge current. 0 if is not currently charging. """ - assert self._status is not None - return self._status["amp"] + return self._status.get("amp") @property - def current_capacity(self) -> int: + def current_capacity(self) -> int | None: """Return the current capacity.""" - assert self._status is not None - return self._status["pilot"] + return self._status.get("pilot") @property - def usage_total(self) -> float: + def usage_total(self) -> float | None: """Return the total energy usage in Wh.""" - assert self._status is not None if "total_energy" in self._status: - return self._status["total_energy"] - return self._status["watthour"] + return self._status.get("total_energy") + return self._status.get("watthour") @property def ambient_temperature(self) -> float | None: """Return the temperature of the ambient sensor, in degrees Celsius.""" - assert self._status is not None - temp = None if "temp" in self._status and self._status["temp"]: - temp = self._status["temp"] / 10 + return self._status.get("temp", 0) / 10 else: - temp = self._status["temp1"] / 10 - return temp + return self._status.get("temp1", 0) / 10 @property def rtc_temperature(self) -> float | None: - """Return the temperature of the real time clock sensor. - - In degrees Celsius. - """ - assert self._status is not None - temp = self._status["temp2"] if self._status["temp2"] else None - if temp is not None: - return temp / 10 - return None + """Return the temperature of the real time clock sensor.""" + temp = self._status.get("temp2", None) + if not temp or temp == "false": + return None + return temp / 10 @property def ir_temperature(self) -> float | None: @@ -1127,27 +1098,25 @@ def ir_temperature(self) -> float | None: In degrees Celsius. """ - assert self._status is not None - temp = self._status["temp3"] if self._status["temp3"] else None - if temp is not None: - return temp / 10 - return None + temp = self._status.get("temp3", None) + if not temp or temp == "false": + return None + return temp / 10 @property def esp_temperature(self) -> float | None: """Return the temperature of the ESP sensor, in degrees Celsius.""" - assert self._status is not None if "temp4" in self._status: - temp = self._status["temp4"] if self._status["temp4"] else None - if temp is not None: - return temp / 10 + temp = self._status.get("temp4", None) + if not temp or temp == "false": + return None + return temp / 10 return None @property def time(self) -> datetime | None: """Get the RTC time.""" value = self._status.get("time") - if value: try: return datetime.fromisoformat(value.replace("Z", "+00:00")) @@ -1156,14 +1125,13 @@ def time(self) -> datetime | None: return None @property - def usage_session(self) -> float: + def usage_session(self) -> float | None: """Get the energy usage for the current charging session. Return the energy usage in Wh. """ - assert self._status is not None if "session_energy" in self._status: - return self._status["session_energy"] + return self._status.get("session_energy") return float(round(self._status["wattsec"] / 3600, 2)) @property @@ -1200,55 +1168,47 @@ def protocol_version(self) -> str | None: return self._config["protocol"] @property - def vehicle(self) -> str: + def vehicle(self) -> bool: """Return if a vehicle is connected dto the EVSE.""" - assert self._status is not None - return self._status["vehicle"] + return self._status.get("vehicle", False) @property - def ota_update(self) -> str: + def ota_update(self) -> bool: """Return if an OTA update is active.""" - assert self._status is not None - return self._status["ota_update"] + return self._status.get("ota_update", False) @property - def manual_override(self) -> str: + def manual_override(self) -> bool: """Return if Manual Override is set.""" - assert self._status is not None - return self._status["manual_override"] + return self._status.get("manual_override", False) @property def divertmode(self) -> str: """Return the divert mode.""" - assert self._status is not None - mode = self._status["divertmode"] + mode = self._status.get("divertmode", 1) if mode == 1: return "fast" return "eco" @property - def charge_mode(self) -> str: + def charge_mode(self) -> str | None: """Return the charge mode.""" - assert self._config is not None - return self._config["charge_mode"] + return self._config.get("charge_mode") @property - def available_current(self) -> float: + def available_current(self) -> float | None: """Return the computed available current for divert.""" - assert self._status is not None - return self._status["available_current"] + return self._status.get("available_current") @property - def smoothed_available_current(self) -> float: + def smoothed_available_current(self) -> float | None: """Return the computed smoothed available current for divert.""" - assert self._status is not None - return self._status["smoothed_available_current"] + return self._status.get("smoothed_available_current") @property - def charge_rate(self) -> float: + def charge_rate(self) -> float | None: """Return the divert charge rate.""" - assert self._status is not None - return self._status["charge_rate"] + return self._status.get("charge_rate") @property def divert_active(self) -> bool: diff --git a/setup.py b/setup.py index d9425da..a5d0803 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ PROJECT_DIR = Path(__file__).parent.resolve() README_FILE = PROJECT_DIR / "README.md" -VERSION = "0.2.2" +VERSION = "0.2.3" setup( name="python_openevse_http", diff --git a/tests/test_main.py b/tests/test_main.py index 1ff312f..bb801eb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -687,29 +687,27 @@ async def test_get_charge_rate(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] + "fixture, expected", [("test_charger", None), ("test_charger_v2", None)] ) async def test_get_available_current(fixture, expected, request): """Test v4 Status reply.""" charger = request.getfixturevalue(fixture) await charger.update() - with pytest.raises(KeyError): - status = charger.available_current - # assert status == expected - await charger.ws_disconnect() + status = charger.available_current + assert status == expected + await charger.ws_disconnect() @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] + "fixture, expected", [("test_charger", None), ("test_charger_v2", None)] ) async def test_get_smoothed_available_current(fixture, expected, request): """Test v4 Status reply.""" charger = request.getfixturevalue(fixture) await charger.update() - with pytest.raises(KeyError): - status = charger.smoothed_available_current - # assert status == expected - await charger.ws_disconnect() + status = charger.smoothed_available_current + assert status == expected + await charger.ws_disconnect() @pytest.mark.parametrize( @@ -726,16 +724,15 @@ async def test_get_divert_active(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] + "fixture, expected", [("test_charger", False), ("test_charger_v2", False)] ) async def test_get_manual_override(fixture, expected, request): """Test v4 Status reply.""" charger = request.getfixturevalue(fixture) await charger.update() - with pytest.raises(KeyError): - status = charger.manual_override - # assert status == expected - await charger.ws_disconnect() + status = charger.manual_override + assert status == expected + await charger.ws_disconnect() async def test_toggle_override( From e901e1e45c25a0a9049c339b6d86267bbcdad012 Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Mon, 19 Jan 2026 10:16:14 -0700 Subject: [PATCH 2/7] linting --- openevsehttp/__main__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openevsehttp/__main__.py b/openevsehttp/__main__.py index 9ff2ff2..ba6af6c 100644 --- a/openevsehttp/__main__.py +++ b/openevsehttp/__main__.py @@ -1081,8 +1081,7 @@ 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 + return self._status.get("temp1", 0) / 10 @property def rtc_temperature(self) -> float | None: From 9f65012b5a9c6a05e3a14975ee6738c2f142cbc0 Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Mon, 19 Jan 2026 10:21:38 -0700 Subject: [PATCH 3/7] update usage_session --- openevsehttp/__main__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openevsehttp/__main__.py b/openevsehttp/__main__.py index ba6af6c..1f425b1 100644 --- a/openevsehttp/__main__.py +++ b/openevsehttp/__main__.py @@ -896,7 +896,7 @@ async def set_divert_mode(self, mode: str = "fast") -> None: raise UnknownError @property - def led_brightness(self) -> str | None: + def led_brightness(self) -> int | None: """Return charger led_brightness.""" if not self._version_check("4.1.0"): _LOGGER.debug("Feature not supported for older firmware.") @@ -1131,7 +1131,10 @@ def usage_session(self) -> float | None: """ if "session_energy" in self._status: return self._status.get("session_energy") - return float(round(self._status["wattsec"] / 3600, 2)) + wattsec = self._status.get("wattsec") + if wattsec is not None: + return float(round(wattsec / 3600, 2)) + return None @property def total_day(self) -> float | None: From 6a4fef30a218f25006aa2c3542018849b1374831 Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Mon, 19 Jan 2026 10:27:05 -0700 Subject: [PATCH 4/7] additional fixes per suggestions --- openevsehttp/__main__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openevsehttp/__main__.py b/openevsehttp/__main__.py index 1f425b1..b45c947 100644 --- a/openevsehttp/__main__.py +++ b/openevsehttp/__main__.py @@ -1164,10 +1164,10 @@ def has_limit(self) -> bool | None: @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 == "-": return None - return self._config["protocol"] + return protocol @property def vehicle(self) -> bool: @@ -1228,7 +1228,7 @@ def charging_power(self) -> float | None: 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) From 7821b3f675ba1e3de59eb11e9192b113de644da1 Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Mon, 19 Jan 2026 10:29:20 -0700 Subject: [PATCH 5/7] more suggested fixes --- openevsehttp/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openevsehttp/__main__.py b/openevsehttp/__main__.py index b45c947..2d9c5f4 100644 --- a/openevsehttp/__main__.py +++ b/openevsehttp/__main__.py @@ -1079,8 +1079,9 @@ def usage_total(self) -> float | None: @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 + temp = self._status.get("temp") + if temp: + return temp / 10 return self._status.get("temp1", 0) / 10 @property From a10c7b25dd91ddc1b01bc4fddeb05ba27d9c4f71 Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Mon, 19 Jan 2026 11:11:43 -0700 Subject: [PATCH 6/7] suggestions --- openevsehttp/__main__.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/openevsehttp/__main__.py b/openevsehttp/__main__.py index 2d9c5f4..513e4f6 100644 --- a/openevsehttp/__main__.py +++ b/openevsehttp/__main__.py @@ -1087,10 +1087,13 @@ def ambient_temperature(self) -> float | None: @property def rtc_temperature(self) -> float | None: """Return the temperature of the real time clock sensor.""" - temp = self._status.get("temp2", None) - if not temp or temp == "false": + temp = self._status.get("temp2") + if temp is None or isinstance(temp, bool): + return None + try: + return float(temp) / 10 + except (TypeError, ValueError): return None - return temp / 10 @property def ir_temperature(self) -> float | None: @@ -1098,20 +1101,24 @@ def ir_temperature(self) -> float | None: In degrees Celsius. """ - temp = self._status.get("temp3", None) - if not temp or temp == "false": + temp = self._status.get("temp3") + if temp is None or isinstance(temp, bool): + return None + try: + return float(temp) / 10 + except (TypeError, ValueError): return None - return temp / 10 @property def esp_temperature(self) -> float | None: """Return the temperature of the ESP sensor, in degrees Celsius.""" - if "temp4" in self._status: - temp = self._status.get("temp4", None) - if not temp or temp == "false": - return None - return temp / 10 - return None + temp = self._status.get("temp4") + if temp is None or isinstance(temp, bool): + return None + try: + return float(temp) / 10 + except (TypeError, ValueError): + return None @property def time(self) -> datetime | None: From aad6ba07c3005de395adfc3a57fecb1e1c07244c Mon Sep 17 00:00:00 2001 From: "firstof9@gmail.com" Date: Mon, 19 Jan 2026 11:20:34 -0700 Subject: [PATCH 7/7] simplify temperature properties --- openevsehttp/__main__.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/openevsehttp/__main__.py b/openevsehttp/__main__.py index 513e4f6..9fee51c 100644 --- a/openevsehttp/__main__.py +++ b/openevsehttp/__main__.py @@ -1090,10 +1090,7 @@ def rtc_temperature(self) -> float | None: temp = self._status.get("temp2") if temp is None or isinstance(temp, bool): return None - try: - return float(temp) / 10 - except (TypeError, ValueError): - return None + return float(temp) / 10 @property def ir_temperature(self) -> float | None: @@ -1104,10 +1101,7 @@ def ir_temperature(self) -> float | None: temp = self._status.get("temp3") if temp is None or isinstance(temp, bool): return None - try: - return float(temp) / 10 - except (TypeError, ValueError): - return None + return float(temp) / 10 @property def esp_temperature(self) -> float | None: @@ -1115,10 +1109,7 @@ def esp_temperature(self) -> float | None: temp = self._status.get("temp4") if temp is None or isinstance(temp, bool): return None - try: - return float(temp) / 10 - except (TypeError, ValueError): - return None + return float(temp) / 10 @property def time(self) -> datetime | None: