From c0afb32ed4f76f81fa2cb8b42a08e7dd3c91521e Mon Sep 17 00:00:00 2001 From: MillerKyle72 Date: Mon, 5 Dec 2022 21:08:14 -0500 Subject: [PATCH 1/6] setup destination location, arrival time, distance --- .../tesla_custom/device_tracker.py | 35 +++++++++ custom_components/tesla_custom/sensor.py | 66 ++++++++++++++++ tests/mock_data/car.py | 7 ++ tests/test_device_tracker.py | 6 ++ tests/test_sensor.py | 75 +++++++++++++++++++ 5 files changed, 189 insertions(+) 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/sensor.py b/custom_components/tesla_custom/sensor.py index cce5433a..437fcfe5 100644 --- a/custom_components/tesla_custom/sensor.py +++ b/custom_components/tesla_custom/sensor.py @@ -72,6 +72,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 ( @@ -556,3 +558,67 @@ 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._value: 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._value + else: + min_duration = round(float(self._car.active_route_minutes_to_arrival), 2) + new_value = dt.utcnow() + timedelta(minutes=min_duration) + if self._value is None or (new_value - self._value).total_seconds() >= 60: + self._value = new_value + return self._value + + @property + def extra_state_attributes(self): + """Return device state attributes.""" + return { + "Energy at arrival": self._car.active_route_energy_at_arrival, + "Minutes traffic delay": round( + self._car.active_route_traffic_minutes_delay, 1 + ), + "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.""" + return round(self._car.active_route_miles_to_arrival, 2) diff --git a/tests/mock_data/car.py b/tests/mock_data/car.py index 3d4c1f44..dadd65ae 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.80, + "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 9cff68c9..e389b8c1 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -81,6 +81,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.""" @@ -529,3 +535,72 @@ async def test_tpms_pressure_none(hass: HomeAssistant) -> None: assert state_fl.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.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 From 61a6f79e53a6a2fd96ea6ea936a5f604f4d2a2c8 Mon Sep 17 00:00:00 2001 From: MillerKyle72 Date: Wed, 7 Dec 2022 16:22:52 -0500 Subject: [PATCH 2/6] account for time passing between api calls --- custom_components/tesla_custom/sensor.py | 26 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/custom_components/tesla_custom/sensor.py b/custom_components/tesla_custom/sensor.py index 437fcfe5..a422e285 100644 --- a/custom_components/tesla_custom/sensor.py +++ b/custom_components/tesla_custom/sensor.py @@ -575,19 +575,33 @@ def __init__( self._attr_device_class = SensorDeviceClass.TIMESTAMP self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_icon = "mdi:timer-sand" - self._value: Optional[datetime] = None + 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._value + return self._datetime_value else: min_duration = round(float(self._car.active_route_minutes_to_arrival), 2) - new_value = dt.utcnow() + timedelta(minutes=min_duration) - if self._value is None or (new_value - self._value).total_seconds() >= 60: - self._value = new_value - return self._value + + 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): From f75c5005fec14a41d84bb8e73df4464c51bd8ce2 Mon Sep 17 00:00:00 2001 From: MillerKyle72 Date: Sat, 10 Dec 2022 10:22:25 -0500 Subject: [PATCH 3/6] bump teslajsonpy --- custom_components/tesla_custom/manifest.json | 2 +- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/tesla_custom/manifest.json b/custom_components/tesla_custom/manifest.json index 0143a087..a8040760 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.3.0" + "teslajsonpy==3.5.0" ], "codeowners": [ "@alandtse" diff --git a/poetry.lock b/poetry.lock index 113dcff1..c6014d7f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1506,7 +1506,7 @@ tests = ["pytest", "pytest-cov"] [[package]] name = "teslajsonpy" -version = "3.3.0" +version = "3.5.0" 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 = "39913e7746e29d1b89ca54649e2919e1bba0f83925b52e2bceff6ba63bc08bd0" +content-hash = "43cc8bb6fc42c924a9365b88f5beee0c9f368c6ae791c04130199178b5e1efc7" [metadata.files] aiohttp = [ @@ -2927,8 +2927,8 @@ termcolor = [ {file = "termcolor-2.1.1.tar.gz", hash = "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b"}, ] teslajsonpy = [ - {file = "teslajsonpy-3.3.0-py3-none-any.whl", hash = "sha256:2eb94c71c3d672adf4acde29a4b92a0aa9c5fe6c1749282ad97dc660675aeaf3"}, - {file = "teslajsonpy-3.3.0.tar.gz", hash = "sha256:1b314badd75346149b188d904faa77d04407311a0d5d6343eebce4c19d7cec7e"}, + {file = "teslajsonpy-3.5.0-py3-none-any.whl", hash = "sha256:6e68d519208f6e84ef309074704e72b904d4d87c5e09034bcc12f158aa76ab55"}, + {file = "teslajsonpy-3.5.0.tar.gz", hash = "sha256:dacbcc8c8e7dfe31d6dd2ad776dd3a8c2414d47a1be91585e879139c5fb95941"}, ] text-unidecode = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, diff --git a/pyproject.toml b/pyproject.toml index 386a4b94..368bf776 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "Apache-2.0" [tool.poetry.dependencies] python = "^3.10" -teslajsonpy = "^3.3.0" +teslajsonpy = "^3.5.0" [tool.poetry.dev-dependencies] homeassistant = ">=2021.10.0" From c07592a55221c3c75e93684f3b2ad92d0f44606f Mon Sep 17 00:00:00 2001 From: MillerKyle72 Date: Sat, 10 Dec 2022 10:37:27 -0500 Subject: [PATCH 4/6] check None before rounding --- custom_components/tesla_custom/sensor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/custom_components/tesla_custom/sensor.py b/custom_components/tesla_custom/sensor.py index a422e285..390150b0 100644 --- a/custom_components/tesla_custom/sensor.py +++ b/custom_components/tesla_custom/sensor.py @@ -606,11 +606,14 @@ def native_value(self) -> Optional[datetime]: @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": round( - self._car.active_route_traffic_minutes_delay, 1 - ), + "Minutes traffic delay": minutes, "Destination": self._car.active_route_destination, } @@ -635,4 +638,6 @@ def __init__( @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) From c70951c11349a677fb38ac4a5201e4995b1c6c1b Mon Sep 17 00:00:00 2001 From: MillerKyle72 Date: Sat, 10 Dec 2022 10:44:33 -0500 Subject: [PATCH 5/6] test --- tests/mock_data/car.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mock_data/car.py b/tests/mock_data/car.py index 11805731..6cec4c34 100644 --- a/tests/mock_data/car.py +++ b/tests/mock_data/car.py @@ -122,7 +122,7 @@ "active_route_energy_at_arrival": 40, "active_route_latitude": 34.111111, "active_route_longitude": -88.11111, - "active_route_miles_to_arrival": 19.80, + "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, From 54e436b0222eed0ee9b5cb010ea12b432b37a0df Mon Sep 17 00:00:00 2001 From: MillerKyle72 Date: Sun, 11 Dec 2022 10:23:35 -0500 Subject: [PATCH 6/6] teslajsonpy 3.5.1 --- custom_components/tesla_custom/manifest.json | 2 +- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/tesla_custom/manifest.json b/custom_components/tesla_custom/manifest.json index 2f4aaf61..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.5.0" + "teslajsonpy==3.5.1" ], "codeowners": [ "@alandtse" diff --git a/poetry.lock b/poetry.lock index 24cc729f..1f8ac63a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1506,7 +1506,7 @@ tests = ["pytest", "pytest-cov"] [[package]] name = "teslajsonpy" -version = "3.5.0" +version = "3.5.1" description = "A library to work with Tesla API." category = "main" optional = false @@ -1678,8 +1678,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "43cc8bb6fc42c924a9365b88f5beee0c9f368c6ae791c04130199178b5e1efc7" - +content-hash = "9cf41b6e3fabd3807396185cba1f43db696c0bb6cb3c640bcf79783ff927331d" [metadata.files] aiohttp = [ @@ -2013,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"}, @@ -2927,8 +2927,8 @@ termcolor = [ {file = "termcolor-2.1.1.tar.gz", hash = "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b"}, ] teslajsonpy = [ - {file = "teslajsonpy-3.5.0-py3-none-any.whl", hash = "sha256:6e68d519208f6e84ef309074704e72b904d4d87c5e09034bcc12f158aa76ab55"}, - {file = "teslajsonpy-3.5.0.tar.gz", hash = "sha256:dacbcc8c8e7dfe31d6dd2ad776dd3a8c2414d47a1be91585e879139c5fb95941"}, + {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 c54c84d0..777a15b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "Apache-2.0" [tool.poetry.dependencies] python = "^3.10" -teslajsonpy = "^3.5.0" +teslajsonpy = "^3.5.1" [tool.poetry.dev-dependencies]