Skip to content
15 changes: 10 additions & 5 deletions pyporscheconnectapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"climatise_on": "Start remote climatisation",
"connected": "Check if vehicle is on-line",
"currentoverview": "Poll vehicle for current overview",
"destinations": "Get navigation favourites",
"direct_charge_off": "Disable direct charging",
"direct_charge_on": "Enable direct charging",
"doors_and_lids": "List status of all doors and lids",
Expand Down Expand Up @@ -81,9 +82,7 @@ async def get_vehicles_with_captcha_retry(controller, connection):
await f.write(f'<!doctype html><html lang="en"><title>Captcha</title><body><img src="{captcha_err.captcha}"/></body></html>')

printc(
"\n⚠️ CAPTCHA required.\n"
f"CAPTCHA image written to: {captcha_file}\n"
"Open this file in your browser, solve the CAPTCHA, and enter the text below.",
f"\n⚠️ CAPTCHA required.\nCAPTCHA image written to: {captcha_file}\nOpen this file in your browser, solve the CAPTCHA, and enter the text below.",
)

captcha_code = await async_input("CAPTCHA: ")
Expand Down Expand Up @@ -150,6 +149,12 @@ async def currentoverview(vehicle, _args):
return vehicle.data


async def destinations(vehicle, _args):
"""Get navigation destinations."""
await vehicle.get_destinations()
return vehicle.data.get("DESTINATIONS")


async def direct_charge_off(vehicle, _args):
"""Disable direct charging."""
service = RemoteServices(vehicle)
Expand Down Expand Up @@ -222,7 +227,7 @@ async def tire_pressures(vehicle, _args):


async def trip_statistics(vehicle, _args):
"""Get pictures (uri) of the vehicle."""
"""Get trip statistics from backend."""
await vehicle.get_trip_statistics()
return vehicle.trip_statistics

