Skip to content

Commit

Permalink
Add ChargeOverride deletion and support new statuses (#52)
Browse files Browse the repository at this point in the history
* Adds ChargeOverride deletion service
* Adds new statuses from Pod Point app
* Adds new `Pending` status when waiting for updates from the API
* Adds diagnostic sensors for WiFi signal, cloud connection status and last message timestamp
* Updates tests
  • Loading branch information
mattrayner authored Apr 8, 2024
1 parent 205eb17 commit 6390ffd
Show file tree
Hide file tree
Showing 30 changed files with 709 additions and 569 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Platform | Description
`sensor` (Balance) | Shows the balance on your PodPoint account.
`sensor` (Charge Mode) | Shows the charge mode your pod is currently in/
`sensor` (***Charge Override End Time) | Shows the end time for any configured 'charge now' override.
`sensor` (Signal Strength) | Shows WiFi signal strength of a given pod.
`sensor` (Last message received) | When was a message last received from a given pod.
`sensor` (Cloud connection status) | Status of pods connection to the cloud.
`switch` (****Allow Charging) | Enable/disable charging by enabling/disabling a schedule.
`switch` (Smart Charge Mode) | Enable the switch for 'Smart' charge mode, disable it for 'Manual' charge mode.
`update` (Firmware Update) | Shows the current firmware version for your device and alerts if an update is available
Expand Down
2 changes: 2 additions & 0 deletions custom_components/pod_point/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
For more details about this integration, please refer to
https://github.com/mattrayner/pod-point-home-assistant-component
"""

import asyncio
from datetime import timedelta
import logging
Expand Down Expand Up @@ -77,6 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
coordinator = PodPointDataUpdateCoordinator(
hass, client=client, scan_interval=scan_interval
)

# Check the credentials we have and ensure that we can perform a refresh
await coordinator.async_config_entry_first_refresh()

Expand Down
62 changes: 48 additions & 14 deletions custom_components/pod_point/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Binary sensor platform for pod_point."""

import logging
from typing import Any, Dict

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.helpers.entity import EntityCategory

from .const import ATTR_STATE, DOMAIN
from .const import ATTR_STATE, DOMAIN, ATTRIBUTION, ATTR_CONNECTION_STATE_ONLINE
from .coordinator import PodPointDataUpdateCoordinator
from .entity import PodPointEntity

Expand All @@ -23,28 +25,24 @@ async def async_setup_entry(hass, entry, async_add_devices):

sensors = []
for i in range(len(coordinator.data)):
sensor = PodPointBinarySensor(coordinator, entry, i)
sensor.pod_id = i
sensors.append(sensor)
cable_sensor = PodPointCableConnectionSensor(coordinator, entry, i)
cable_sensor.pod_id = i
sensors.append(cable_sensor)

cloud_sensor = PodPointCloudConnectionSensor(coordinator, entry, i)
cloud_sensor.pod_id = i
sensors.append(cloud_sensor)

async_add_devices(sensors)


class PodPointBinarySensor(PodPointEntity, BinarySensorEntity):
"""pod_point binary_sensor class."""
class PodPointCableConnectionSensor(PodPointEntity, BinarySensorEntity):
"""pod_point cable connection class."""

_attr_has_entity_name = True
_attr_name = "Cable Status"
_attr_device_class = BinarySensorDeviceClass.PLUG

@property
def extra_state_attributes(self) -> Dict[str, Any]:
state = super().extra_state_attributes.get(ATTR_STATE, "")
return {
ATTR_STATE: state,
"current_kwhs": self.pod.current_kwh,
}

@property
def unique_id(self):
return f"{super().unique_id}_cable_status"
Expand All @@ -53,3 +51,39 @@ def unique_id(self):
def is_on(self):
"""Return true if the binary_sensor is on."""
return self.connected


class PodPointCloudConnectionSensor(PodPointEntity, BinarySensorEntity):
"""pod_point cloud connection class."""

_attr_has_entity_name = True
_attr_name = "Cloud Connection"
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
_attr_entity_category = EntityCategory.DIAGNOSTIC

@property
def unique_id(self):
return f"{super().unique_id}_cloud_connection"

@property
def is_on(self):
"""Return true if the binary_sensor is on."""
if self.pod is None:
return False

if self.pod.connectivity_status is None:
return False

return (
self.pod.connectivity_status.connectivity_status
== ATTR_CONNECTION_STATE_ONLINE
)

@property
def icon(self):
"""Return the icon of the sensor."""

if self.is_on:
return "mdi:cloud-check-variant"

return "mdi:cloud-off"
1 change: 1 addition & 0 deletions custom_components/pod_point/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Adds config flow for Pod Point."""

import logging
from typing import Dict

Expand Down
10 changes: 10 additions & 0 deletions custom_components/pod_point/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Constants for pod_point."""

from podpointclient.version import __version__ as pod_point_client_version

from .version import __version__ as integration_version
Expand Down Expand Up @@ -28,6 +29,7 @@
ENERGY = "energy"

SERVICE_CHARGE_NOW = "charge_now"
SERVICE_STOP_CHARGE_NOW = "stop_charge_now"

# Configuration and options
CONF_ENABLED = "enabled"
Expand Down Expand Up @@ -85,16 +87,24 @@
ATTR_STATE_AVAILABLE = "available"
ATTR_STATE_UNAVAILABLE = "unavailable"
ATTR_STATE_CHARGING = "charging"
ATTR_STATE_IDLE = "idle"
ATTR_STATE_SUSPENDED_EV = "suspended-ev"
ATTR_STATE_SUSPENDED_EVSE = "suspended-evse"
ATTR_STATE_PENDING = "pending"
ATTR_STATE_OUT_OF_SERVICE = "out-of-service"
ATTR_STATE_WAITING = "waiting-for-schedule"
ATTR_STATE_CONNECTED_WAITING = "connected-waiting-for-schedule"
ATTR_STATE_CHARGE_OVERRIED = "charge-override"
ATTR_STATE_RANKING = [
ATTR_STATE_AVAILABLE,
ATTR_STATE_UNAVAILABLE,
ATTR_STATE_IDLE,
ATTR_STATE_CHARGING,
ATTR_STATE_SUSPENDED_EV,
ATTR_STATE_OUT_OF_SERVICE,
ATTR_STATE_SUSPENDED_EVSE,
]
ATTR_CONNECTION_STATE_ONLINE = "ONLINE"

ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_HOURS = "hours"
Expand Down
70 changes: 52 additions & 18 deletions custom_components/pod_point/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""
Data coordinator for pod point client
"""

import logging
from typing import Dict, List, Set, Tuple

import pytz
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import issue_registry as ir
Expand All @@ -14,6 +16,8 @@
from podpointclient.pod import Firmware, Pod
from podpointclient.user import User

from datetime import datetime, timedelta

from .const import DOMAIN, LIMITED_POD_INCLUDES

_LOGGER: logging.Logger = logging.getLogger(__package__)
Expand All @@ -25,10 +29,7 @@ class PodPointDataUpdateCoordinator(DataUpdateCoordinator):
_firmware_refresh_interval = 5 # How many refreshes between a firmware update call

def __init__(
self,
hass: HomeAssistant,
client: PodPointClient,
scan_interval: int
self, hass: HomeAssistant, client: PodPointClient, scan_interval: timedelta
) -> None:
"""Initialize."""
self.api: PodPointClient = client
Expand All @@ -45,6 +46,7 @@ def __init__(
self.online = None
self.firmware_refresh = 1 # Initial refresh will be a firmware refresh too, ensuring we pull firmware for all pods at startup
self.user: User = None
self.last_message_at = datetime(1970, 1, 1, 0, 0, 0, 0, pytz.UTC)

super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=scan_interval)

Expand All @@ -56,6 +58,7 @@ async def _async_update_data(self):
self.pod_dict: Dict[int, Pod] = None

self.user = await self.api.async_get_user()

new_pods = await self.__async_update_pods()

_LOGGER.debug(
Expand All @@ -68,14 +71,23 @@ async def _async_update_data(self):
# they were performed on
new_pods_by_id = self.__group_pods_by_unit_id(pods=new_pods)

(new_pods, new_pods_by_id) = await self.__async_group_pods(new_pods, new_pods_by_id)
(new_pods, new_pods_by_id) = await self.__async_group_pods(
new_pods, new_pods_by_id
)

new_pods_by_id = self.__group_pods_by_unit_id(pods=new_pods)

# Fetch firmware data for pods, if it is needed
self.firmware_refresh -= 1
if self.firmware_refresh <= 0:
new_pods_by_id = await self.__async_refresh_firmware(new_pods, new_pods_by_id)
new_pods_by_id = await self.__async_refresh_firmware(
new_pods, new_pods_by_id
)

# Fetch connection status data for pods
new_pods_by_id = await self.__async_update_pod_connection_status(
new_pods_by_id
)

# Determine if we should fetch for all charges, or just the most recent for a user.
should_fetch_all_charges = self.__should_fetch_all_charges(
Expand Down Expand Up @@ -302,17 +314,13 @@ async def __async_update_pods(self) -> List[Pod]:
# Should we get a limited set of data (subsiquent refreshes)
if len(self.pods) > 0:
_LOGGER.debug("Existing pods found, performing a limited data pull")
return await self.api.async_get_all_pods(
includes=LIMITED_POD_INCLUDES
)
return await self.api.async_get_all_pods(includes=LIMITED_POD_INCLUDES)
else:
_LOGGER.debug("No existing pods found, performing a full data pull")
return await self.api.async_get_all_pods()

async def __async_group_pods(
self,
new_pods,
new_pods_by_id
self, new_pods, new_pods_by_id
) -> Tuple[List[Pod], Dict[str, Pod]]:
# Attempt to update our new pods with additional data from the existing pods.
# This allows us to query less data each refresh, kinder on the Pod Point APIs.
Expand All @@ -332,16 +340,12 @@ async def __async_group_pods(
return (new_pods, new_pods_by_id)

async def __async_refresh_firmware(
self,
new_pods: List[Pod],
new_pods_by_id: Dict[str, List[Pod]]
self, new_pods: List[Pod], new_pods_by_id: Dict[str, List[Pod]]
) -> Dict[str, List[Pod]]:
_LOGGER.debug("=== FIRMWARE STATUS UPDATE ===")

for pod in new_pods:
pod_firmwares: List[Firmware] = await self.api.async_get_firmware(
pod=pod
)
pod_firmwares: List[Firmware] = await self.api.async_get_firmware(pod=pod)

if len(pod_firmwares) <= 0:
_LOGGER.warning(
Expand All @@ -361,3 +365,33 @@ async def __async_refresh_firmware(
self.firmware_refresh = self._firmware_refresh_interval

return new_pods_by_id

async def __async_update_pod_connection_status(
self, new_pods_by_id: Dict[str, List[Pod]]
) -> Dict[str, List[Pod]]:
_LOGGER.debug("=== POD CONNECTION STATUS UPDATE ===")

# flat_pods = [item for row in new_pods_by_id.values() for item in row]
# Fetch connection status for each pod
for pod in new_pods_by_id.values():
connectivity_status = await self.api.async_get_connectivity_status(pod=pod)

if connectivity_status is not None:
pod.connectivity_status = connectivity_status
pod.last_message_at = connectivity_status.last_message_at
pod.charging_state = connectivity_status.charging_state

pod.statuses.append(
Pod.Status(
99,
connectivity_status.charging_state,
connectivity_status.charging_state,
connectivity_status.charging_state,
"A",
1,
)
)

new_pods_by_id[pod.unit_id] = pod

return new_pods_by_id
Loading

0 comments on commit 6390ffd

Please sign in to comment.