Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add destination location #423

Merged
merged 10 commits into from
Dec 11, 2022
35 changes: 35 additions & 0 deletions custom_components/tesla_custom/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie

for car in cars.values():
entities.append(TeslaCarLocation(hass, car, coordinator))
entities.append(TeslaCarDestinationLocation(hass, car, coordinator))

async_add_entities(entities, True)

Expand Down Expand Up @@ -66,3 +67,37 @@ def extra_state_attributes(self):
def force_update(self):
"""Disable forced updated since we are polling via the coordinator updates."""
return False


class TeslaCarDestinationLocation(TeslaCarEntity, TrackerEntity):
"""Representation of a Tesla car destination location device tracker."""

def __init__(
self,
hass: HomeAssistant,
car: TeslaCar,
coordinator: TeslaDataUpdateCoordinator,
) -> None:
"""Initialize car destination location entity."""
super().__init__(hass, car, coordinator)
self.type = "destination location tracker"

@property
def source_type(self):
"""Return device tracker source type."""
return SOURCE_TYPE_GPS

@property
def longitude(self):
"""Return destination longitude."""
return self._car.active_route_longitude

@property
def latitude(self):
"""Return destination latitude."""
return self._car.active_route_latitude

@property
def force_update(self):
"""Disable forced updated since we are polling via the coordinator updates."""
return False
2 changes: 1 addition & 1 deletion custom_components/tesla_custom/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"documentation": "https://github.com/alandtse/tesla/wiki",
"issue_tracker": "https://github.com/alandtse/tesla/issues",
"requirements": [
"teslajsonpy==3.4.1"
"teslajsonpy==3.5.1"
],
"codeowners": [
"@alandtse"
Expand Down
85 changes: 85 additions & 0 deletions custom_components/tesla_custom/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie
entities.append(
TeslaCarTpmsPressureSensor(hass, car, coordinator, tpms_sensor)
)
entities.append(TeslaCarArrivalTime(hass, car, coordinator))
entities.append(TeslaCarDistanceToArrival(hass, car, coordinator))

for energysite in energysites.values():
if (
Expand Down Expand Up @@ -557,3 +559,86 @@ def extra_state_attributes(self):
return {
"tpms_last_seen_pressure_timestamp": timestamp,
}


class TeslaCarArrivalTime(TeslaCarEntity, SensorEntity):
"""Representation of the Tesla car route arrival time."""

def __init__(
self,
hass: HomeAssistant,
car: TeslaCar,
coordinator: TeslaDataUpdateCoordinator,
) -> None:
"""Initialize time charge complete entity."""
super().__init__(hass, car, coordinator)
self.type = "arrival time"
self._attr_device_class = SensorDeviceClass.TIMESTAMP
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_icon = "mdi:timer-sand"
self._datetime_value: Optional[datetime] = None
self._last_known_value: Optional[int] = None
self._last_update_time: Optional[datetime] = None

@property
def native_value(self) -> Optional[datetime]:
"""Return route arrival time."""
if self._car.active_route_minutes_to_arrival is None:
return self._datetime_value
else:
min_duration = round(float(self._car.active_route_minutes_to_arrival), 2)

if self._last_known_value != min_duration:
self._last_known_value = min_duration
self._last_update_time = dt.utcnow()

new_value = (
dt.utcnow()
+ timedelta(minutes=min_duration)
- (dt.utcnow() - self._last_update_time)
)
if (
self._datetime_value is None
or (new_value - self._datetime_value).total_seconds() >= 60
):
self._datetime_value = new_value
return self._datetime_value

@property
def extra_state_attributes(self):
"""Return device state attributes."""
if self._car.active_route_traffic_minutes_delay is None:
minutes = None
else:
minutes = round(self._car.active_route_traffic_minutes_delay, 1)

return {
"Energy at arrival": self._car.active_route_energy_at_arrival,
"Minutes traffic delay": minutes,
"Destination": self._car.active_route_destination,
}


class TeslaCarDistanceToArrival(TeslaCarEntity, SensorEntity):
"""Representation of the Tesla distance to arrival."""

def __init__(
self,
hass: HomeAssistant,
car: TeslaCar,
coordinator: TeslaDataUpdateCoordinator,
) -> None:
"""Initialize distance to arrival entity."""
super().__init__(hass, car, coordinator)
self.type = "distance to arrival"
self._attr_device_class = SensorDeviceClass.DISTANCE
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = LENGTH_MILES
self._attr_icon = "mdi:map-marker-distance"

@property
def native_value(self) -> float:
"""Return the distance to arrival."""
if self._car.active_route_miles_to_arrival is None:
return None
return round(self._car.active_route_miles_to_arrival, 2)
9 changes: 5 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ license = "Apache-2.0"

[tool.poetry.dependencies]
python = "^3.10"
teslajsonpy = "^3.4.1"
teslajsonpy = "^3.5.1"


[tool.poetry.dev-dependencies]
homeassistant = ">=2021.10.0"
Expand Down
7 changes: 7 additions & 0 deletions tests/mock_data/car.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@
"steering_wheel_heater": True,
},
"drive_state": {
"active_route_destination": "Saved destination name",
"active_route_energy_at_arrival": 40,
"active_route_latitude": 34.111111,
"active_route_longitude": -88.11111,
"active_route_miles_to_arrival": 19.83,
"active_route_minutes_to_arrival": 34.13,
"active_route_traffic_minutes_delay": 0.0,
"gps_as_of": 1661641173,
"heading": 182,
"latitude": 33.111111,
Expand Down
6 changes: 6 additions & 0 deletions tests/test_device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@
# state.attributes.get("speed")
# == car_mock_data.VEHICLE_DATA["drive_state"]["speed"]
# )

# async def test_car_destination_location(hass: HomeAssistant) -> None:
# """Tests car destination location is getting the correct value."""
# await setup_platform(hass, DEVICE_TRACKER_DOMAIN)

# state = hass.states.get("device_tracker.my_model_s_destination_location_tracker")
75 changes: 75 additions & 0 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ async def test_registry_entries(hass: HomeAssistant) -> None:
entry = entity_registry.async_get("sensor.battery_home_backup_reserve")
assert entry.unique_id == "67890_backup_reserve"

entry = entity_registry.async_get("sensor.my_model_s_arrival_time")
assert entry.unique_id == f"{car_mock_data.VIN.lower()}_arrival_time"

entry = entity_registry.async_get("sensor.my_model_s_distance_to_arrival")
assert entry.unique_id == f"{car_mock_data.VIN.lower()}_distance_to_arrival"


async def test_battery(hass: HomeAssistant) -> None:
"""Tests battery is getting the correct value."""
Expand Down Expand Up @@ -530,3 +536,72 @@ async def test_tpms_pressure_none(hass: HomeAssistant) -> None:
assert state_fl.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI

assert state_fl.attributes.get("tpms_last_seen_pressure_timestamp") == None


async def test_arrival_time(hass: HomeAssistant) -> None:
"""Tests arrival time is getting the correct value."""
await setup_platform(hass, SENSOR_DOMAIN)

state = hass.states.get("sensor.my_model_s_arrival_time")
arrival_time = datetime.utcnow() + timedelta(
minutes=round(
float(
car_mock_data.VEHICLE_DATA["drive_state"][
"active_route_minutes_to_arrival"
]
),
2,
)
)
arrival_time_str = datetime.strftime(arrival_time, "%Y-%m-%dT%H:%M:%S+00:00")

assert state.state == arrival_time_str

assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert (
state.attributes.get("Energy at arrival")
== car_mock_data.VEHICLE_DATA["drive_state"]["active_route_energy_at_arrival"]
)
assert state.attributes.get("Minutes traffic delay") == round(
car_mock_data.VEHICLE_DATA["drive_state"]["active_route_traffic_minutes_delay"],
1,
)
assert (
state.attributes.get("Destination")
== car_mock_data.VEHICLE_DATA["drive_state"]["active_route_destination"]
)


async def test_distance_to_arrival(hass: HomeAssistant) -> None:
"""Tests distance to arrival is getting the correct value."""
await setup_platform(hass, SENSOR_DOMAIN)

state = hass.states.get("sensor.my_model_s_distance_to_arrival")

if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS:
assert state.state == str(
round(
DistanceConverter.convert(
car_mock_data.VEHICLE_DATA["drive_state"][
"active_route_miles_to_arrival"
],
LENGTH_MILES,
LENGTH_KILOMETERS,
),
2,
)
)
else:
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES
assert state.state == str(
round(
car_mock_data.VEHICLE_DATA["drive_state"][
"active_route_miles_to_arrival"
],
2,
)
)

assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT