Skip to content

Commit 6390ffd

Browse files
authored
Add ChargeOverride deletion and support new statuses (#52)
* 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
1 parent 205eb17 commit 6390ffd

30 files changed

+709
-569
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Platform | Description
2929
`sensor` (Balance) | Shows the balance on your PodPoint account.
3030
`sensor` (Charge Mode) | Shows the charge mode your pod is currently in/
3131
`sensor` (***Charge Override End Time) | Shows the end time for any configured 'charge now' override.
32+
`sensor` (Signal Strength) | Shows WiFi signal strength of a given pod.
33+
`sensor` (Last message received) | When was a message last received from a given pod.
34+
`sensor` (Cloud connection status) | Status of pods connection to the cloud.
3235
`switch` (****Allow Charging) | Enable/disable charging by enabling/disabling a schedule.
3336
`switch` (Smart Charge Mode) | Enable the switch for 'Smart' charge mode, disable it for 'Manual' charge mode.
3437
`update` (Firmware Update) | Shows the current firmware version for your device and alerts if an update is available

custom_components/pod_point/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
For more details about this integration, please refer to
55
https://github.com/mattrayner/pod-point-home-assistant-component
66
"""
7+
78
import asyncio
89
from datetime import timedelta
910
import logging
@@ -77,6 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
7778
coordinator = PodPointDataUpdateCoordinator(
7879
hass, client=client, scan_interval=scan_interval
7980
)
81+
8082
# Check the credentials we have and ensure that we can perform a refresh
8183
await coordinator.async_config_entry_first_refresh()
8284

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""Binary sensor platform for pod_point."""
2+
23
import logging
34
from typing import Any, Dict
45

56
from homeassistant.components.binary_sensor import (
67
BinarySensorDeviceClass,
78
BinarySensorEntity,
89
)
10+
from homeassistant.helpers.entity import EntityCategory
911

10-
from .const import ATTR_STATE, DOMAIN
12+
from .const import ATTR_STATE, DOMAIN, ATTRIBUTION, ATTR_CONNECTION_STATE_ONLINE
1113
from .coordinator import PodPointDataUpdateCoordinator
1214
from .entity import PodPointEntity
1315

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

2426
sensors = []
2527
for i in range(len(coordinator.data)):
26-
sensor = PodPointBinarySensor(coordinator, entry, i)
27-
sensor.pod_id = i
28-
sensors.append(sensor)
28+
cable_sensor = PodPointCableConnectionSensor(coordinator, entry, i)
29+
cable_sensor.pod_id = i
30+
sensors.append(cable_sensor)
31+
32+
cloud_sensor = PodPointCloudConnectionSensor(coordinator, entry, i)
33+
cloud_sensor.pod_id = i
34+
sensors.append(cloud_sensor)
2935

3036
async_add_devices(sensors)
3137

3238

33-
class PodPointBinarySensor(PodPointEntity, BinarySensorEntity):
34-
"""pod_point binary_sensor class."""
39+
class PodPointCableConnectionSensor(PodPointEntity, BinarySensorEntity):
40+
"""pod_point cable connection class."""
3541

3642
_attr_has_entity_name = True
3743
_attr_name = "Cable Status"
3844
_attr_device_class = BinarySensorDeviceClass.PLUG
3945

40-
@property
41-
def extra_state_attributes(self) -> Dict[str, Any]:
42-
state = super().extra_state_attributes.get(ATTR_STATE, "")
43-
return {
44-
ATTR_STATE: state,
45-
"current_kwhs": self.pod.current_kwh,
46-
}
47-
4846
@property
4947
def unique_id(self):
5048
return f"{super().unique_id}_cable_status"
@@ -53,3 +51,39 @@ def unique_id(self):
5351
def is_on(self):
5452
"""Return true if the binary_sensor is on."""
5553
return self.connected
54+
55+
56+
class PodPointCloudConnectionSensor(PodPointEntity, BinarySensorEntity):
57+
"""pod_point cloud connection class."""
58+
59+
_attr_has_entity_name = True
60+
_attr_name = "Cloud Connection"
61+
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
62+
_attr_entity_category = EntityCategory.DIAGNOSTIC
63+
64+
@property
65+
def unique_id(self):
66+
return f"{super().unique_id}_cloud_connection"
67+
68+
@property
69+
def is_on(self):
70+
"""Return true if the binary_sensor is on."""
71+
if self.pod is None:
72+
return False
73+
74+
if self.pod.connectivity_status is None:
75+
return False
76+
77+
return (
78+
self.pod.connectivity_status.connectivity_status
79+
== ATTR_CONNECTION_STATE_ONLINE
80+
)
81+
82+
@property
83+
def icon(self):
84+
"""Return the icon of the sensor."""
85+
86+
if self.is_on:
87+
return "mdi:cloud-check-variant"
88+
89+
return "mdi:cloud-off"

custom_components/pod_point/config_flow.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Adds config flow for Pod Point."""
2+
23
import logging
34
from typing import Dict
45

custom_components/pod_point/const.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Constants for pod_point."""
2+
23
from podpointclient.version import __version__ as pod_point_client_version
34

45
from .version import __version__ as integration_version
@@ -28,6 +29,7 @@
2829
ENERGY = "energy"
2930

3031
SERVICE_CHARGE_NOW = "charge_now"
32+
SERVICE_STOP_CHARGE_NOW = "stop_charge_now"
3133

3234
# Configuration and options
3335
CONF_ENABLED = "enabled"
@@ -85,16 +87,24 @@
8587
ATTR_STATE_AVAILABLE = "available"
8688
ATTR_STATE_UNAVAILABLE = "unavailable"
8789
ATTR_STATE_CHARGING = "charging"
90+
ATTR_STATE_IDLE = "idle"
91+
ATTR_STATE_SUSPENDED_EV = "suspended-ev"
92+
ATTR_STATE_SUSPENDED_EVSE = "suspended-evse"
93+
ATTR_STATE_PENDING = "pending"
8894
ATTR_STATE_OUT_OF_SERVICE = "out-of-service"
8995
ATTR_STATE_WAITING = "waiting-for-schedule"
9096
ATTR_STATE_CONNECTED_WAITING = "connected-waiting-for-schedule"
9197
ATTR_STATE_CHARGE_OVERRIED = "charge-override"
9298
ATTR_STATE_RANKING = [
9399
ATTR_STATE_AVAILABLE,
94100
ATTR_STATE_UNAVAILABLE,
101+
ATTR_STATE_IDLE,
95102
ATTR_STATE_CHARGING,
103+
ATTR_STATE_SUSPENDED_EV,
96104
ATTR_STATE_OUT_OF_SERVICE,
105+
ATTR_STATE_SUSPENDED_EVSE,
97106
]
107+
ATTR_CONNECTION_STATE_ONLINE = "ONLINE"
98108

99109
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
100110
ATTR_HOURS = "hours"

custom_components/pod_point/coordinator.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""
22
Data coordinator for pod point client
33
"""
4+
45
import logging
56
from typing import Dict, List, Set, Tuple
67

8+
import pytz
79
from homeassistant.core import HomeAssistant
810
from homeassistant.exceptions import ConfigEntryAuthFailed
911
from homeassistant.helpers import issue_registry as ir
@@ -14,6 +16,8 @@
1416
from podpointclient.pod import Firmware, Pod
1517
from podpointclient.user import User
1618

19+
from datetime import datetime, timedelta
20+
1721
from .const import DOMAIN, LIMITED_POD_INCLUDES
1822

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

2731
def __init__(
28-
self,
29-
hass: HomeAssistant,
30-
client: PodPointClient,
31-
scan_interval: int
32+
self, hass: HomeAssistant, client: PodPointClient, scan_interval: timedelta
3233
) -> None:
3334
"""Initialize."""
3435
self.api: PodPointClient = client
@@ -45,6 +46,7 @@ def __init__(
4546
self.online = None
4647
self.firmware_refresh = 1 # Initial refresh will be a firmware refresh too, ensuring we pull firmware for all pods at startup
4748
self.user: User = None
49+
self.last_message_at = datetime(1970, 1, 1, 0, 0, 0, 0, pytz.UTC)
4850

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

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

5860
self.user = await self.api.async_get_user()
61+
5962
new_pods = await self.__async_update_pods()
6063

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

71-
(new_pods, new_pods_by_id) = await self.__async_group_pods(new_pods, new_pods_by_id)
74+
(new_pods, new_pods_by_id) = await self.__async_group_pods(
75+
new_pods, new_pods_by_id
76+
)
7277

7378
new_pods_by_id = self.__group_pods_by_unit_id(pods=new_pods)
7479

7580
# Fetch firmware data for pods, if it is needed
7681
self.firmware_refresh -= 1
7782
if self.firmware_refresh <= 0:
78-
new_pods_by_id = await self.__async_refresh_firmware(new_pods, new_pods_by_id)
83+
new_pods_by_id = await self.__async_refresh_firmware(
84+
new_pods, new_pods_by_id
85+
)
86+
87+
# Fetch connection status data for pods
88+
new_pods_by_id = await self.__async_update_pod_connection_status(
89+
new_pods_by_id
90+
)
7991

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

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

334342
async def __async_refresh_firmware(
335-
self,
336-
new_pods: List[Pod],
337-
new_pods_by_id: Dict[str, List[Pod]]
343+
self, new_pods: List[Pod], new_pods_by_id: Dict[str, List[Pod]]
338344
) -> Dict[str, List[Pod]]:
339345
_LOGGER.debug("=== FIRMWARE STATUS UPDATE ===")
340346

341347
for pod in new_pods:
342-
pod_firmwares: List[Firmware] = await self.api.async_get_firmware(
343-
pod=pod
344-
)
348+
pod_firmwares: List[Firmware] = await self.api.async_get_firmware(pod=pod)
345349

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

363367
return new_pods_by_id
368+
369+
async def __async_update_pod_connection_status(
370+
self, new_pods_by_id: Dict[str, List[Pod]]
371+
) -> Dict[str, List[Pod]]:
372+
_LOGGER.debug("=== POD CONNECTION STATUS UPDATE ===")
373+
374+
# flat_pods = [item for row in new_pods_by_id.values() for item in row]
375+
# Fetch connection status for each pod
376+
for pod in new_pods_by_id.values():
377+
connectivity_status = await self.api.async_get_connectivity_status(pod=pod)
378+
379+
if connectivity_status is not None:
380+
pod.connectivity_status = connectivity_status
381+
pod.last_message_at = connectivity_status.last_message_at
382+
pod.charging_state = connectivity_status.charging_state
383+
384+
pod.statuses.append(
385+
Pod.Status(
386+
99,
387+
connectivity_status.charging_state,
388+
connectivity_status.charging_state,
389+
connectivity_status.charging_state,
390+
"A",
391+
1,
392+
)
393+
)
394+
395+
new_pods_by_id[pod.unit_id] = pod
396+
397+
return new_pods_by_id

0 commit comments

Comments
 (0)