Skip to content

Commit

Permalink
Added charge slot active sensor
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-r committed Dec 30, 2023
1 parent 7cafdba commit 18ef3a5
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 21 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ This integration exposes the following entities:
* Car Connected - On when a car is plugged in
* Car Charging - On when a car is connected and drawing power
* Pending Approval - On when a car is connected and waiting for approval
* Charge Slot Active - On when a charge slot is in progress according to the Ohme-generated charge plan
* Sensors (Charge power) - **These are only available during a charge session**
* Power Draw (Watts) - Power draw of connected car
* Current Draw (Amps) - Current draw of connected car
Expand Down
72 changes: 65 additions & 7 deletions custom_components/ohme/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
BinarySensorEntity
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.util.dt import (utcnow)
from .const import DOMAIN, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, DATA_CLIENT
from .coordinator import OhmeChargeSessionsCoordinator
from .utils import charge_graph_in_slot


async def async_setup_entry(
Expand All @@ -21,14 +23,15 @@ async def async_setup_entry(
client = hass.data[DOMAIN][DATA_CLIENT]
coordinator = hass.data[DOMAIN][DATA_COORDINATORS][COORDINATOR_CHARGESESSIONS]

sensors = [ConnectedSensor(coordinator, hass, client),
ChargingSensor(coordinator, hass, client),
PendingApprovalSensor(coordinator, hass, client)]
sensors = [ConnectedBinarySensor(coordinator, hass, client),
ChargingBinarySensor(coordinator, hass, client),
PendingApprovalBinarySensor(coordinator, hass, client),
CurrentSlotBinarySensor(coordinator, hass, client)]

async_add_entities(sensors, update_before_add=True)


class ConnectedSensor(
class ConnectedBinarySensor(
CoordinatorEntity[OhmeChargeSessionsCoordinator],
BinarySensorEntity):
"""Binary sensor for if car is plugged in."""
Expand Down Expand Up @@ -74,7 +77,7 @@ def is_on(self) -> bool:
return self._state


class ChargingSensor(
class ChargingBinarySensor(
CoordinatorEntity[OhmeChargeSessionsCoordinator],
BinarySensorEntity):
"""Binary sensor for if car is charging."""
Expand Down Expand Up @@ -121,7 +124,7 @@ def is_on(self) -> bool:
return self._state


class PendingApprovalSensor(
class PendingApprovalBinarySensor(
CoordinatorEntity[OhmeChargeSessionsCoordinator],
BinarySensorEntity):
"""Binary sensor for if a charge is pending approval."""
Expand Down Expand Up @@ -165,3 +168,58 @@ def is_on(self) -> bool:
self.coordinator.data["mode"] == "PENDING_APPROVAL")

return self._state


class CurrentSlotBinarySensor(
CoordinatorEntity[OhmeChargeSessionsCoordinator],
BinarySensorEntity):
"""Binary sensor for if we are currently in a smart charge slot."""

_attr_name = "Charge Slot Active"

def __init__(
self,
coordinator: OhmeChargeSessionsCoordinator,
hass: HomeAssistant,
client):
super().__init__(coordinator=coordinator)

self._attributes = {}
self._last_updated = None
self._state = False
self._client = client

self.entity_id = generate_entity_id(
"binary_sensor.{}", "ohme_slot_active", hass=hass)

self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info(
)

@property
def icon(self):
"""Icon of the sensor."""
return "mdi:calendar-check"

@property
def unique_id(self) -> str:
"""Return the unique ID of the sensor."""
return self._client.get_unique_id("ohme_slot_active")

@property
def is_on(self) -> bool:
return self._state

@callback
def _handle_coordinator_update(self) -> None:
"""Are we in a charge slot? This is a bit slow so we only update on coordinator data update."""
if self.coordinator.data is None:
self._state = None
elif self.coordinator.data["mode"] == "DISCONNECTED":
self._state = False
else:
self._state = charge_graph_in_slot(
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])

self._last_updated = utcnow()

self.async_write_ha_state()
12 changes: 6 additions & 6 deletions custom_components/ohme/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.util.dt import (utcnow)
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_STATISTICS, COORDINATOR_ADVANCED
from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator
from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAdvancedSettingsCoordinator
from .utils import charge_graph_next_slot


Expand Down Expand Up @@ -165,15 +165,15 @@ def native_value(self):
return None


class CTSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity):
class CTSensor(CoordinatorEntity[OhmeAdvancedSettingsCoordinator], SensorEntity):
"""Sensor for car power draw."""
_attr_name = "CT Reading"
_attr_device_class = SensorDeviceClass.CURRENT
_attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE

def __init__(
self,
coordinator: OhmeChargeSessionsCoordinator,
coordinator: OhmeAdvancedSettingsCoordinator,
hass: HomeAssistant,
client):
super().__init__(coordinator=coordinator)
Expand Down Expand Up @@ -215,7 +215,7 @@ class EnergyUsageSensor(CoordinatorEntity[OhmeStatisticsCoordinator], SensorEnti

def __init__(
self,
coordinator: OhmeChargeSessionsCoordinator,
coordinator: OhmeStatisticsCoordinator,
hass: HomeAssistant,
client):
super().__init__(coordinator=coordinator)
Expand Down Expand Up @@ -250,7 +250,7 @@ def native_value(self):
return None


class NextSlotStartSensor(CoordinatorEntity[OhmeStatisticsCoordinator], SensorEntity):
class NextSlotStartSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity):
"""Sensor for next smart charge slot start time."""
_attr_name = "Next Charge Slot Start"
_attr_device_class = SensorDeviceClass.TIMESTAMP
Expand Down Expand Up @@ -302,7 +302,7 @@ def _handle_coordinator_update(self) -> None:
self.async_write_ha_state()


class NextSlotEndSensor(CoordinatorEntity[OhmeStatisticsCoordinator], SensorEntity):
class NextSlotEndSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity):
"""Sensor for next smart charge slot end time."""
_attr_name = "Next Charge Slot End"
_attr_device_class = SensorDeviceClass.TIMESTAMP
Expand Down
38 changes: 30 additions & 8 deletions custom_components/ohme/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
import pytz


def charge_graph_next_slot(charge_start, points):
"""Get the next charge slot from a list of graph points."""
# Get start and current timestamp in seconds
def _format_charge_graph(charge_start, points):
"""Convert relative time in points array to real timestamp (s)."""

charge_start = round(charge_start / 1000)
now = int(time())
return [{"t": x["x"] + charge_start, "y": x["y"]} for x in points]


# Replace relative timestamp (seconds) with real timestamp
data = [{"t": x["x"] + charge_start, "y": x["y"]} for x in points]
def charge_graph_next_slot(charge_start, points):
"""Get the next charge slot start/end times from a list of graph points."""
now = int(time())
data = _format_charge_graph(charge_start, points)

# Filter to points from now onwards
data = [x for x in data if x["t"] > now]
Expand All @@ -32,9 +35,10 @@ def charge_graph_next_slot(charge_start, points):
if delta > 10 and not start_ts:
# 1s added here as it otherwise often rounds down to xx:59:59
start_ts = data[idx]["t"] + 1
elif start_ts and delta == 0: # If we have seen a start and see a delta of 0, this is the end

# Take the first delta of 0 as the end
if delta == 0 and not end_ts:
end_ts = data[idx]["t"] + 1
break

# These need to be presented with tzinfo or Home Assistant will reject them
return {
Expand All @@ -43,6 +47,24 @@ def charge_graph_next_slot(charge_start, points):
}


def charge_graph_in_slot(charge_start, points):
"""Are we currently in a charge slot?"""
now = int(time())
data = _format_charge_graph(charge_start, points)

# Loop through every value, skipping the last
for idx in range(0, len(data) - 1):
# This is our current point
if data[idx]["t"] < now and data[idx + 1]["t"] > now:
# If the delta line we are on is steeper than 10,
# we are in a charge slot.
if data[idx + 1]["y"] - data[idx]["y"] > 10:
return True
break

return False


def time_next_occurs(hour, minute):
"""Find when this time next occurs."""
current = datetime.now()
Expand Down

0 comments on commit 18ef3a5

Please sign in to comment.