diff --git a/src/rivian/parallax/departure.py b/src/rivian/parallax/departure.py index 4feff1f..d7448c9 100644 --- a/src/rivian/parallax/departure.py +++ b/src/rivian/parallax/departure.py @@ -53,11 +53,19 @@ def _decode_schedule(schedule: _Schedules.Schedule) -> dict[str, Any]: entry["name"] = v entry["isEnabled"] = schedule.is_enabled - occurrence = schedule.occurrence - entry["occurrence"] = { - "days": [day for field, day in _WEEKDAYS if getattr(occurrence.days, field)], - "startsAtMin": occurrence.starts_at, - } + if schedule.HasField("single_occurrence"): + starts_at = schedule.single_occurrence.starts_at + entry["occurrence"] = { + "startsAt": from_epoch(starts_at.seconds + starts_at.nanos / 1e9) + } + else: + occurrence = schedule.occurrence + entry["occurrence"] = { + "days": [ + day for field, day in _WEEKDAYS if getattr(occurrence.days, field) + ], + "startsAtMin": occurrence.starts_at, + } settings = schedule.departure_settings comfort = settings.comfort_settings @@ -87,7 +95,9 @@ def decode_departure_schedules(m: departure_pb2.DepartureSchedules) -> dict[str, departureSchedules: list[dict] — the gateway's DepartureSchedule shape: id: str, name: str, isEnabled: bool occurrence: {"days": list[str] ("monday" ... "sunday"; order - inferred), "startsAtMin": int (minutes after local midnight)} + inferred), "startsAtMin": int (minutes after local midnight)}, + or {"startsAt": datetime} for the one-off "s-" schedule a + departNow adds (departing 10 minutes after the call) departureSettings: {"shouldOverrideChargeSchedule": bool, "comfortSettings": {"cabinTempCelsius": float, "frontDefogDefrost": "off" | "defog" | "defrost", diff --git a/src/rivian/parallax/proto/departure.proto b/src/rivian/parallax/proto/departure.proto index c75f756..cea4544 100644 --- a/src/rivian/parallax/proto/departure.proto +++ b/src/rivian/parallax/proto/departure.proto @@ -15,9 +15,21 @@ message DepartureSchedules { optional string name = 2; bool is_enabled = 3; Occurrence occurrence = 4; + // Set instead of occurrence on the one-off "s-" schedule a departNow + // adds; the gateway leaves that schedule out. + SingleOccurrence single_occurrence = 5; DepartureSettings departure_settings = 6; } + message SingleOccurrence { + StartsAt starts_at = 1; + + message StartsAt { + int64 seconds = 1; // epoch seconds + int32 nanos = 2; + } + } + message Occurrence { Days days = 1; optional int32 starts_at = 2; // minutes after local midnight diff --git a/src/rivian/parallax/proto/departure_pb2.py b/src/rivian/parallax/proto/departure_pb2.py index b5c68d2..954dc11 100644 --- a/src/rivian/parallax/proto/departure_pb2.py +++ b/src/rivian/parallax/proto/departure_pb2.py @@ -24,31 +24,35 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%rivian/parallax/proto/departure.proto\x12\x19rivian.parallax.departure\"\xd2\x0b\n\x12\x44\x65partureSchedules\x12H\n\x08schedule\x18\x01 \x03(\x0b\x32\x36.rivian.parallax.departure.DepartureSchedules.Schedule\x12K\n\nupdated_at\x18\x02 \x01(\x0b\x32\x37.rivian.parallax.departure.DepartureSchedules.UpdatedAt\x1a\xfd\x01\n\x08Schedule\x12\x0f\n\x02id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x11\n\x04name\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x12\n\nis_enabled\x18\x03 \x01(\x08\x12L\n\noccurrence\x18\x04 \x01(\x0b\x32\x38.rivian.parallax.departure.DepartureSchedules.Occurrence\x12[\n\x12\x64\x65parture_settings\x18\x06 \x01(\x0b\x32?.rivian.parallax.departure.DepartureSchedules.DepartureSettingsB\x05\n\x03_idB\x07\n\x05_name\x1at\n\nOccurrence\x12@\n\x04\x64\x61ys\x18\x01 \x01(\x0b\x32\x32.rivian.parallax.departure.DepartureSchedules.Days\x12\x16\n\tstarts_at\x18\x02 \x01(\x05H\x00\x88\x01\x01\x42\x0c\n\n_starts_at\x1a~\n\x04\x44\x61ys\x12\x0e\n\x06monday\x18\x01 \x01(\x08\x12\x0f\n\x07tuesday\x18\x02 \x01(\x08\x12\x11\n\twednesday\x18\x03 \x01(\x08\x12\x10\n\x08thursday\x18\x04 \x01(\x08\x12\x0e\n\x06\x66riday\x18\x05 \x01(\x08\x12\x10\n\x08saturday\x18\x06 \x01(\x08\x12\x0e\n\x06sunday\x18\x07 \x01(\x08\x1a\x95\x01\n\x11\x44\x65partureSettings\x12W\n\x10\x63omfort_settings\x18\x01 \x01(\x0b\x32=.rivian.parallax.departure.DepartureSchedules.ComfortSettings\x12\'\n\x1fshould_override_charge_schedule\x18\x02 \x01(\x08\x1a\xde\x01\n\x0f\x43omfortSettings\x12\x19\n\x11\x63\x61\x62in_temperature\x18\x01 \x01(\x02\x12I\n\x13\x66ront_defog_defrost\x18\x02 \x01(\x0e\x32,.rivian.parallax.departure.DefogDefrostLevel\x12\x65\n\x18surface_heat_vent_levels\x18\x03 \x01(\x0b\x32\x43.rivian.parallax.departure.DepartureSchedules.SurfaceHeatVentLevels\x1a\x88\x03\n\x15SurfaceHeatVentLevels\x12H\n\x0f\x66ront_left_seat\x18\x01 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x12I\n\x10\x66ront_right_seat\x18\x02 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x12G\n\x0erear_left_seat\x18\x03 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x12H\n\x0frear_right_seat\x18\x04 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x12G\n\x0esteering_wheel\x18\x05 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x1a+\n\tUpdatedAt\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05*^\n\x11\x44\x65\x66ogDefrostLevel\x12\x15\n\x11\x44\x45\x46OG_DEFROST_OFF\x10\x00\x12\x17\n\x13\x44\x45\x46OG_DEFROST_DEFOG\x10\x01\x12\x19\n\x15\x44\x45\x46OG_DEFROST_DEFROST\x10\x02*\x9f\x01\n\x14SurfaceHeatVentLevel\x12\x0f\n\x0bSURFACE_OFF\x10\x00\x12\x12\n\x0eSURFACE_HEAT_1\x10\x01\x12\x12\n\x0eSURFACE_HEAT_2\x10\x02\x12\x12\n\x0eSURFACE_HEAT_3\x10\x03\x12\x12\n\x0eSURFACE_VENT_1\x10\x04\x12\x12\n\x0eSURFACE_VENT_2\x10\x05\x12\x12\n\x0eSURFACE_VENT_3\x10\x06\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%rivian/parallax/proto/departure.proto\x12\x19rivian.parallax.departure\"\xca\r\n\x12\x44\x65partureSchedules\x12H\n\x08schedule\x18\x01 \x03(\x0b\x32\x36.rivian.parallax.departure.DepartureSchedules.Schedule\x12K\n\nupdated_at\x18\x02 \x01(\x0b\x32\x37.rivian.parallax.departure.DepartureSchedules.UpdatedAt\x1a\xd8\x02\n\x08Schedule\x12\x0f\n\x02id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x11\n\x04name\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x12\n\nis_enabled\x18\x03 \x01(\x08\x12L\n\noccurrence\x18\x04 \x01(\x0b\x32\x38.rivian.parallax.departure.DepartureSchedules.Occurrence\x12Y\n\x11single_occurrence\x18\x05 \x01(\x0b\x32>.rivian.parallax.departure.DepartureSchedules.SingleOccurrence\x12[\n\x12\x64\x65parture_settings\x18\x06 \x01(\x0b\x32?.rivian.parallax.departure.DepartureSchedules.DepartureSettingsB\x05\n\x03_idB\x07\n\x05_name\x1a\x9a\x01\n\x10SingleOccurrence\x12Z\n\tstarts_at\x18\x01 \x01(\x0b\x32G.rivian.parallax.departure.DepartureSchedules.SingleOccurrence.StartsAt\x1a*\n\x08StartsAt\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x1at\n\nOccurrence\x12@\n\x04\x64\x61ys\x18\x01 \x01(\x0b\x32\x32.rivian.parallax.departure.DepartureSchedules.Days\x12\x16\n\tstarts_at\x18\x02 \x01(\x05H\x00\x88\x01\x01\x42\x0c\n\n_starts_at\x1a~\n\x04\x44\x61ys\x12\x0e\n\x06monday\x18\x01 \x01(\x08\x12\x0f\n\x07tuesday\x18\x02 \x01(\x08\x12\x11\n\twednesday\x18\x03 \x01(\x08\x12\x10\n\x08thursday\x18\x04 \x01(\x08\x12\x0e\n\x06\x66riday\x18\x05 \x01(\x08\x12\x10\n\x08saturday\x18\x06 \x01(\x08\x12\x0e\n\x06sunday\x18\x07 \x01(\x08\x1a\x95\x01\n\x11\x44\x65partureSettings\x12W\n\x10\x63omfort_settings\x18\x01 \x01(\x0b\x32=.rivian.parallax.departure.DepartureSchedules.ComfortSettings\x12\'\n\x1fshould_override_charge_schedule\x18\x02 \x01(\x08\x1a\xde\x01\n\x0f\x43omfortSettings\x12\x19\n\x11\x63\x61\x62in_temperature\x18\x01 \x01(\x02\x12I\n\x13\x66ront_defog_defrost\x18\x02 \x01(\x0e\x32,.rivian.parallax.departure.DefogDefrostLevel\x12\x65\n\x18surface_heat_vent_levels\x18\x03 \x01(\x0b\x32\x43.rivian.parallax.departure.DepartureSchedules.SurfaceHeatVentLevels\x1a\x88\x03\n\x15SurfaceHeatVentLevels\x12H\n\x0f\x66ront_left_seat\x18\x01 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x12I\n\x10\x66ront_right_seat\x18\x02 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x12G\n\x0erear_left_seat\x18\x03 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x12H\n\x0frear_right_seat\x18\x04 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x12G\n\x0esteering_wheel\x18\x05 \x01(\x0e\x32/.rivian.parallax.departure.SurfaceHeatVentLevel\x1a+\n\tUpdatedAt\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05*^\n\x11\x44\x65\x66ogDefrostLevel\x12\x15\n\x11\x44\x45\x46OG_DEFROST_OFF\x10\x00\x12\x17\n\x13\x44\x45\x46OG_DEFROST_DEFOG\x10\x01\x12\x19\n\x15\x44\x45\x46OG_DEFROST_DEFROST\x10\x02*\x9f\x01\n\x14SurfaceHeatVentLevel\x12\x0f\n\x0bSURFACE_OFF\x10\x00\x12\x12\n\x0eSURFACE_HEAT_1\x10\x01\x12\x12\n\x0eSURFACE_HEAT_2\x10\x02\x12\x12\n\x0eSURFACE_HEAT_3\x10\x03\x12\x12\n\x0eSURFACE_VENT_1\x10\x04\x12\x12\n\x0eSURFACE_VENT_2\x10\x05\x12\x12\n\x0eSURFACE_VENT_3\x10\x06\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'rivian.parallax.proto.departure_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_DEFOGDEFROSTLEVEL']._serialized_start=1561 - _globals['_DEFOGDEFROSTLEVEL']._serialized_end=1655 - _globals['_SURFACEHEATVENTLEVEL']._serialized_start=1658 - _globals['_SURFACEHEATVENTLEVEL']._serialized_end=1817 + _globals['_DEFOGDEFROSTLEVEL']._serialized_start=1809 + _globals['_DEFOGDEFROSTLEVEL']._serialized_end=1903 + _globals['_SURFACEHEATVENTLEVEL']._serialized_start=1906 + _globals['_SURFACEHEATVENTLEVEL']._serialized_end=2065 _globals['_DEPARTURESCHEDULES']._serialized_start=69 - _globals['_DEPARTURESCHEDULES']._serialized_end=1559 + _globals['_DEPARTURESCHEDULES']._serialized_end=1807 _globals['_DEPARTURESCHEDULES_SCHEDULE']._serialized_start=243 - _globals['_DEPARTURESCHEDULES_SCHEDULE']._serialized_end=496 - _globals['_DEPARTURESCHEDULES_OCCURRENCE']._serialized_start=498 - _globals['_DEPARTURESCHEDULES_OCCURRENCE']._serialized_end=614 - _globals['_DEPARTURESCHEDULES_DAYS']._serialized_start=616 - _globals['_DEPARTURESCHEDULES_DAYS']._serialized_end=742 - _globals['_DEPARTURESCHEDULES_DEPARTURESETTINGS']._serialized_start=745 - _globals['_DEPARTURESCHEDULES_DEPARTURESETTINGS']._serialized_end=894 - _globals['_DEPARTURESCHEDULES_COMFORTSETTINGS']._serialized_start=897 - _globals['_DEPARTURESCHEDULES_COMFORTSETTINGS']._serialized_end=1119 - _globals['_DEPARTURESCHEDULES_SURFACEHEATVENTLEVELS']._serialized_start=1122 - _globals['_DEPARTURESCHEDULES_SURFACEHEATVENTLEVELS']._serialized_end=1514 - _globals['_DEPARTURESCHEDULES_UPDATEDAT']._serialized_start=1516 - _globals['_DEPARTURESCHEDULES_UPDATEDAT']._serialized_end=1559 + _globals['_DEPARTURESCHEDULES_SCHEDULE']._serialized_end=587 + _globals['_DEPARTURESCHEDULES_SINGLEOCCURRENCE']._serialized_start=590 + _globals['_DEPARTURESCHEDULES_SINGLEOCCURRENCE']._serialized_end=744 + _globals['_DEPARTURESCHEDULES_SINGLEOCCURRENCE_STARTSAT']._serialized_start=702 + _globals['_DEPARTURESCHEDULES_SINGLEOCCURRENCE_STARTSAT']._serialized_end=744 + _globals['_DEPARTURESCHEDULES_OCCURRENCE']._serialized_start=746 + _globals['_DEPARTURESCHEDULES_OCCURRENCE']._serialized_end=862 + _globals['_DEPARTURESCHEDULES_DAYS']._serialized_start=864 + _globals['_DEPARTURESCHEDULES_DAYS']._serialized_end=990 + _globals['_DEPARTURESCHEDULES_DEPARTURESETTINGS']._serialized_start=993 + _globals['_DEPARTURESCHEDULES_DEPARTURESETTINGS']._serialized_end=1142 + _globals['_DEPARTURESCHEDULES_COMFORTSETTINGS']._serialized_start=1145 + _globals['_DEPARTURESCHEDULES_COMFORTSETTINGS']._serialized_end=1367 + _globals['_DEPARTURESCHEDULES_SURFACEHEATVENTLEVELS']._serialized_start=1370 + _globals['_DEPARTURESCHEDULES_SURFACEHEATVENTLEVELS']._serialized_end=1762 + _globals['_DEPARTURESCHEDULES_UPDATEDAT']._serialized_start=1764 + _globals['_DEPARTURESCHEDULES_UPDATEDAT']._serialized_end=1807 # @@protoc_insertion_point(module_scope) diff --git a/src/rivian/parallax/proto/departure_pb2.pyi b/src/rivian/parallax/proto/departure_pb2.pyi index 114138c..a8954f3 100644 --- a/src/rivian/parallax/proto/departure_pb2.pyi +++ b/src/rivian/parallax/proto/departure_pb2.pyi @@ -36,18 +36,32 @@ SURFACE_VENT_3: SurfaceHeatVentLevel class DepartureSchedules(_message.Message): __slots__ = ("schedule", "updated_at") class Schedule(_message.Message): - __slots__ = ("id", "name", "is_enabled", "occurrence", "departure_settings") + __slots__ = ("id", "name", "is_enabled", "occurrence", "single_occurrence", "departure_settings") ID_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] IS_ENABLED_FIELD_NUMBER: _ClassVar[int] OCCURRENCE_FIELD_NUMBER: _ClassVar[int] + SINGLE_OCCURRENCE_FIELD_NUMBER: _ClassVar[int] DEPARTURE_SETTINGS_FIELD_NUMBER: _ClassVar[int] id: str name: str is_enabled: bool occurrence: DepartureSchedules.Occurrence + single_occurrence: DepartureSchedules.SingleOccurrence departure_settings: DepartureSchedules.DepartureSettings - def __init__(self, id: _Optional[str] = ..., name: _Optional[str] = ..., is_enabled: bool = ..., occurrence: _Optional[_Union[DepartureSchedules.Occurrence, _Mapping]] = ..., departure_settings: _Optional[_Union[DepartureSchedules.DepartureSettings, _Mapping]] = ...) -> None: ... + def __init__(self, id: _Optional[str] = ..., name: _Optional[str] = ..., is_enabled: bool = ..., occurrence: _Optional[_Union[DepartureSchedules.Occurrence, _Mapping]] = ..., single_occurrence: _Optional[_Union[DepartureSchedules.SingleOccurrence, _Mapping]] = ..., departure_settings: _Optional[_Union[DepartureSchedules.DepartureSettings, _Mapping]] = ...) -> None: ... + class SingleOccurrence(_message.Message): + __slots__ = ("starts_at",) + class StartsAt(_message.Message): + __slots__ = ("seconds", "nanos") + SECONDS_FIELD_NUMBER: _ClassVar[int] + NANOS_FIELD_NUMBER: _ClassVar[int] + seconds: int + nanos: int + def __init__(self, seconds: _Optional[int] = ..., nanos: _Optional[int] = ...) -> None: ... + STARTS_AT_FIELD_NUMBER: _ClassVar[int] + starts_at: DepartureSchedules.SingleOccurrence.StartsAt + def __init__(self, starts_at: _Optional[_Union[DepartureSchedules.SingleOccurrence.StartsAt, _Mapping]] = ...) -> None: ... class Occurrence(_message.Message): __slots__ = ("days", "starts_at") DAYS_FIELD_NUMBER: _ClassVar[int] diff --git a/tests/parallax/test_departure.py b/tests/parallax/test_departure.py index 876bb28..ef70211 100644 --- a/tests/parallax/test_departure.py +++ b/tests/parallax/test_departure.py @@ -64,6 +64,23 @@ def test_departure_schedules() -> None: } +def test_departure_schedules_single_occurrence() -> None: + """The one-off departNow schedule has a start time instead of days.""" + schedule = Schedules.Schedule( + id="s-1", + is_enabled=True, + single_occurrence=Schedules.SingleOccurrence( + starts_at=Schedules.SingleOccurrence.StartsAt( + seconds=1791761210, nanos=959000000 + ) + ), + ) + result = decode("departure.schedule.schedule", Schedules(schedule=[schedule])) + assert result["departureSchedules"][0]["occurrence"] == { + "startsAt": epoch(1791761210_959) + } + + def test_departure_schedules_empty() -> None: """No schedules reads as an empty list.""" assert decode("departure.schedule.schedule") == {"departureSchedules": []}