diff --git a/custom_components/tesla_custom/device_tracker.py b/custom_components/tesla_custom/device_tracker.py index 3b154e71..9b146af6 100644 --- a/custom_components/tesla_custom/device_tracker.py +++ b/custom_components/tesla_custom/device_tracker.py @@ -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) @@ -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 diff --git a/custom_components/tesla_custom/manifest.json b/custom_components/tesla_custom/manifest.json index 15344455..d1990993 100644 --- a/custom_components/tesla_custom/manifest.json +++ b/custom_components/tesla_custom/manifest.json @@ -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" diff --git a/custom_components/tesla_custom/sensor.py b/custom_components/tesla_custom/sensor.py index aa1058a6..4d212d8f 100644 --- a/custom_components/tesla_custom/sensor.py +++ b/custom_components/tesla_custom/sensor.py @@ -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 ( @@ -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) diff --git a/poetry.lock b/poetry.lock index 81f6ffe0..1f8ac63a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1506,7 +1506,7 @@ tests = ["pytest", "pytest-cov"] [[package]] name = "teslajsonpy" -version = "3.4.1" +version = "3.5.1" description = "A library to work with Tesla API." category = "main" optional = false @@ -1678,7 +1678,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "8c8d7db623863241944928cd489ba210a169fa21d270293f7d7eea493d295320" +content-hash = "9cf41b6e3fabd3807396185cba1f43db696c0bb6cb3c640bcf79783ff927331d" [metadata.files] aiohttp = [ @@ -2012,6 +2012,7 @@ coverage = [ cryptography = [ {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"}, {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"}, + {file = "cryptography-38.0.3-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:726e3a1bfee0e919b278c8f766fdcf1fe30f8e6feea590e3f248d3636b58ffb3"}, {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"}, {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"}, {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"}, @@ -2926,8 +2927,8 @@ termcolor = [ {file = "termcolor-2.1.1.tar.gz", hash = "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b"}, ] teslajsonpy = [ - {file = "teslajsonpy-3.4.1-py3-none-any.whl", hash = "sha256:ef80d16460f347e582e331246311ad8973bbdbdf4bc1002b32c1cfb55da0fa6f"}, - {file = "teslajsonpy-3.4.1.tar.gz", hash = "sha256:6ff768e522def412a96b2c0cda295b91fbdc309aecad75815bd3e57067b41a75"}, + {file = "teslajsonpy-3.5.1-py3-none-any.whl", hash = "sha256:5a08fe43a84f617e8d6c03d794fac14681d9f73b0daae635e9f11fc431977735"}, + {file = "teslajsonpy-3.5.1.tar.gz", hash = "sha256:4918b10e531d51fc09042b934db70d6d45994155093a177a46d5a5b5edade234"}, ] text-unidecode = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, diff --git a/pyproject.toml b/pyproject.toml index f6b0c4ec..777a15b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/mock_data/car.py b/tests/mock_data/car.py index 2454d1bc..6cec4c34 100644 --- a/tests/mock_data/car.py +++ b/tests/mock_data/car.py @@ -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, diff --git a/tests/test_device_tracker.py b/tests/test_device_tracker.py index 5f7b34ba..85bbae2c 100644 --- a/tests/test_device_tracker.py +++ b/tests/test_device_tracker.py @@ -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") diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 1e981441..85ff1281 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -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.""" @@ -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