Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -307,6 +307,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