Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions homeassistant/components/peblar/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@
"delete_vehicle_token": {
"service": "mdi:car-off"
},
"get_meter_history": {
"service": "mdi:history"
},
"list_rfid_tokens": {
"service": "mdi:card-account-details"
},
Expand Down
88 changes: 87 additions & 1 deletion homeassistant/components/peblar/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
)
import voluptuous as vol

from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_ALIAS, CONF_DESCRIPTION
from homeassistant.const import (
ATTR_CONFIG_ENTRY_ID,
CONF_AFTER,
CONF_ALIAS,
CONF_BEFORE,
CONF_DESCRIPTION,
)
from homeassistant.core import (
HomeAssistant,
ServiceCall,
Expand All @@ -26,6 +32,8 @@
async_get_config_entry,
async_register_admin_service,
)
from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonValueType

from .const import CONF_EVCC_ID, CONF_UID, DOMAIN
from .coordinator import PeblarConfigEntry
Expand All @@ -35,11 +43,19 @@
SERVICE_ADD_VEHICLE_TOKEN = "add_vehicle_token"
SERVICE_DELETE_RFID_TOKEN = "delete_rfid_token"
SERVICE_DELETE_VEHICLE_TOKEN = "delete_vehicle_token"
SERVICE_GET_METER_HISTORY = "get_meter_history"
SERVICE_LIST_RFID_TOKENS = "list_rfid_tokens"
SERVICE_LIST_VEHICLE_TOKENS = "list_vehicle_tokens"

CHARGER_SCHEMA = vol.Schema({vol.Required(ATTR_CONFIG_ENTRY_ID): str})

METER_HISTORY_SCHEMA = CHARGER_SCHEMA.extend(
{
vol.Optional(CONF_AFTER): cv.datetime,
vol.Optional(CONF_BEFORE): cv.datetime,
}
)

TOKEN_SCHEMA = CHARGER_SCHEMA.extend({vol.Required(CONF_UID): str})
ADD_TOKEN_SCHEMA = TOKEN_SCHEMA.extend({vol.Required(CONF_DESCRIPTION): str})

Expand All @@ -59,6 +75,12 @@
)


def _get_peblar(hass: HomeAssistant, entry_id: str) -> Peblar:
"""Return the client for a charger, whatever hardware it carries."""
entry: PeblarConfigEntry = async_get_config_entry(hass, DOMAIN, entry_id)
return entry.runtime_data.user_configuration_coordinator.peblar