Expand Down Expand Up @@ -257,7 +262,7 @@ def obj_to_json(obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
msg = f"Can't convert type {type(obj)}"
raise TypeError (msg)
raise TypeError(msg)


async def main(args):
Expand Down
78 changes: 60 additions & 18 deletions pyporscheconnectapi/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
AUDIENCE = "https://api.porsche.com"
CLIENT_ID = "XhygisuebbrqQ80byOuU5VncxLIm8E6H"
X_CLIENT_ID = "41843fb4-691d-4970-85c7-2673e8ecef40"
USER_AGENT = "pyporscheconnectapi/0.2.0"
USER_AGENT = "de.porsche.one/20.26.36-row+186611 (android)"

API_BASE_URL = "https://api.ppa.porsche.com/app"
AUTHORIZATION_URL = f"https://{AUTHORIZATION_SERVER}/authorize"
TOKEN_URL = f"https://{AUTHORIZATION_SERVER}/oauth/token"
Expand Down Expand Up @@ -45,37 +46,43 @@
TIRE_PRESSURE_TOLERANCE = 0.3

MEASUREMENTS = [
"ACV_STATE",
"ALARM_STATE",
"BATTERY_CHARGING_STATE",
"BATTERY_CONDITION",
"BATTERY_LEVEL",
"BEM_LEVEL",
"BLEID_DDADATA",
"CHARGING_PROFILES",
"CHARGING_RATE",
"CHARGING_SESSION",
"CHARGING_SETTINGS",
"CHARGING_SUMMARY",
"CLIMATIZER_STATE",
"CONNECT_CONTRACT",
"DEPARTURES",
"DIRECT_CHARGING",
"E_RANGE",
"FUEL_LEVEL",
"FUEL_RESERVE",
"GLOBAL_PRIVACY_MODE",
"GLOBAL_TIMESTAMP",
"GPS_LOCATION",
"HEATING_STATE",
"HVAC_STATE",
"HVAC_SUMMARY",
"INSTRUMENT_CLUSTER_ALERTS",
"INTERMEDIATE_SERVICE_RANGE",
"INTERMEDIATE_SERVICE_TIME",
"LOCATION_ALARMS",
"LOCK_STATE_VEHICLE",
"MAIN_SERVICE_RANGE",
"MAIN_SERVICE_TIME",
"MDK_ACTIVATION_STATE",
"MDK_CARD_STATE",
"MDK_PAIRING_PASSWORD",
"MDK_PAIRING_STATE",
"MILEAGE",
"OIL_LEVEL_CURRENT",
"OIL_LEVEL_MAX",
"OIL_LEVEL_MIN_WARNING",
"OIL_SERVICE_RANGE",
"OIL_SERVICE_TIME",
"OPEN_STATE_CHARGE_FLAP_LEFT",
"OPEN_STATE_CHARGE_FLAP_RIGHT",
"OPEN_STATE_DOOR_FRONT_LEFT",
"OPEN_STATE_DOOR_FRONT_RIGHT",
"OPEN_STATE_DOOR_REAR_LEFT",
Expand All @@ -91,21 +98,37 @@
"OPEN_STATE_WINDOW_FRONT_RIGHT",
"OPEN_STATE_WINDOW_REAR_LEFT",
"OPEN_STATE_WINDOW_REAR_RIGHT",
"OTA_CONSENT_STATUS",
"OTA_UPDATE_DETAILS",
"PAIRING_CODE",
"PARKING_BRAKE",
"PARKING_LIGHT",
"PRED_PRECON_LOCATION_EXCEPTIONS",
"PRED_PRECON_USER_SETTINGS",
"RANGE",
"REMOTE_ACCESS_AUTHORIZATION",
"SERVICE_PREDICTIONS",
"SPEED_ALARMS",
"THEFT_STATE",
"TIMERS",
"TIRE_PRESSURE",
"TIMEZONE",
"TIRE_PRESSURE_FRONT_LEFT",
"TIRE_PRESSURE_FRONT_RIGHT",
"TIRE_PRESSURE_REAR_LEFT",
"TIRE_PRESSURE_REAR_RIGHT",
"VALET_ALARM",
"VTS_CERTIFICATE_LIST",
"VTS_CONFIGURATION",
"VTS_MODES",
]

HISTORY_MEASUREMENTS = [
"CAR_ALARMS_HISTORY",
"LOCATION_ALARMS_HISTORY",
"SPEED_ALARMS_HISTORY",
"VALET_ALARM_HISTORY",
]

COMMANDS = [
"BIDIRECTIONAL_CHARGING_V2L_ABSENCE_EDIT",
"BLEID_AGREEMENT_GIVE",
"BLEID_AGREEMENT_REVOKE",
"BLEID_DEVICEKEY_UPLOAD",
Expand All @@ -115,45 +138,64 @@
"CHARGING_SETTINGS_BATTERYCAREMODE_EDIT",
"CHARGING_SETTINGS_CERTIFICATES_RESET",
"CHARGING_SETTINGS_EDIT",
"CHARGING_START",
"CHARGING_STOP",
"CS_ACCOUNT_FEDERATION",
"CS_APP_SHOP_ENABLE",
"CS_APP_SHOP_UPLOAD",
"CS_C2P_IN_VEHICLE_INFOTAINMENT",
"CS_CUSTOMER_CONNECTIVITY_DEVICE",
"CS_DATA_PASS",
"CS_DESTINATION_SYNC",
"CS_PCM_ACCOUNT_SERVICES",
"CS_PCM_CALENDAR",
"CS_PILOTED_PARKING",
"CS_VOICE_MIMIC",
"CS_YOUNITED",
"CS_VIDEOSTREAMING_VOUCHER",
"DEPARTURES_EDIT",
"DESTINATIONS_DELETE",
"DESTINATIONS_EDIT",
"DIRECT_CHARGING_START",
"DIRECT_CHARGING_STOP",
"HONK_FLASH",
"LOCATION_ALARMS_EDIT",
"LOCK",
"PRED_PRECON_LOCATION_EXCEPTION_EDIT",
"PRED_PRECON_USER_SETTINGS_EDIT",
"MDK_CARD_DISABLE",
"MDK_UNPAIR",
"OTA_UPDATE_REMOTE_START",
"POI_SEND_TO_CAR",
"REMOTE_ACV_START",
"REMOTE_ACV_STOP",
"REMOTE_CLIMATIZER_START",
"REMOTE_CLIMATIZER_STOP",
"REMOTE_HEATING_START",
"REMOTE_HEATING_STOP",
"ROUTE_CALCULATE",
"SERVICE_PREDICTIONS_RESET",
"SERVICE_PREDICTIONS_VISIBILITY_EDIT",
"SPEED_ALARMS_EDIT",
"SPIN_CHALLENGE",
"SPIN_VALIDATION",
"TIMERS_DISABLE",
"TIMERS_EDIT",
"TRUNK_UNLOCK",
"UNLOCK",
"VALET_ALARM_DISABLE",
"VALET_ALARM_EDIT",
"VTS_CONFIGURATION_EDIT",
"WINDOWS_SUNROOF_CLOSE",
"WINDOWS_SUNROOF_OPEN",
"WINDOWS_SUNROOF_VENT",
]

TRIP_STATISTICS = [
"TRIP_STATISTICS_CYCLIC",
"TRIP_STATISTICS_CYCLIC_HISTORY",
"TRIP_STATISTICS_LONG_TERM",
"TRIP_STATISTICS_LONG_TERM_HISTORY",
"TRIP_STATISTICS_SHORT_TERM_HISTORY",
"TRIP_STATISTICS_CYCLIC_HISTORY",
"TRIP_STATISTICS_MONTHLY_REPORT",
"TRIP_STATISTICS_SHORT_TERM",
"TRIP_STATISTICS_SHORT_TERM_HISTORY",
]

DESTINATIONS = [
"DESTINATIONS",
]
44 changes: 43 additions & 1 deletion pyporscheconnectapi/remote_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,22 @@ async def climatise_on(
"""Remote service for turning climatisation on."""
_LOGGER.debug("Starting remote climatisation for %s", self._vehicle.vin)

cap = await self._climatiser_capability()
md = cap.get("metadata") or {}
lo, hi = md.get("minTemperature"), md.get("maxTemperature")
if lo is not None and hi is not None:
if not lo <= target_temperature <= hi:
msg = f"target_temperature {target_temperature} K is outside the vehicle's allowed range {lo}-{hi} K ({lo - 273.15:.1f}-{hi - 273.15:.1f} °C)"
raise ValueError(msg)
else:
_LOGGER.warning(
"Vehicle did not report climatisation temperature bounds; skipping client-side range check for %s",
self._vehicle.vin,
)
payload = {
"key": "REMOTE_CLIMATIZER_START",
"payload": {
"spin": None,
"climateZonesEnabled": {
"frontLeft": front_left,
"frontRight": front_right,
Expand All @@ -134,9 +147,38 @@ async def climatise_on(
"targetTemperature": target_temperature,
},
}

return await self._send_command(payload)

async def _climatiser_capability(self) -> dict:
"""Return the REMOTE_CLIMATIZER_START capability, fetching capabilities if needed.

Raises if the vehicle does not support remote climatisation, or if it's
currently unavailable (e.g. deactivated licence).
"""

def _find():
for cap in self._vehicle.capabilities.get("commands", []):
if cap.get("key") == "REMOTE_CLIMATIZER_START":
return cap
return None

cap = _find()
if cap is None:
await self._vehicle.get_capabilities()
cap = _find()

if cap is None:
msg = "Vehicle does not report remote climatisation capability"
raise PorscheRemoteServiceError(msg)

status = cap.get("status", {})
if not status.get("isEnabled"):
cause = status.get("cause", "unknown")
msg = f"Remote climatisation is not available on this vehicle (cause: {cause})"
raise PorscheRemoteServiceError(msg)

return cap

async def climatise_off(
self,
):
Expand Down
Loading
Loading