Skip to content

Commit

Permalink
Merge pull request #449 from alandtse/dev
Browse files Browse the repository at this point in the history
2022-12-20
  • Loading branch information
alandtse authored Dec 21, 2022
2 parents a004b08 + 865c637 commit a1ad476
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 24 deletions.
103 changes: 102 additions & 1 deletion custom_components/tesla_custom/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie
entities.append(TeslaCarChargerConnection(hass, car, coordinator))
entities.append(TeslaCarCharging(hass, car, coordinator))
entities.append(TeslaCarDoors(hass, car, coordinator))
entities.append(TeslaCarScheduledCharging(hass, car, coordinator))
entities.append(TeslaCarScheduledDeparture(hass, car, coordinator))
entities.append(TeslaCarUserPresent(hass, car, coordinator))

for energysite in energysites.values():
if energysite.resource_type == RESOURCE_TYPE_BATTERY:
Expand Down Expand Up @@ -226,7 +229,12 @@ def __init__(
@property
def is_on(self):
"""Return True if a car door is open."""
return self._car.door_df or self._car.door_dr or self._car.door_pf or self._car.door_pr
return (
self._car.door_df
or self._car.door_dr
or self._car.door_pf
or self._car.door_pr
)

@property
def extra_state_attributes(self):
Expand All @@ -243,3 +251,96 @@ def _open_or_closed(self, door):
if door:
return "Open"
return "Closed"


class TeslaCarScheduledCharging(TeslaCarEntity, BinarySensorEntity):
"""Representation of a Tesla car scheduled charging binary sensor."""

def __init__(
self,
hass: HomeAssistant,
car: TeslaCar,
coordinator: TeslaDataUpdateCoordinator,
) -> None:
"""Initialize scheduled charging entity."""
super().__init__(hass, car, coordinator)
self.type = "scheduled charging"
self._attr_icon = "mdi:calendar-plus"
self._attr_device_class = None

@property
def is_on(self):
"""Return True if scheduled charging enebaled."""
if self._car.scheduled_charging_mode == "StartAt":
return True
return False

@property
def extra_state_attributes(self):
"""Return device state attributes."""
return {
"Scheduled charging time": self._car.scheduled_charging_start_time_app,
}


class TeslaCarScheduledDeparture(TeslaCarEntity, BinarySensorEntity):
"""Representation of a Tesla car scheduled departure binary sensor."""

def __init__(
self,
hass: HomeAssistant,
car: TeslaCar,
coordinator: TeslaDataUpdateCoordinator,
) -> None:
"""Initialize scheduled departure entity."""
super().__init__(hass, car, coordinator)
self.type = "scheduled departure"
self._attr_icon = "mdi:calendar-plus"
self._attr_device_class = None

@property
def is_on(self):
"""Return True if scheduled departure enebaled."""
if self._car.scheduled_charging_mode == "DepartBy":
return True
return False

@property
def extra_state_attributes(self):
"""Return device state attributes."""
return {
"Departure time": self._car.scheduled_departure_time_minutes,
"Preconditioning enabled": self._car.is_preconditioning_enabled,
"Preconditioning weekdays only": self._car.is_preconditioning_weekday_only,
"Off peak charging enabled": self._car.is_off_peak_charging_enabled,
"Off peak charging weekdays only": self._car.is_off_peak_charging_weekday_only,
"End off peak time": self._car.off_peak_hours_end_time,
}


class TeslaCarUserPresent(TeslaCarEntity, BinarySensorEntity):
"""Representation of a Tesla car user present binary sensor."""

def __init__(
self,
hass: HomeAssistant,
car: TeslaCar,
coordinator: TeslaDataUpdateCoordinator,
) -> None:
"""Initialize user present entity."""
super().__init__(hass, car, coordinator)
self.type = "user present"
self._attr_icon = "mdi:account-check"
self._attr_device_class = None

@property
def is_on(self):
"""Return True if user present enebaled."""
return self._car._vehicle_data.get("vehicle_state", {}).get("is_user_present")

@property
def extra_state_attributes(self):
"""Return device state attributes."""
user_id = str(self._car._vehicle_data.get("user_id"))

return {"user_id": user_id}
34 changes: 23 additions & 11 deletions custom_components/tesla_custom/sensor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""Support for the Tesla sensors."""

from datetime import datetime, timedelta
from typing import Optional

from teslajsonpy.car import TeslaCar
from teslajsonpy.const import RESOURCE_TYPE_SOLAR, RESOURCE_TYPE_BATTERY
from teslajsonpy.energy import EnergySite, PowerwallSite
Expand All @@ -20,20 +24,16 @@
PRESSURE_PSI,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TIME_HOURS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.util.unit_conversion import DistanceConverter, PressureConverter
from homeassistant.util.unit_conversion import DistanceConverter
from homeassistant.util import dt

from . import TeslaDataUpdateCoordinator
from .base import TeslaCarEntity, TeslaEnergyEntity
from .const import DISTANCE_UNITS_KM_HR, DOMAIN

from datetime import datetime, timedelta
from typing import Optional

SOLAR_SITE_SENSORS = ["solar power", "grid power", "load power"]
BATTERY_SITE_SENSORS = SOLAR_SITE_SENSORS + ["battery power"]

Expand Down Expand Up @@ -319,6 +319,21 @@ def native_value(self) -> float:

return round(range_value, 2)

@property
def extra_state_attributes(self):
"""Return device state attributes."""
est_battery_range = self._car._vehicle_data.get("charge_state", {}).get(
"est_battery_range"
)
est_battery_range_km = DistanceConverter.convert(
est_battery_range, LENGTH_MILES, LENGTH_KILOMETERS
)

return {
"est_battery_range_miles": est_battery_range,
"est_battery_range_km": est_battery_range_km,
}


class TeslaCarTemp(TeslaCarEntity, SensorEntity):
"""Representation of a Tesla car temp sensor."""
Expand Down Expand Up @@ -501,7 +516,6 @@ def __init__(
super().__init__(hass, car, coordinator)
self.type = "time charge complete"
self._attr_device_class = SensorDeviceClass.TIMESTAMP
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_icon = "mdi:timer-plus"
self._value: Optional[datetime] = None

Expand Down Expand Up @@ -574,7 +588,6 @@ def __init__(
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
Expand All @@ -588,14 +601,13 @@ def native_value(self) -> Optional[datetime]:
else:
min_duration = round(float(self._car.active_route_minutes_to_arrival), 2)

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

new_value = (
dt.utcnow()
+ timedelta(minutes=min_duration)
- (dt.utcnow() - self._last_update_time)
utcnow + timedelta(minutes=min_duration) - (utcnow - self._last_update_time)
)
if (
self._datetime_value is None
Expand Down
2 changes: 1 addition & 1 deletion tests/mock_data/car.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"scheduled_charging_mode": "DepartBy",
"scheduled_charging_pending": False,
"scheduled_charging_start_time": None,
"scheduled_charging_start_time_app": 0,
"scheduled_charging_start_time_app": 480,
"scheduled_departure_time": 1661515200,
"scheduled_departure_time_minutes": 300,
"supercharger_session_trip_planner": False,
Expand Down
93 changes: 90 additions & 3 deletions tests/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,21 @@ async def test_registry_entries(hass: HomeAssistant) -> None:
entry = entity_registry.async_get("binary_sensor.my_model_s_online")
assert entry.unique_id == f"{car_mock_data.VIN.lower()}_online"

entry = entity_registry.async_get("binary_sensor.my_model_s_scheduled_charging")
assert entry.unique_id == f"{car_mock_data.VIN.lower()}_scheduled_charging"

entry = entity_registry.async_get("binary_sensor.my_model_s_scheduled_departure")
assert entry.unique_id == f"{car_mock_data.VIN.lower()}_scheduled_departure"

entry = entity_registry.async_get("binary_sensor.battery_home_battery_charging")
assert entry.unique_id == "67890_battery_charging"

entry = entity_registry.async_get("binary_sensor.battery_home_grid_status")
assert entry.unique_id == "67890_grid_status"

entry = entity_registry.async_get("binary_sensor.my_model_s_user_present")
assert entry.unique_id == f"{car_mock_data.VIN.lower()}_user_present"


async def test_parking_brake(hass: HomeAssistant) -> None:
"""Tests car parking brake is getting the correct value."""
Expand Down Expand Up @@ -138,18 +147,96 @@ async def test_grid_status(hass: HomeAssistant) -> None:

assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.POWER


async def test_car_doors(hass: HomeAssistant) -> None:
"""Tests car door is getting the correct value."""
await setup_platform(hass, BINARY_SENSOR_DOMAIN)

state = hass.states.get("binary_sensor.my_model_s_doors")
assert state.state == STATE_ON

assert (
state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
)
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR

assert state.attributes.get("Driver Front") == "Open"
assert state.attributes.get("Driver Rear") == "Closed"
assert state.attributes.get("Passenger Front") == "Closed"
assert state.attributes.get("Passenger Rear") == "Closed"


async def test_car_scheduled_charging(hass: HomeAssistant) -> None:
"""Tests scheduled charging is getting the correct value."""
await setup_platform(hass, BINARY_SENSOR_DOMAIN)

state = hass.states.get("binary_sensor.my_model_s_scheduled_charging")
assert state.state == STATE_OFF

assert (
state.attributes.get("Scheduled charging time")
== car_mock_data.VEHICLE_DATA["charge_state"][
"scheduled_charging_start_time_app"
]
)


async def test_car_scheduled_departure(hass: HomeAssistant) -> None:
"""Tests scheduled departure is getting the correct value."""
await setup_platform(hass, BINARY_SENSOR_DOMAIN)

state = hass.states.get("binary_sensor.my_model_s_scheduled_departure")
assert state.state == STATE_ON

assert (
state.attributes.get("Departure time")
== car_mock_data.VEHICLE_DATA["charge_state"][
"scheduled_departure_time_minutes"
]
)

assert (
state.attributes.get("Preconditioning enabled")
== car_mock_data.VEHICLE_DATA["charge_state"]["preconditioning_enabled"]
)

if (
car_mock_data.VEHICLE_DATA["charge_state"]["preconditioning_times"]
== "weekdays"
):
check_precondition_weekdays_only = True
else:
check_precondition_weekdays_only = False
assert (
state.attributes.get("Preconditioning weekdays only")
== check_precondition_weekdays_only
)

assert (
state.attributes.get("Off peak charging enabled")
== car_mock_data.VEHICLE_DATA["charge_state"]["off_peak_charging_enabled"]
)

if (
car_mock_data.VEHICLE_DATA["charge_state"]["off_peak_charging_times"]
== "weekdays"
):
check_off_peak_weekdays_only = True
else:
check_off_peak_weekdays_only = False
assert (
state.attributes.get("Off peak charging weekdays only")
== check_off_peak_weekdays_only
)

assert (
state.attributes.get("End off peak time")
== car_mock_data.VEHICLE_DATA["charge_state"]["off_peak_hours_end_time"]
)


async def test_car_user_present(hass: HomeAssistant) -> None:
"""Tests user present is getting the correct value."""
await setup_platform(hass, BINARY_SENSOR_DOMAIN)

state = hass.states.get("binary_sensor.my_model_s_user_present")
assert state.state == STATE_OFF

assert state.attributes.get("user_id") == str(car_mock_data.VEHICLE_DATA["user_id"])
Loading

0 comments on commit a1ad476

Please sign in to comment.