def _get_rfid_peblar(hass: HomeAssistant, entry_id: str) -> Peblar:
"""Return the client, for a charger that has an RFID reader.

Expand Down Expand Up @@ -213,6 +235,62 @@ async def _handle_authorize_charge_session(call: ServiceCall) -> None:
name=call.data.get(CONF_DESCRIPTION),
)

async def _handle_get_meter_history(call: ServiceCall) -> ServiceResponse:
entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
peblar = _get_peblar(hass, entry_id)

# A naive moment is read by the charger as its own local time, which
# is not necessarily the one Home Assistant runs in. Both bounds are
# anchored here instead, the way the datetime platform does it.
after = call.data.get(CONF_AFTER)
before = call.data.get(CONF_BEFORE)
async with _handle_peblar_errors(hass, entry_id):
history = await peblar.meter_history(
start=None if after is None else dt_util.as_utc(after),
stop=None if before is None else dt_util.as_utc(before),
)

sessions: list[JsonValueType] = []
for index, session in enumerate(history.session):
# The session that is running has no end yet, and so no energy
# total to report for it either.
end_energy_kwh = None
end_time = None
energy_kwh = None
if (end_energy := session.session_end_energy_mwh) is not None:
end_energy_kwh = end_energy / 1000000
energy_kwh = (end_energy - session.session_start_energy_mwh) / 1000000
if session.session_end_time is not None:
end_time = dt_util.utc_from_timestamp(
session.session_end_time
).isoformat()
sessions.append(
{
"session_number": session.session_number,
CONF_UID: session.auth_token,
"start_time": dt_util.utc_from_timestamp(
session.session_start_time
).isoformat(),
"end_time": end_time,
"start_energy_kwh": session.session_start_energy_mwh / 1000000,
"end_energy_kwh": end_energy_kwh,
"energy_kwh": energy_kwh,
"checksum": session.checksum,
# The charger checks each record against its own checksum
# and reports the outcomes as a list of their own. A
# charger that returns fewer of those than it returns
# sessions leaves the rest unanswered, rather than
# unreported.
"corrupted": (
history.corrupted_session[index]
if index < len(history.corrupted_session)
else None
),
}
)

return {"corrupted": history.corrupted, "sessions": sessions}

async def _handle_list_vehicle_tokens(call: ServiceCall) -> ServiceResponse:
entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
peblar = _get_autocharge_peblar(hass, entry_id)
Expand Down Expand Up @@ -291,3 +369,11 @@ async def _handle_delete_vehicle_token(call: ServiceCall) -> None:
_handle_authorize_charge_session,
schema=AUTHORIZE_SCHEMA,
)
async_register_admin_service(
hass,
DOMAIN,
SERVICE_GET_METER_HISTORY,
_handle_get_meter_history,
schema=METER_HISTORY_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
14 changes: 14 additions & 0 deletions homeassistant/components/peblar/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,17 @@ authorize_charge_session:
description:
selector:
text:

get_meter_history:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: peblar
after:
selector:
datetime:
before:
selector:
datetime:
18 changes: 18 additions & 0 deletions homeassistant/components/peblar/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,24 @@
},
"name": "Delete autocharge vehicle"
},
"get_meter_history": {
"description": "Returns the charging sessions the charger's certified meter has recorded, including the token each session was authorized with.",
"fields": {
"after": {
"description": "Only return sessions that started at or after this moment.",
"name": "After"
},
"before": {
"description": "Only return sessions that started at or before this moment.",
"name": "Before"
},
"config_entry_id": {
"description": "The Peblar EV charger to read the meter history from.",
"name": "Peblar EV charger"
}
},
"name": "Get meter history"
},
"list_rfid_tokens": {
"description": "Returns the RFID tokens configured in the charger's standalone authorization list.",
"fields": {
Expand Down
4 changes: 4 additions & 0 deletions tests/components/peblar/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from peblar import (
PeblarEVInterface,
PeblarMeter,
PeblarMeterHistory,
PeblarSystem,
PeblarSystemInformation,
PeblarUserConfiguration,
Expand Down Expand Up @@ -80,6 +81,9 @@ def mock_peblar(request: pytest.FixtureRequest) -> Generator[MagicMock]:
peblar.system_information.return_value = PeblarSystemInformation.from_dict(
system_information
)
peblar.meter_history.return_value = PeblarMeterHistory.from_json(
load_fixture("meter_history.json", DOMAIN)
)

# The event stream parks here until the entry unloads, the way a
# real one waits on the charger rather than returning.
Expand Down
32 changes: 32 additions & 0 deletions tests/components/peblar/fixtures/meter_history.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"Corrupted": false,
"CorruptedSession": [true, false],
"MetaData": {
"MeterHash": "0ac1e2b3c4d5e6f7",
"MeterVersion": "1.0.0",
"MidCertified": true,
"ProductPn": "PBLR-0000",
"ProductSn": "23-45-A4O-MOF",
"TimeZone": "Europe/Amsterdam"
},
"Session": [
{
"AuthToken": "AA:BB:CC:DD",
"Checksum": 42,
"SessionNumber": 1,
"SessionStartEnergymWh": 1000000,
"SessionStartTime": 1769083200,
"SessionEndEnergymWh": 8500000,
"SessionEndTime": 1769108400
},
{
"AuthToken": null,
"Checksum": 43,
"SessionNumber": 2,
"SessionStartEnergymWh": 8500000,
"SessionStartTime": 1769169600,
"SessionEndEnergymWh": null,
"SessionEndTime": null
}
]
}
Loading
Loading