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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ integration only creates entities that the car can actually drive.
| `sensor` | `charging_power` | `has_electric_drivetrain` |
| `sensor` | `charging_finished` | `has_electric_drivetrain` |
| `sensor` | `remaining_range_electric` | `has_electric_drivetrain` |
| `sensor` | `last_update` (when the car last sent its data, diagnostic) | `GLOBAL_TIMESTAMP` reported |
| `sensor` | `mileage` | `has_porsche_connect` (default) |
| `sensor` | `remaining_range` | `has_ice_drivetrain` |
| `sensor` | `fuel_level` | `has_ice_drivetrain` |
Expand All @@ -113,7 +114,7 @@ integration only creates entities that the car can actually drive.
| `device_tracker`| Vehicle location | `has_porsche_connect` and `privacy_mode == false` |
| `image` | `front_view`, `side_view`, `rear_view`, `rear_top_view`, `top_view`| Pictures returned by the API |
| `lock` | Door lock | `has_porsche_connect` (unlock needs S-PIN) |
| `climate` | Remote climatisation (target temperature) | `has_remote_climatisation` and `has_remote_services` |
| `climate` | Remote climatisation (target read from the car, `remaining_time`) | `has_remote_climatisation` and `has_remote_services` |
| `switch` | `climatise` (disabled by default) | `has_remote_climatisation` and `has_remote_services` |
| `switch` | `direct_charging` | `has_direct_charge` and `has_remote_services` |
| `number` | `target_soc` (25–100 %, step 5) | `has_electric_drivetrain` and `has_remote_services` |
Expand Down
22 changes: 19 additions & 3 deletions custom_components/porscheconnect/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,25 @@ def __init__(
"""Initialise the climate entity."""
super().__init__(coordinator, vehicle)
self._attr_unique_id = f"{self._vin}-climate"
# The API reports on/off but not the previously-requested target, so we
# keep an optimistic target the user can adjust.
self._attr_native_target_temperature = _DEFAULT_TEMP_C
# Start from the target the car reports; the user's later choice is kept
# until climatisation runs, then the car's target is followed again.
reported = vehicle.climatisation_target_temperature
self._attr_native_target_temperature = (
reported if reported is not None else _DEFAULT_TEMP_C
)

@callback
def _handle_coordinator_update(self) -> None:
"""Follow the target the car applies while climatisation runs."""
reported = self.vehicle.climatisation_target_temperature
if self.vehicle.remote_climatise_on and reported is not None:
self._attr_native_target_temperature = reported
super()._handle_coordinator_update()

@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the remaining climatisation time, in minutes, while it runs."""
return {"remaining_time": self.vehicle.climatisation_remaining_time}

@property
def hvac_mode(self) -> HVACMode:
Expand Down
4 changes: 2 additions & 2 deletions custom_components/porscheconnect/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/bartolije/ha-porscheconnect/issues",
"quality_scale": "gold",
"requirements": ["pyporscheconnectapi-bartolije==0.5.1"],
"version": "0.5.4"
"requirements": ["pyporscheconnectapi-bartolije==0.5.2"],
"version": "0.6.0"
}
9 changes: 9 additions & 0 deletions custom_components/porscheconnect/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ def _extract(v: PorscheVehicle) -> float | None:
suggested_display_precision=0,
is_available=lambda v: v.has_electric_drivetrain,
),
PorscheSensorEntityDescription(
# When the car last sent its data: the other values are that old.
key="last_update",
translation_key="last_update",
value_fn=lambda v: v.last_global_update,
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
is_available=lambda v: "GLOBAL_TIMESTAMP" in v.data,
),
PorscheSensorEntityDescription(
key="mileage",
translation_key="mileage",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/porscheconnect/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
"state_of_charge": {
"name": "State of charge"
},
"last_update": {
"name": "Last update from the car"
},
"mileage": {
"name": "Mileage"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/porscheconnect/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
"state_of_charge": {
"name": "Ladezustand"
},
"last_update": {
"name": "Letzte Meldung des Fahrzeugs"
},
"mileage": {
"name": "Kilometerstand"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/porscheconnect/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
"state_of_charge": {
"name": "State of charge"
},
"last_update": {
"name": "Last update from the car"
},
"mileage": {
"name": "Mileage"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/porscheconnect/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
"state_of_charge": {
"name": "Estado de carga"
},
"last_update": {
"name": "Última actualización del vehículo"
},
"mileage": {
"name": "Kilometraje"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/porscheconnect/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
"state_of_charge": {
"name": "Niveau de charge"
},
"last_update": {
"name": "Dernière remontée de la voiture"
},
"mileage": {
"name": "Kilométrage"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/porscheconnect/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
"state_of_charge": {
"name": "Stato di carica"
},
"last_update": {
"name": "Ultimo aggiornamento dal veicolo"
},
"mileage": {
"name": "Chilometraggio"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/porscheconnect/translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
"state_of_charge": {
"name": "Laadtoestand"
},
"last_update": {
"name": "Laatste update van de auto"
},
"mileage": {
"name": "Kilometerstand"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/porscheconnect/translations/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
"state_of_charge": {
"name": "Laddningsnivå"
},
"last_update": {
"name": "Senaste uppdatering från bilen"
},
"mileage": {
"name": "Mätarställning"
},
Expand Down
2 changes: 1 addition & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pyporscheconnectapi-bartolije==0.5.1
pyporscheconnectapi-bartolije==0.5.2
pytest-homeassistant==2026.11.0
pytest-timeout==2.4.0
pytest-cov==7.1.0
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def _make_vehicle() -> SimpleNamespace:
vehicle.privacy_mode = False
vehicle.picture_locations = {}
vehicle.seat_heating = None
vehicle.climatisation_target_temperature = None
vehicle.climatisation_remaining_time = None
vehicle.last_global_update = None
vehicle.location = (None, None, None)
vehicle.location_updated_at = None
vehicle.data = {
Expand Down
28 changes: 28 additions & 0 deletions tests/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def _vehicle(**overrides) -> SimpleNamespace:
"has_remote_climatisation": True,
"has_remote_services": True,
"remote_climatise_on": False,
"climatisation_target_temperature": None,
"climatisation_remaining_time": None,
}
attrs.update(overrides)
return SimpleNamespace(**attrs)
Expand Down Expand Up @@ -126,3 +128,29 @@ async def test_api_error_maps_to_home_assistant_error():

with pytest.raises(HomeAssistantError):
await climate.async_turn_on()


def test_target_starts_from_the_car():
"""The reported target is the starting point, 20 °C when none is reported."""
coord = MagicMock()
assert _make(coord, _vehicle(climatisation_target_temperature=22.5)).native_target_temperature == 22.5
assert _make(coord, _vehicle()).native_target_temperature == 20.0


def test_target_follows_the_car_only_while_running():
"""A target chosen while off is kept; the car's target is followed while on."""
vehicle = _vehicle(climatisation_target_temperature=19.0)
climate = _make(MagicMock(), vehicle)
climate.async_write_ha_state = MagicMock()

climate._attr_native_target_temperature = 23.0
climate._handle_coordinator_update()
assert climate.native_target_temperature == 23.0

vehicle.remote_climatise_on = True
vehicle.climatisation_target_temperature = 21.5
vehicle.climatisation_remaining_time = 14
climate._handle_coordinator_update()
assert climate.native_target_temperature == 21.5
assert climate.extra_state_attributes == {"remaining_time": 14}

31 changes: 31 additions & 0 deletions tests/test_last_update_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Tests for the "last update from the car" sensor."""

from __future__ import annotations

from datetime import UTC, datetime

from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from pytest_homeassistant.common import MockConfigEntry

from custom_components.porscheconnect.const import DOMAIN


async def test_last_update_sensor_reports_when_the_car_sent_data(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_vehicle,
mock_connection_cls,
mock_account_cls,
) -> None:
"""The sensor exists when the car reports GLOBAL_TIMESTAMP and shows it."""
mock_vehicle.data["GLOBAL_TIMESTAMP"] = {"timestamp": "2026-11-06T06:30:00Z"}
mock_vehicle.last_global_update = datetime(2026, 11, 6, 6, 30, tzinfo=UTC)
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

entity_id = er.async_get(hass).async_get_entity_id(
"sensor", DOMAIN, f"{mock_vehicle.vin}-last_update"
)
assert hass.states.get(entity_id).state == "2026-11-06T06:30:00+00:00"
Loading