Skip to content

Commit

Permalink
Tweaked charge state detection and added user-agent (#28)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dan-r authored Jan 2, 2024
1 parent 616a93e commit 8956d41
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 14 deletions.
5 changes: 3 additions & 2 deletions custom_components/ohme/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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):
Expand Down
47 changes: 42 additions & 5 deletions custom_components/ohme/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion custom_components/ohme/const.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 5 additions & 4 deletions custom_components/ohme/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions custom_components/ohme/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
},
Expand All @@ -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": {}
},
Expand Down

0 comments on commit 8956d41

Please sign in to comment.