Skip to content

Commit

Permalink
feat: add navigation destination entities (#409)
Browse files Browse the repository at this point in the history
closes #384
  • Loading branch information
alandtse authored Dec 11, 2022
2 parents ba7735a + 54e436b commit d646c4f
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 6 deletions.
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

0 comments on commit d646c4f

Please sign in to comment.