From 3e3d1a384eb19cf13c07d78c2c46ea747c0c9be3 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Fri, 13 Aug 2021 00:15:24 -0700 Subject: [PATCH] feat: add ChargingEnergySensor --- teslajsonpy/controller.py | 8 +- teslajsonpy/homeassistant/charger.py | 145 ++++++++++++++++++ .../homeassistant/test_charging_sensor.py | 61 ++++++-- 3 files changed, 201 insertions(+), 13 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index ef679982..878def91 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -33,7 +33,12 @@ ParkingSensor, UpdateSensor, ) -from teslajsonpy.homeassistant.charger import ChargerSwitch, ChargingSensor, RangeSwitch +from teslajsonpy.homeassistant.charger import ( + ChargerSwitch, + ChargingEnergySensor, + ChargingSensor, + RangeSwitch, +) from teslajsonpy.homeassistant.climate import Climate, TempSensor from teslajsonpy.homeassistant.gps import GPS, Odometer from teslajsonpy.homeassistant.heated_seats import HeatedSeatSwitch @@ -548,6 +553,7 @@ def _add_components(self, car): self.__components.append(ChargerLock(car, self)) self.__components.append(ChargerConnectionSensor(car, self)) self.__components.append(ChargingSensor(car, self)) + self.__components.append(ChargingEnergySensor(car, self)) self.__components.append(ChargerSwitch(car, self)) self.__components.append(RangeSwitch(car, self)) self.__components.append(ParkingSensor(car, self)) diff --git a/teslajsonpy/homeassistant/charger.py b/teslajsonpy/homeassistant/charger.py index 5ecc5453..3b49e227 100644 --- a/teslajsonpy/homeassistant/charger.py +++ b/teslajsonpy/homeassistant/charger.py @@ -7,6 +7,7 @@ """ import time from typing import Dict, Optional, Text +import datetime from teslajsonpy.homeassistant.vehicle import VehicleDevice @@ -270,3 +271,147 @@ def charger_power(self) -> float: def device_class(self) -> Text: """Return the HA device class.""" return self._device_class + + +class ChargingEnergySensor(VehicleDevice): + """Home-Assistant energy sensor class for a Tesla VehicleDevice.""" + + def __init__(self, data: Dict, controller) -> None: + """Initialize the Charger sensor. + + Args: + data (Dict): The charging parameters for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/chargestate + controller (Controller): The controller that controls updates to the Tesla API. + + """ + super().__init__(data, controller) + self.type: Text = "energy added sensor" + self.__rated: bool = True + self.__miles: bool = True + self.measurement: Text = "kWh" + self.hass_type: Text = "sensor" + self._device_class: Optional[Text] = "energy" + self.name: Text = self._name() + self.uniq_name: Text = self._uniq_name() + self.__added_range = None + self.__charge_energy_added = None + self.__charging_rate = None + self.__time_to_full = None + self.__charge_current_request = None + self.__charger_actual_current = None + self.__charger_voltage = None + self.__charge_limit_soc = None + self.__charger_power = None + self.__last_reset: datetime.datetime = datetime.time.min + + async def async_update(self, wake_if_asleep=False, force=False) -> None: + """Update the battery state.""" + await super().async_update(wake_if_asleep=wake_if_asleep) + self.refresh() + + def refresh(self) -> None: + """Refresh data. + + This assumes the controller has already been updated + """ + super().refresh() + data = self._controller.get_gui_params(self._id) + if data: + self.__miles = data["gui_distance_units"] == "mi/hr" + self.__rated = data["gui_range_display"] == "Rated" + data = self._controller.get_charging_params(self._id) + if data: + self.attrs["charger_phases"] = data["charger_phases"] + self.__added_range = ( + data["charge_miles_added_rated"] + if self.__rated + else data["charge_miles_added_ideal"] + ) + if ( + self.__charge_energy_added + and self.__charge_energy_added >= data["charge_energy_added"] + ): + self.__last_reset = datetime.datetime.utcnow() + self.__charge_energy_added = data["charge_energy_added"] + self.__charging_rate = data["charge_rate"] + self.__time_to_full = data["time_to_full_charge"] + self.__charge_current_request = data["charge_current_request"] + self.__charger_actual_current = data["charger_actual_current"] + self.__charger_voltage = data["charger_voltage"] + self.__charge_limit_soc = data["charge_limit_soc"] + self.__charger_power = data["charger_power"] + self.attrs["charge_limit_soc"] = self.charge_limit_soc + self.attrs["last_reset"] = str(self.last_reset) + if self.__miles: + self.__added_range = round(self.__added_range / 0.621371, 2) + self.__charging_rate = round(self.__charging_rate / 0.621371, 2) + + @staticmethod + def has_battery() -> bool: + """Return whether the device has a battery.""" + return False + + @property + def charging_rate(self) -> float: + """Return the charging rate.""" + return self.__charging_rate + + @property + def time_left(self) -> float: + """Return the time left to full in hours.""" + return self.__time_to_full + + @property + def added_range(self) -> float: + """Return the added range.""" + return self.__added_range + + @property + def charge_current_request(self) -> float: + """Return the requested current.""" + return self.__charge_current_request + + @property + def charger_actual_current(self) -> float: + """Return the actual current.""" + return self.__charger_actual_current + + @property + def charger_voltage(self) -> float: + """Return the voltage.""" + return self.__charger_voltage + + @property + def charge_energy_added(self) -> float: + """Return the energy added.""" + return self.__charge_energy_added + + @property + def charge_limit_soc(self) -> int: + """Return the state of charge limit.""" + return self.__charge_limit_soc + + @property + def charger_power(self) -> float: + """Return the state of charger power.""" + return self.__charger_power + + @property + def device_class(self) -> Text: + """Return the HA device class.""" + return self._device_class + + @property + def last_reset(self) -> datetime.datetime: + """Return the last reset time.""" + return self.__last_reset + + @property + def state_class(self) -> Text: + """Return the state class.""" + return "measurement" + + def get_value(self) -> float: + """Return charge energy added.""" + return self.charge_energy_added diff --git a/tests/unit_tests/homeassistant/test_charging_sensor.py b/tests/unit_tests/homeassistant/test_charging_sensor.py index c91f9def..a7581215 100644 --- a/tests/unit_tests/homeassistant/test_charging_sensor.py +++ b/tests/unit_tests/homeassistant/test_charging_sensor.py @@ -3,7 +3,7 @@ import pytest from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.charger import ChargingSensor +from teslajsonpy.homeassistant.charger import ChargingSensor, ChargingEnergySensor from tests.tesla_mock import TeslaMock @@ -32,6 +32,21 @@ def test_device_class(monkeypatch): assert _sensor.device_class is None +def test_state_class(monkeypatch): + """Test device_class().""" + + _mock = TeslaMock(monkeypatch) + _controller = Controller(None) + + _data = _mock.data_request_vehicle() + _sensor = ChargingSensor(_data, _controller) + + assert _sensor.device_class is None + + _sensor2 = ChargingEnergySensor(_data, _controller) + assert _sensor2.device_class == "energy" + + def test_get_value_on_init(monkeypatch): """Test get_value() after initialization.""" @@ -39,17 +54,23 @@ def test_get_value_on_init(monkeypatch): _controller = Controller(None) _data = _mock.data_request_vehicle() - _sensor = ChargingSensor(_data, _controller) + sensors = [ + ChargingSensor(_data, _controller), + ChargingEnergySensor(_data, _controller), + ] + + for _sensor in sensors: - assert not _sensor is None - assert _sensor.charging_rate is None - assert _sensor.time_left is None - assert _sensor.added_range is None - assert _sensor.charge_current_request is None - assert _sensor.charger_actual_current is None - assert _sensor.charger_voltage is None - assert _sensor.charge_energy_added is None - assert _sensor.charge_limit_soc is None + assert _sensor is not None + assert _sensor.charging_rate is None + assert _sensor.time_left is None + assert _sensor.added_range is None + assert _sensor.charge_current_request is None + assert _sensor.charger_actual_current is None + assert _sensor.charger_voltage is None + assert _sensor.charger_power is None + assert _sensor.charge_energy_added is None + assert _sensor.charge_limit_soc is None @pytest.mark.asyncio @@ -61,7 +82,7 @@ async def test_get_value_after_update(monkeypatch): _data = _mock.data_request_vehicle() _sensor = ChargingSensor(_data, _controller) - + _sensor2 = ChargingEnergySensor(_data, _controller) await _sensor.async_update() assert _sensor is not None @@ -74,7 +95,23 @@ async def test_get_value_after_update(monkeypatch): assert _sensor.charger_power == 0 assert _sensor.charge_energy_added == 12.41 assert _sensor.charge_limit_soc == 90 + assert _sensor.device_class is None + _sensor2 = ChargingEnergySensor(_data, _controller) + await _sensor2.async_update() + + assert _sensor2 is not None + assert _sensor2.charging_rate == 0 + assert _sensor2.time_left == 0 + assert _sensor2.charge_current_request == 48 + assert _sensor2.charger_actual_current == 0 + assert _sensor2.charger_voltage == 0 + assert _sensor2.charger_power == 0 + assert _sensor2.charge_energy_added == 12.41 + assert _sensor2.charge_limit_soc == 90 + assert _sensor2.last_reset != 0 + assert _sensor2.state_class == "measurement" + assert _sensor2.device_class == "energy" @pytest.mark.asyncio async def test_async_update(monkeypatch):