Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/add-charging-v1-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@teslemetry/tesla-protocol": minor
---

Add a new `charging` source-of-record module modelling `com.tesla.proto.charging.v1`, the EV charge-session energy metering and per-session billing schema (`Energy`, `StemInfo`, `StemEventInfo`, `ChargeSessionTimeSeries`, `StemUi`, `StemBilling`) - based on our own observations and contributions from the community.
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This file is the project's committed home for project-intrinsic agent knowledge:
- `CarServer.Response.response_msg` (`proto/command/car_server.proto`) models the full server reply surface except fields 17 and 19, which are `reserved` with a reason comment (`centerdisplay.server`/`webrtc_comms` replies, app-only namespaces, intentionally excluded). Field 12 (`GetRateTariffResponse`) is modelled, mirroring the tariff document already declared for `SetRateTariffRequest` (tag 55). `scripts/check_reply_coverage.py` (run in CI) fails if any field in its known-field set is neither modelled in the oneof nor `reserved` with a comment - before adding a new reply oneof field, check whether its payload type pulls in an app-only namespace first, and either model it or add it to `KNOWN_REPLY_FIELDS` as a reserved entry.
- Un-reserving a field to model it (as field 12 above did) trips buf's `RESERVED_MESSAGE_NO_DELETE` breaking check even though it's a wire-compatible addition; `buf.yaml` excepts that rule repo-wide for this reason, so the same move for fields 17/19 later won't need a fresh exception.
- The committed `proto/command/` baseline already reflects one specific Tesla app build reconstructed via cross-version protobuf diffing (`app-4.58.6`, see `TESLEMETRY-EXT` markers). A later app-version-diff pass will find most candidate fields already modelled here - check the current `.proto` files for the exact field/tag before assuming it needs adding, and only model the genuine delta.
- `proto/charging/` is a documented exception to the "app-only namespaces are not published" policy (`PROVENANCE.md`): it has no BLE or Fleet API wire path today, but was published because it describes the vehicle-charging product domain. Any future app-only module wanting the same exception should be argued explicitly, not assumed from this precedent.

## Maintaining this file

Expand Down
8 changes: 7 additions & 1 deletion PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ directories no public upstream exists and this repo is the source of record.
| `proto/command/session.proto` | Teslemetry source-of-record | no upstream (Teslemetry session persistence) |
| `proto/energy_device/`, `proto/energy_command/` | Teslemetry source-of-record | no upstream (TEG gateway local API, from our own observations and contributions from the community) |
| `proto/teslapower/` | Teslemetry source-of-record | no upstream (Powerwall local API, from our own observations and contributions from the community, credit @brianhealey) |
| `proto/charging/` | Teslemetry source-of-record | no upstream (EV charge-session energy metering / billing schema, from our own observations and contributions from the community) |

## How the boundary is kept visible

Expand Down Expand Up @@ -52,4 +53,9 @@ policed for marker presence.
- Wire-compatible additions = minor; field/enum renames = major; tag
renumbering = forbidden (`buf breaking` gate).
- Tesla's internal app-only schema namespaces are deliberately NOT published
here; only consumer-needed command/telemetry/energy surfaces are.
here; only consumer-needed command/telemetry/energy surfaces are. `proto/charging/`
is a deliberate, narrow exception: it models the vehicle-charging product
domain even though no BLE or Fleet API surface exposes it as protobuf today
(the public Fleet API charging endpoints return JSON), because the schema
itself is a genuine, self-contained description of Tesla's charge-session
metering/billing data.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ proto/ source of truth (the only hand-edited artifacts)
├── telemetry/ fleet-telemetry protos (upstream verbatim)
├── energy_device/ TEG gateway local-API protos
├── energy_command/ energy command identifiers
└── teslapower/ Powerwall local-API proto
├── teslapower/ Powerwall local-API proto
└── charging/ EV charge-session metering/billing ("STEM") protos
packages/typescript/ @teslemetry/tesla-protocol (generated, committed)
packages/python/ tesla-protocol (generated, committed)
```
Expand Down
1 change: 1 addition & 0 deletions buf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ modules:
- path: proto/energy_device
- path: proto/energy_command
- path: proto/teslapower
- path: proto/charging
lint:
# Tesla's upstream style (package CarServer, enum value SpeedLimit, file-level
# layout) violates buf's naming rules; we inherit their style, we don't fix it.
Expand Down
Empty file.
1 change: 1 addition & 0 deletions packages/python/tesla_protocol/charging/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import charging_pb2
26 changes: 26 additions & 0 deletions packages/python/tesla_protocol/charging/charging_pb2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(_runtime_version.Domain.PUBLIC, 6, 31, 1, '', 'charging.proto')
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0echarging.proto\x12\x1bcom.tesla.proto.charging.v1"6\n\x06Energy\x12\x12\n\nwatt_hours\x18\x01 \x01(\r\x12\x18\n\x10milli_watt_hours\x18\x02 \x01(\r"\\\n\x08StemInfo\x12\x12\n\nfw_githash\x18\x01 \x01(\x0c\x12\x15\n\rserial_number\x18\x02 \x01(\t\x12\x13\n\x0bpart_number\x18\x03 \x01(\t\x12\x10\n\x08meter_id\x18\x04 \x01(\t"W\n\rStemEventInfo\x12\x16\n\x0eevent_occurred\x18\x01 \x01(\x08\x12\x13\n\x0binvalidated\x18\x02 \x01(\x08\x12\x19\n\x11time_synchronized\x18\x03 \x01(\x08"\xd1\x02\n\x17ChargeSessionTimeSeries\x12\x1a\n\x12start_time_epoch_s\x18\x01 \x01(\x04\x12\x18\n\x10end_time_epoch_s\x18\x02 \x01(\x04\x12\x1e\n\x16total_energy_vended_wh\x18\x03 \x01(\r\x12\x19\n\x11bucket_duration_s\x18\x04 \x01(\r\x12\x1f\n\x17first_bucket_duration_s\x18\x05 \x01(\r\x12\x1f\n\x17bucket_energy_vended_wh\x18\x06 \x03(\r\x12@\n\x13total_energy_vended\x18\x07 \x01(\x0b2#.com.tesla.proto.charging.v1.Energy\x12A\n\x14bucket_energy_vended\x18\x08 \x03(\x0b2#.com.tesla.proto.charging.v1.Energy"\x85\x02\n\x06StemUi\x128\n\tstem_info\x18\x01 \x01(\x0b2%.com.tesla.proto.charging.v1.StemInfo\x12\x12\n\nsession_id\x18\x02 \x01(\r\x12\x1b\n\x13system_time_epoch_s\x18\x03 \x01(\x04\x12\x13\n\x0bidle_time_s\x18\x04 \x01(\r\x12\x1a\n\x12start_time_epoch_s\x18\x05 \x01(\x04\x12C\n\x0fstem_event_info\x18\x06 \x01(\x0b2*.com.tesla.proto.charging.v1.StemEventInfo\x12\x1a\n\x12session_energy_kwh\x18\x07 \x01(\x02"\xd3\x02\n\x0bStemBilling\x128\n\tstem_info\x18\x01 \x01(\x0b2%.com.tesla.proto.charging.v1.StemInfo\x12\x12\n\nsession_id\x18\x02 \x01(\r\x12C\n\x0fstem_event_info\x18\x03 \x01(\x0b2*.com.tesla.proto.charging.v1.StemEventInfo\x12!\n\x19lifetime_energy_start_kwh\x18\x04 \x01(\x02\x12\x1f\n\x17lifetime_energy_end_kwh\x18\x05 \x01(\x02\x12\x13\n\x0bidle_time_s\x18\x06 \x01(\r\x12X\n\x1acharge_session_time_series\x18\x07 \x01(\x0b24.com.tesla.proto.charging.v1.ChargeSessionTimeSeriesb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'charging_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_ENERGY']._serialized_start = 47
_globals['_ENERGY']._serialized_end = 101
_globals['_STEMINFO']._serialized_start = 103
_globals['_STEMINFO']._serialized_end = 195
_globals['_STEMEVENTINFO']._serialized_start = 197
_globals['_STEMEVENTINFO']._serialized_end = 284
_globals['_CHARGESESSIONTIMESERIES']._serialized_start = 287
_globals['_CHARGESESSIONTIMESERIES']._serialized_end = 624
_globals['_STEMUI']._serialized_start = 627
_globals['_STEMUI']._serialized_end = 888
_globals['_STEMBILLING']._serialized_start = 891
_globals['_STEMBILLING']._serialized_end = 1230
104 changes: 104 additions & 0 deletions packages/python/tesla_protocol/charging/charging_pb2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor

class Energy(_message.Message):
__slots__ = ('watt_hours', 'milli_watt_hours')
WATT_HOURS_FIELD_NUMBER: _ClassVar[int]
MILLI_WATT_HOURS_FIELD_NUMBER: _ClassVar[int]
watt_hours: int
milli_watt_hours: int

def __init__(self, watt_hours: _Optional[int]=..., milli_watt_hours: _Optional[int]=...) -> None:
...

class StemInfo(_message.Message):
__slots__ = ('fw_githash', 'serial_number', 'part_number', 'meter_id')
FW_GITHASH_FIELD_NUMBER: _ClassVar[int]
SERIAL_NUMBER_FIELD_NUMBER: _ClassVar[int]
PART_NUMBER_FIELD_NUMBER: _ClassVar[int]
METER_ID_FIELD_NUMBER: _ClassVar[int]
fw_githash: bytes
serial_number: str
part_number: str
meter_id: str

def __init__(self, fw_githash: _Optional[bytes]=..., serial_number: _Optional[str]=..., part_number: _Optional[str]=..., meter_id: _Optional[str]=...) -> None:
...

class StemEventInfo(_message.Message):
__slots__ = ('event_occurred', 'invalidated', 'time_synchronized')
EVENT_OCCURRED_FIELD_NUMBER: _ClassVar[int]
INVALIDATED_FIELD_NUMBER: _ClassVar[int]
TIME_SYNCHRONIZED_FIELD_NUMBER: _ClassVar[int]
event_occurred: bool
invalidated: bool
time_synchronized: bool

def __init__(self, event_occurred: bool=..., invalidated: bool=..., time_synchronized: bool=...) -> None:
...

class ChargeSessionTimeSeries(_message.Message):
__slots__ = ('start_time_epoch_s', 'end_time_epoch_s', 'total_energy_vended_wh', 'bucket_duration_s', 'first_bucket_duration_s', 'bucket_energy_vended_wh', 'total_energy_vended', 'bucket_energy_vended')
START_TIME_EPOCH_S_FIELD_NUMBER: _ClassVar[int]
END_TIME_EPOCH_S_FIELD_NUMBER: _ClassVar[int]
TOTAL_ENERGY_VENDED_WH_FIELD_NUMBER: _ClassVar[int]
BUCKET_DURATION_S_FIELD_NUMBER: _ClassVar[int]
FIRST_BUCKET_DURATION_S_FIELD_NUMBER: _ClassVar[int]
BUCKET_ENERGY_VENDED_WH_FIELD_NUMBER: _ClassVar[int]
TOTAL_ENERGY_VENDED_FIELD_NUMBER: _ClassVar[int]
BUCKET_ENERGY_VENDED_FIELD_NUMBER: _ClassVar[int]
start_time_epoch_s: int
end_time_epoch_s: int
total_energy_vended_wh: int
bucket_duration_s: int
first_bucket_duration_s: int
bucket_energy_vended_wh: _containers.RepeatedScalarFieldContainer[int]
total_energy_vended: Energy
bucket_energy_vended: _containers.RepeatedCompositeFieldContainer[Energy]

def __init__(self, start_time_epoch_s: _Optional[int]=..., end_time_epoch_s: _Optional[int]=..., total_energy_vended_wh: _Optional[int]=..., bucket_duration_s: _Optional[int]=..., first_bucket_duration_s: _Optional[int]=..., bucket_energy_vended_wh: _Optional[_Iterable[int]]=..., total_energy_vended: _Optional[_Union[Energy, _Mapping]]=..., bucket_energy_vended: _Optional[_Iterable[_Union[Energy, _Mapping]]]=...) -> None:
...

class StemUi(_message.Message):
__slots__ = ('stem_info', 'session_id', 'system_time_epoch_s', 'idle_time_s', 'start_time_epoch_s', 'stem_event_info', 'session_energy_kwh')
STEM_INFO_FIELD_NUMBER: _ClassVar[int]
SESSION_ID_FIELD_NUMBER: _ClassVar[int]
SYSTEM_TIME_EPOCH_S_FIELD_NUMBER: _ClassVar[int]
IDLE_TIME_S_FIELD_NUMBER: _ClassVar[int]
START_TIME_EPOCH_S_FIELD_NUMBER: _ClassVar[int]
STEM_EVENT_INFO_FIELD_NUMBER: _ClassVar[int]
SESSION_ENERGY_KWH_FIELD_NUMBER: _ClassVar[int]
stem_info: StemInfo
session_id: int
system_time_epoch_s: int
idle_time_s: int
start_time_epoch_s: int
stem_event_info: StemEventInfo
session_energy_kwh: float

def __init__(self, stem_info: _Optional[_Union[StemInfo, _Mapping]]=..., session_id: _Optional[int]=..., system_time_epoch_s: _Optional[int]=..., idle_time_s: _Optional[int]=..., start_time_epoch_s: _Optional[int]=..., stem_event_info: _Optional[_Union[StemEventInfo, _Mapping]]=..., session_energy_kwh: _Optional[float]=...) -> None:
...

class StemBilling(_message.Message):
__slots__ = ('stem_info', 'session_id', 'stem_event_info', 'lifetime_energy_start_kwh', 'lifetime_energy_end_kwh', 'idle_time_s', 'charge_session_time_series')
STEM_INFO_FIELD_NUMBER: _ClassVar[int]
SESSION_ID_FIELD_NUMBER: _ClassVar[int]
STEM_EVENT_INFO_FIELD_NUMBER: _ClassVar[int]
LIFETIME_ENERGY_START_KWH_FIELD_NUMBER: _ClassVar[int]
LIFETIME_ENERGY_END_KWH_FIELD_NUMBER: _ClassVar[int]
IDLE_TIME_S_FIELD_NUMBER: _ClassVar[int]
CHARGE_SESSION_TIME_SERIES_FIELD_NUMBER: _ClassVar[int]
stem_info: StemInfo
session_id: int
stem_event_info: StemEventInfo
lifetime_energy_start_kwh: float
lifetime_energy_end_kwh: float
idle_time_s: int
charge_session_time_series: ChargeSessionTimeSeries

def __init__(self, stem_info: _Optional[_Union[StemInfo, _Mapping]]=..., session_id: _Optional[int]=..., stem_event_info: _Optional[_Union[StemEventInfo, _Mapping]]=..., lifetime_energy_start_kwh: _Optional[float]=..., lifetime_energy_end_kwh: _Optional[float]=..., idle_time_s: _Optional[int]=..., charge_session_time_series: _Optional[_Union[ChargeSessionTimeSeries, _Mapping]]=...) -> None:
...
Loading
Loading