From 8956d4152fb5cb2b507f6dc5ad9cd86e53a162d6 Mon Sep 17 00:00:00 2001 From: Dan Raper Date: Tue, 2 Jan 2024 19:18:37 +0000 Subject: [PATCH] Tweaked charge state detection and added user-agent (#28) * Added user-agent to all HTTP requests * Added more detail to auth_error * Improve charging detection logic * Added fix to target time when pending approval --- custom_components/ohme/api_client.py | 5 ++- custom_components/ohme/binary_sensor.py | 47 ++++++++++++++++++--- custom_components/ohme/const.py | 4 +- custom_components/ohme/time.py | 9 ++-- custom_components/ohme/translations/en.json | 4 +- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/custom_components/ohme/api_client.py b/custom_components/ohme/api_client.py index 1fbd66b..7f4298c 100644 --- a/custom_components/ohme/api_client.py +++ b/custom_components/ohme/api_client.py @@ -4,7 +4,7 @@ from time import time from datetime import datetime, timedelta from homeassistant.helpers.entity import DeviceInfo -from .const import DOMAIN +from .const import DOMAIN, USER_AGENT, INTEGRATION_VERSION from .utils import time_next_occurs _LOGGER = logging.getLogger(__name__) @@ -111,7 +111,8 @@ def _get_headers(self): """Get auth and content-type headers""" return { "Authorization": "Firebase %s" % self._token, - "Content-Type": "application/json" + "Content-Type": "application/json", + "User-Agent": f"{USER_AGENT}/{INTEGRATION_VERSION}" } async def _post_request(self, url, skip_json=False, data=None): diff --git a/custom_components/ohme/binary_sensor.py b/custom_components/ohme/binary_sensor.py index b896f59..500e810 100644 --- a/custom_components/ohme/binary_sensor.py +++ b/custom_components/ohme/binary_sensor.py @@ -12,7 +12,6 @@ from .const import DOMAIN, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, DATA_CLIENT from .coordinator import OhmeChargeSessionsCoordinator from .utils import charge_graph_in_slot -from time import time _LOGGER = logging.getLogger(__name__) @@ -103,6 +102,9 @@ def __init__( self._last_reading = None self._last_reading_in_slot = False + # State variables for charge state detection + self._trigger_count = 0 + self.entity_id = generate_entity_id( "binary_sensor.{}", "ohme_car_charging", hass=hass) @@ -129,6 +131,7 @@ def _calculate_state(self) -> bool: # If no last reading or no batterySoc/power, fallback to power > 0 if not self._last_reading or not self._last_reading['batterySoc'] or not self._last_reading['power']: + _LOGGER.debug("ChargingBinarySensor: No last reading, defaulting to power > 0") return power > 0 # See if we are in a charge slot now and if we were for the last reading @@ -146,23 +149,57 @@ def _calculate_state(self) -> bool: # This condition makes sure we get the charge state updated on the tick immediately after charge stop. lr_power = self._last_reading["power"]["watt"] if lr_in_charge_slot and not in_charge_slot and lr_power > 0 and power / lr_power < 0.6: + _LOGGER.debug("ChargingBinarySensor: Power drop on state boundary, assuming not charging") + self._trigger_count = 0 return False # Failing that, we use the watt hours field to check charge state: - # - If Wh has positive delta and a nonzero power reading, we are charging - # This isn't ideal - eg. quirk of MG ZS in #13, so need to revisit + # - If Wh has positive delta + # - We have a nonzero power reading + # We are charging. Using the power reading isn't ideal - eg. quirk of MG ZS in #13, so need to revisit wh_delta = self.coordinator.data['batterySoc']['wh'] - self._last_reading['batterySoc']['wh'] - - return wh_delta > 0 and power > 0 + trigger_state = wh_delta > 0 and power > 0 + + _LOGGER.debug(f"ChargingBinarySensor: Reading Wh delta of {wh_delta} and power of {power}w") + + # If state is going upwards, report straight away + if trigger_state and not self._state: + _LOGGER.debug("ChargingBinarySensor: Upwards state change, reporting immediately") + self._trigger_count = 0 + return True + + # If state is going to change (downwards only for now), we want to see 2 consecutive readings of the state having + # changed before reporting it. + if self._state != trigger_state: + _LOGGER.debug("ChargingBinarySensor: Downwards state change, incrementing counter") + self._trigger_count += 1 + if self._trigger_count > 1: + _LOGGER.debug("ChargingBinarySensor: Counter hit, publishing downward state change") + self._trigger_count = 0 + return trigger_state + else: + self._trigger_count = 0 + + _LOGGER.debug("ChargingBinarySensor: Returning existing state") + + # State hasn't changed or we haven't seen 2 changed values - return existing state + return self._state @callback def _handle_coordinator_update(self) -> None: """Update data.""" + # Don't accept updates if 20s hasnt passed + # State calculations use deltas that may be unreliable to check if requests are too often + if self._last_updated and (utcnow().timestamp() - self._last_updated.timestamp() < 20): + _LOGGER.debug("ChargingBinarySensor: State update too soon - suppressing") + return + # If we have power info and the car is plugged in, calculate state. Otherwise, false if self.coordinator.data and self.coordinator.data["power"] and self.coordinator.data['mode'] != "DISCONNECTED": self._state = self._calculate_state() else: self._state = False + _LOGGER.debug("ChargingBinarySensor: No power data or car disconnected - reporting False") self._last_reading = self.coordinator.data self._last_updated = utcnow() diff --git a/custom_components/ohme/const.py b/custom_components/ohme/const.py index b3445f5..6f5e008 100644 --- a/custom_components/ohme/const.py +++ b/custom_components/ohme/const.py @@ -1,6 +1,8 @@ """Component constants""" - DOMAIN = "ohme" +USER_AGENT = "dan-r-homeassistant-ohme" +INTEGRATION_VERSION = "0.2.7" + DATA_CLIENT = "client" DATA_COORDINATORS = "coordinators" COORDINATOR_CHARGESESSIONS = 0 diff --git a/custom_components/ohme/time.py b/custom_components/ohme/time.py index 4577428..19f406b 100644 --- a/custom_components/ohme/time.py +++ b/custom_components/ohme/time.py @@ -35,7 +35,7 @@ def __init__(self, coordinator, hass: HomeAssistant, client): self._client = client - self._state = 0 + self._state = None self._last_updated = None self._attributes = {} @@ -64,11 +64,12 @@ def icon(self): @property def native_value(self): """Get value from data returned from API by coordinator""" - if self.coordinator.data and self.coordinator.data['appliedRule']: + # Make sure we're not pending approval, as this sets the target time to now + if self.coordinator.data and self.coordinator.data['appliedRule'] and self.coordinator.data['mode'] != "PENDING_APPROVAL": target = self.coordinator.data['appliedRule']['targetTime'] - return dt_time( + self._state = dt_time( hour=target // 3600, minute=(target % 3600) // 60, second=0 ) - return None + return self._state diff --git a/custom_components/ohme/translations/en.json b/custom_components/ohme/translations/en.json index 70383f2..14a1a76 100644 --- a/custom_components/ohme/translations/en.json +++ b/custom_components/ohme/translations/en.json @@ -11,7 +11,7 @@ } }, "error": { - "auth_error": "Invalid credentials provided." + "auth_error": "Invalid credentials provided. Please ensure you are using an Ohme account and not a social account (eg. Google)." }, "abort": {} }, @@ -27,7 +27,7 @@ } }, "error": { - "auth_error": "Invalid credentials provided." + "auth_error": "Invalid credentials provided. Please ensure you are using an Ohme account and not a social account (eg. Google)." }, "abort": {} },