From f602a1bd45bcbd778146747f66fd841f0a9275f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B6rrle?= <7945681+CM000n@users.noreply.github.com> Date: Sat, 17 Feb 2024 20:02:57 +0100 Subject: [PATCH] Fix: "Use metric units" toggle has no effect and simplify sensor creation (#219) * Initiate config flow and add reauth function * remove DATA_SCHEMA * remove DATA_SCHEMA * add logger * add logger * set default metric values setting to true * add more logging regarding metric values * call for config entry inside get function * update logging * get metric_value from coordinator * log sensor entries * Add suggested_unit_of_measurement * Simplify sensor generation * change statistics SensorStateClass to TOTAL * Rever change on statistics senosrs state_class --- custom_components/toyota/__init__.py | 7 +- custom_components/toyota/config_flow.py | 97 ++++++------ custom_components/toyota/entity.py | 1 + custom_components/toyota/sensor.py | 191 +++++++----------------- 4 files changed, 111 insertions(+), 185 deletions(-) diff --git a/custom_components/toyota/__init__.py b/custom_components/toyota/__init__.py index efd527e..56a28b0 100644 --- a/custom_components/toyota/__init__.py +++ b/custom_components/toyota/__init__.py @@ -52,7 +52,6 @@ async def async_setup_entry( # pylint: disable=too-many-statements email = entry.data[CONF_EMAIL] password = entry.data[CONF_PASSWORD] - use_metric_values = entry.data[CONF_METRIC_VALUES] client = MyT( username=email, @@ -68,14 +67,16 @@ async def async_setup_entry( # pylint: disable=too-many-statements async def async_get_vehicle_data() -> Optional[list[VehicleData]]: """Fetch vehicle data from Toyota API.""" + metric_values = entry.data[CONF_METRIC_VALUES] + try: - vehicles = await asyncio.wait_for(client.get_vehicles(metric=use_metric_values), 15) + vehicles = await asyncio.wait_for(client.get_vehicles(metric=metric_values), 15) vehicle_informations: list[VehicleData] = [] if vehicles is not None: for vehicle in vehicles: await vehicle.update() vehicle_data = VehicleData( - data=vehicle, statistics=None, metric_values=use_metric_values + data=vehicle, statistics=None, metric_values=metric_values ) if vehicle.vin is not None: diff --git a/custom_components/toyota/config_flow.py b/custom_components/toyota/config_flow.py index d498949..9c8729e 100644 --- a/custom_components/toyota/config_flow.py +++ b/custom_components/toyota/config_flow.py @@ -1,52 +1,50 @@ """Config flow for Toyota Connected Services integration.""" import logging +from typing import Any, Mapping import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from mytoyota.client import MyT from mytoyota.exceptions import ToyotaInvalidUsernameError, ToyotaLoginError -# https://github.com/PyCQA/pylint/issues/3202 -from .const import CONF_METRIC_VALUES, DOMAIN # pylint: disable=unused-import +from .const import CONF_METRIC_VALUES, DOMAIN _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_EMAIL): str, - vol.Required(CONF_PASSWORD): str, - vol.Required( - CONF_METRIC_VALUES, - default=True, - ): selector.BooleanSelector(), - } -) - class ToyotaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Toyota Connected Services.""" VERSION = 1 + def __init__(self): + """Start the toyota custom component config flow.""" + self._reauth_entry = None + self._email = None + self._metric_values = True + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) + self._email = user_input[CONF_EMAIL] + self._metric_values = user_input[CONF_METRIC_VALUES] + unique_id = user_input[CONF_EMAIL].lower() - try: - client = MyT( - username=user_input[CONF_EMAIL], - password=user_input[CONF_PASSWORD], - ) + client = MyT( + username=user_input[CONF_EMAIL], + password=user_input[CONF_PASSWORD], + ) + await self.async_set_unique_id(unique_id) + if not self._reauth_entry: + self._abort_if_unique_id_configured() + try: await client.login() - except ToyotaLoginError as ex: errors["base"] = "invalid_auth" _LOGGER.error(ex) @@ -57,29 +55,34 @@ async def async_step_user(self, user_input=None) -> FlowResult: errors["base"] = "unknown" _LOGGER.error("An unknown error occurred during login request: %s", ex) else: - return self.async_create_entry(title=user_input[CONF_EMAIL], data=user_input) - - return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors=errors) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get async options flow.""" - return ToyotaOptionsFlowHandler(config_entry) - - -class ToyotaOptionsFlowHandler(config_entries.OptionsFlow): - """Config flow options handler for Toyota Connected Services.""" - - def __init__(self, config_entry): - """Initialize options flow.""" - self.config_entry = config_entry - self.options = dict(config_entry.options) - - async def async_step_init(self, user_input=None) -> FlowResult: - """Manage the options.""" - if user_input is not None: - self.options.update(user_input) - return self.async_create_entry( - title=self.config_entry.data.get(CONF_EMAIL), data=self.options - ) + if not self._reauth_entry: + return self.async_create_entry(title=user_input[CONF_EMAIL], data=user_input) + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input, unique_id=unique_id + ) + # Reload the config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL, default=self._email): str, + vol.Required(CONF_PASSWORD): str, + vol.Required( + CONF_METRIC_VALUES, default=self._metric_values + ): selector.BooleanSelector(), + } + ), + errors=errors, + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth if the user credentials have changed.""" + self._reauth_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self._email = entry_data[CONF_EMAIL] + self._metric_values = entry_data[CONF_METRIC_VALUES] + return await self.async_step_user() diff --git a/custom_components/toyota/entity.py b/custom_components/toyota/entity.py index d957715..182b0fa 100644 --- a/custom_components/toyota/entity.py +++ b/custom_components/toyota/entity.py @@ -49,6 +49,7 @@ def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self.vehicle = self.coordinator.data[self.index]["data"] self.statistics = self.coordinator.data[self.index]["statistics"] + self.metric_values = self.coordinator.data[self.index]["metric_values"] super()._handle_coordinator_update() async def async_added_to_hass(self) -> None: diff --git a/custom_components/toyota/sensor.py b/custom_components/toyota/sensor.py index cc4a35f..97cf295 100644 --- a/custom_components/toyota/sensor.py +++ b/custom_components/toyota/sensor.py @@ -22,7 +22,7 @@ from mytoyota.models.vehicle import Vehicle from . import StatisticsData, VehicleData -from .const import CONF_METRIC_VALUES, DOMAIN +from .const import DOMAIN from .entity import ToyotaBaseEntity from .utils import ( format_statistics_attributes, @@ -46,37 +46,21 @@ class ToyotaSensorEntityDescription(SensorEntityDescription, ToyotaSensorEntityD """Describes a Toyota sensor entity.""" -# TODO: There is currently no information on the licence plate. Add it, wehen available VIN_ENTITY_DESCRIPTION = ToyotaSensorEntityDescription( key="vin", translation_key="vin", icon="mdi:car-info", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, - native_unit_of_measurement=None, state_class=None, value_fn=lambda vehicle: vehicle.vin, attributes_fn=lambda vehicle: format_vin_sensor_attributes(vehicle._vehicle_info), ) -ODOMETER_ENTITY_DESCRIPTION_KM = ToyotaSensorEntityDescription( +ODOMETER_ENTITY_DESCRIPTION = ToyotaSensorEntityDescription( key="odometer", translation_key="odometer", icon="mdi:counter", device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.KILOMETERS, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda vehicle: None - if vehicle.dashboard is None - else round_number(vehicle.dashboard.odometer), - suggested_display_precision=0, - attributes_fn=lambda vehicle: None, # noqa : ARG005 -) -ODOMETER_ENTITY_DESCRIPTION_MILES = ToyotaSensorEntityDescription( - key="odometer", - translation_key="odometer", - icon="mdi:counter", - device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.MILES, state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda vehicle: None if vehicle.dashboard is None @@ -89,7 +73,6 @@ class ToyotaSensorEntityDescription(SensorEntityDescription, ToyotaSensorEntityD translation_key="fuel_level", icon="mdi:gas-station", device_class=None, - native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda vehicle: None if vehicle.dashboard is None @@ -97,25 +80,11 @@ class ToyotaSensorEntityDescription(SensorEntityDescription, ToyotaSensorEntityD suggested_display_precision=0, attributes_fn=lambda vehicle: None, # noqa : ARG005 ) -FUEL_RANGE_ENTITY_DESCRIPTION_KM = ToyotaSensorEntityDescription( - key="fuel_range", - translation_key="fuel_range", - icon="mdi:map-marker-distance", - device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.KILOMETERS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda vehicle: None - if vehicle.dashboard is None - else round_number(vehicle.dashboard.fuel_range), - suggested_display_precision=0, - attributes_fn=lambda vehicle: None, # noqa : ARG005 -) -FUEL_RANGE_ENTITY_DESCRIPTION_MILES = ToyotaSensorEntityDescription( +FUEL_RANGE_ENTITY_DESCRIPTION = ToyotaSensorEntityDescription( key="fuel_range", translation_key="fuel_range", icon="mdi:map-marker-distance", device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.MILES, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda vehicle: None if vehicle.dashboard is None @@ -127,8 +96,7 @@ class ToyotaSensorEntityDescription(SensorEntityDescription, ToyotaSensorEntityD key="battery_level", translation_key="battery_level", icon="mdi:car-electric", - device_class=None, - native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda vehicle: None if vehicle.dashboard is None @@ -136,12 +104,11 @@ class ToyotaSensorEntityDescription(SensorEntityDescription, ToyotaSensorEntityD suggested_display_precision=0, attributes_fn=lambda vehicle: None, # noqa : ARG005 ) -BATTERY_RANGE_ENTITY_DESCRIPTION_KM = ToyotaSensorEntityDescription( +BATTERY_RANGE_ENTITY_DESCRIPTION = ToyotaSensorEntityDescription( key="battery_range", translation_key="battery_range", icon="mdi:map-marker-distance", device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda vehicle: None if vehicle.dashboard is None @@ -149,38 +116,11 @@ class ToyotaSensorEntityDescription(SensorEntityDescription, ToyotaSensorEntityD suggested_display_precision=0, attributes_fn=lambda vehicle: None, # noqa : ARG005 ) -BATTERY_RANGE_ENTITY_DESCRIPTION_MILES = ToyotaSensorEntityDescription( - key="battery_range", - translation_key="battery_range", - icon="mdi:map-marker-distance", - device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.MILES, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda vehicle: None - if vehicle.dashboard is None - else round_number(vehicle.dashboard.battery_range), - suggested_display_precision=0, - attributes_fn=lambda vehicle: None, # noqa : ARG005 -) -BATTERY_RANGE_AC_ENTITY_DESCRIPTION_KM = ToyotaSensorEntityDescription( - key="battery_range_ac", - translation_key="battery_range_ac", - icon="mdi:map-marker-distance", - device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.KILOMETERS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda vehicle: None - if vehicle.dashboard is None - else round_number(vehicle.dashboard.battery_range_with_ac), - suggested_display_precision=0, - attributes_fn=lambda vehicle: None, # noqa : ARG005 -) -BATTERY_RANGE_AC_ENTITY_DESCRIPTION_MILES = ToyotaSensorEntityDescription( +BATTERY_RANGE_AC_ENTITY_DESCRIPTION = ToyotaSensorEntityDescription( key="battery_range_ac", translation_key="battery_range_ac", icon="mdi:map-marker-distance", device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.MILES, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda vehicle: None if vehicle.dashboard is None @@ -188,25 +128,11 @@ class ToyotaSensorEntityDescription(SensorEntityDescription, ToyotaSensorEntityD suggested_display_precision=0, attributes_fn=lambda vehicle: None, # noqa : ARG005 ) -TOTAL_RANGE_ENTITY_DESCRIPTION_KM = ToyotaSensorEntityDescription( - key="total_range", - translation_key="total_range", - icon="mdi:map-marker-distance", - device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.KILOMETERS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda vehicle: None - if vehicle.dashboard is None - else round_number(vehicle.dashboard.range), - suggested_display_precision=0, - attributes_fn=lambda vehicle: None, # noqa : ARG005 -) -TOTAL_RANGE_ENTITY_DESCRIPTION_MILES = ToyotaSensorEntityDescription( +TOTAL_RANGE_ENTITY_DESCRIPTION = ToyotaSensorEntityDescription( key="total_range", translation_key="total_range", icon="mdi:map-marker-distance", device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.MILES, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda vehicle: None if vehicle.dashboard is None @@ -282,103 +208,81 @@ async def async_setup_entry( sensors: list[Union[ToyotaSensor, ToyotaStatisticsSensor]] = [] for index, _ in enumerate(coordinator.data): vehicle = coordinator.data[index]["data"] + metric_values = coordinator.data[index]["metric_values"] + capabilities_descriptions = [ ( True, VIN_ENTITY_DESCRIPTION, ToyotaSensor, + None, ), ( - entry.data[CONF_METRIC_VALUES] is True - and vehicle._vehicle_info.extended_capabilities.telemetry_capable, - ODOMETER_ENTITY_DESCRIPTION_KM, - ToyotaSensor, - ), - ( - entry.data[CONF_METRIC_VALUES] is False - and vehicle._vehicle_info.extended_capabilities.telemetry_capable, - ODOMETER_ENTITY_DESCRIPTION_MILES, + vehicle._vehicle_info.extended_capabilities.telemetry_capable, + ODOMETER_ENTITY_DESCRIPTION, ToyotaSensor, + UnitOfLength.KILOMETERS if metric_values is True else UnitOfLength.MILES, ), ( vehicle._vehicle_info.extended_capabilities.fuel_level_available, FUEL_LEVEL_ENTITY_DESCRIPTION, ToyotaSensor, + PERCENTAGE, ), ( - entry.data[CONF_METRIC_VALUES] is True - and vehicle._vehicle_info.extended_capabilities.fuel_range_available, - FUEL_RANGE_ENTITY_DESCRIPTION_KM, - ToyotaSensor, - ), - ( - entry.data[CONF_METRIC_VALUES] is False - and vehicle._vehicle_info.extended_capabilities.fuel_range_available, - FUEL_RANGE_ENTITY_DESCRIPTION_MILES, + vehicle._vehicle_info.extended_capabilities.fuel_range_available, + FUEL_RANGE_ENTITY_DESCRIPTION, ToyotaSensor, + UnitOfLength.KILOMETERS if metric_values is True else UnitOfLength.MILES, ), ( vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable, BATTERY_LEVEL_ENTITY_DESCRIPTION, ToyotaSensor, + PERCENTAGE, ), ( - entry.data[CONF_METRIC_VALUES] is True - and vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable, - BATTERY_RANGE_ENTITY_DESCRIPTION_KM, - ToyotaSensor, - ), - ( - entry.data[CONF_METRIC_VALUES] is False - and vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable, - BATTERY_RANGE_ENTITY_DESCRIPTION_MILES, - ToyotaSensor, - ), - ( - entry.data[CONF_METRIC_VALUES] is True - and vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable, - BATTERY_RANGE_AC_ENTITY_DESCRIPTION_KM, - ToyotaSensor, - ), - ( - entry.data[CONF_METRIC_VALUES] is False - and vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable, - BATTERY_RANGE_AC_ENTITY_DESCRIPTION_MILES, + vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable, + BATTERY_RANGE_ENTITY_DESCRIPTION, ToyotaSensor, + UnitOfLength.KILOMETERS if metric_values is True else UnitOfLength.MILES, ), ( - entry.data[CONF_METRIC_VALUES] is True - and vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable - and vehicle._vehicle_info.extended_capabilities.fuel_range_available, - TOTAL_RANGE_ENTITY_DESCRIPTION_KM, + vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable, + BATTERY_RANGE_AC_ENTITY_DESCRIPTION, ToyotaSensor, + UnitOfLength.KILOMETERS if metric_values is True else UnitOfLength.MILES, ), ( - entry.data[CONF_METRIC_VALUES] is False - and vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable + vehicle._vehicle_info.extended_capabilities.econnect_vehicle_status_capable and vehicle._vehicle_info.extended_capabilities.fuel_range_available, - TOTAL_RANGE_ENTITY_DESCRIPTION_MILES, + TOTAL_RANGE_ENTITY_DESCRIPTION, ToyotaSensor, + UnitOfLength.KILOMETERS if metric_values is True else UnitOfLength.MILES, ), ( True, # TODO Unsure of the required capability STATISTICS_ENTITY_DESCRIPTIONS_DAILY, ToyotaStatisticsSensor, + UnitOfLength.KILOMETERS if metric_values is True else UnitOfLength.MILES, ), ( True, # TODO Unsure of the required capability STATISTICS_ENTITY_DESCRIPTIONS_WEEKLY, ToyotaStatisticsSensor, + UnitOfLength.KILOMETERS if metric_values is True else UnitOfLength.MILES, ), ( True, # TODO Unsure of the required capability STATISTICS_ENTITY_DESCRIPTIONS_MONTHLY, ToyotaStatisticsSensor, + UnitOfLength.KILOMETERS if metric_values is True else UnitOfLength.MILES, ), ( True, # TODO Unsure of the required capability STATISTICS_ENTITY_DESCRIPTIONS_YEARLY, ToyotaStatisticsSensor, + UnitOfLength.KILOMETERS if metric_values is True else UnitOfLength.MILES, ), ] @@ -388,8 +292,9 @@ async def async_setup_entry( entry_id=entry.entry_id, vehicle_index=index, description=description, + unit=unit, ) - for capability, description, sensor_type in capabilities_descriptions + for capability, description, sensor_type, unit in capabilities_descriptions if capability ) @@ -399,35 +304,51 @@ async def async_setup_entry( class ToyotaSensor(ToyotaBaseEntity, SensorEntity): """Representation of a Toyota sensor.""" + vehicle: Vehicle + + def __init__( # noqa: PLR0913 + self, + coordinator: DataUpdateCoordinator[list[VehicleData]], + entry_id: str, + vehicle_index: int, + description: ToyotaSensorEntityDescription, + unit: Union[UnitOfLength, str], + ) -> None: + """Initialise the ToyotaSensor class.""" + super().__init__(coordinator, entry_id, vehicle_index, description) + self.description = description + self._attr_native_unit_of_measurement = unit + self._attr_suggested_unit_of_measurement = unit + @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return self.entity_description.value_fn(self.vehicle) + return self.description.value_fn(self.vehicle) @property def extra_state_attributes(self) -> Optional[dict[str, Any]]: """Return the attributes of the sensor.""" - return self.entity_description.attributes_fn(self.vehicle) + return self.description.attributes_fn(self.vehicle) -class ToyotaStatisticsSensor(ToyotaSensor): +class ToyotaStatisticsSensor(ToyotaBaseEntity, SensorEntity): """Representation of a Toyota statistics sensor.""" statistics: StatisticsData - def __init__( + def __init__( # noqa: PLR0913 self, coordinator: DataUpdateCoordinator[list[VehicleData]], entry_id: str, vehicle_index: int, description: ToyotaStatisticsSensorEntityDescription, + unit: Union[UnitOfLength, str], ) -> None: """Initialise the ToyotaStatisticsSensor class.""" super().__init__(coordinator, entry_id, vehicle_index, description) self.period: Literal["day", "week", "month", "year"] = description.period - self._attr_native_unit_of_measurement = ( - UnitOfLength.KILOMETERS if self.metric_values else UnitOfLength.MILES - ) + self._attr_native_unit_of_measurement = unit + self._attr_suggested_unit_of_measurement = unit @property def native_value(self) -> StateType: