Update sensors for Subaru integration (#66996)

* Update sensor.py

* Change "EV Time to Fully Charged" type to datetime object (HA 2022.2)

* Validate types before accessing dict entries

* Test handling of invalid data from Subaru

* Bump to subarulink 0.4.2

* Incorporate style suggestion

* Update sensor.py to use SensorEntity

* isort tests

* Remove SubaruSensor.current_value

* Fix isort errors

* Resolve conflict from previous PR (add locks)

* Fix linting errors in config_flow.py

* Incorporate PR review comments for sensor

* Incorporate PR review comments for sensor

* Make 3rd party library responsible for API data parsing

* Add type annotations to sensor.py

* Incorporate PR review comments

* Incorporate PR review comments

* Set _attr_has_entity_name = True for sensors
This commit is contained in:
Garrett 2022-10-01 18:25:49 -04:00 committed by GitHub
parent 35fa73eee9
commit 9058b5b9c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 225 additions and 272 deletions

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
import logging import logging
from typing import Any
from subarulink import ( from subarulink import (
Controller as SubaruAPI, Controller as SubaruAPI,
@ -16,6 +17,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN
@ -36,7 +38,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.config_data = {CONF_PIN: None} self.config_data = {CONF_PIN: None}
self.controller = None self.controller = None
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the start of the config flow.""" """Handle the start of the config flow."""
error = None error = None
@ -117,7 +121,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.debug("Successfully authenticated with Subaru API") _LOGGER.debug("Successfully authenticated with Subaru API")
self.config_data.update(data) self.config_data.update(data)
async def async_step_two_factor(self, user_input=None): async def async_step_two_factor(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Select contact method and request 2FA code from Subaru.""" """Select contact method and request 2FA code from Subaru."""
error = None error = None
if user_input: if user_input:
@ -143,7 +149,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="two_factor", data_schema=data_schema, errors=error step_id="two_factor", data_schema=data_schema, errors=error
) )
async def async_step_two_factor_validate(self, user_input=None): async def async_step_two_factor_validate(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Validate received 2FA code with Subaru.""" """Validate received 2FA code with Subaru."""
error = None error = None
if user_input: if user_input:
@ -166,7 +174,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="two_factor_validate", data_schema=data_schema, errors=error step_id="two_factor_validate", data_schema=data_schema, errors=error
) )
async def async_step_pin(self, user_input=None): async def async_step_pin(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle second part of config flow, if required.""" """Handle second part of config flow, if required."""
error = None error = None
if user_input and self.controller.update_saved_pin(user_input[CONF_PIN]): if user_input and self.controller.update_saved_pin(user_input[CONF_PIN]):
@ -193,7 +203,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry self.config_entry = config_entry
async def async_step_init(self, user_input=None): async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle options flow.""" """Handle options flow."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)

View File

@ -31,7 +31,7 @@ VEHICLE_STATUS = "status"
API_GEN_1 = "g1" API_GEN_1 = "g1"
API_GEN_2 = "g2" API_GEN_2 = "g2"
MANUFACTURER = "Subaru Corp." MANUFACTURER = "Subaru"
PLATFORMS = [ PLATFORMS = [
Platform.LOCK, Platform.LOCK,

View File

@ -1,35 +0,0 @@
"""Base class for all Subaru Entities."""
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER, VEHICLE_NAME, VEHICLE_VIN
class SubaruEntity(CoordinatorEntity):
"""Representation of a Subaru Entity."""
def __init__(self, vehicle_info, coordinator):
"""Initialize the Subaru Entity."""
super().__init__(coordinator)
self.car_name = vehicle_info[VEHICLE_NAME]
self.vin = vehicle_info[VEHICLE_VIN]
self.entity_type = "entity"
@property
def name(self):
"""Return name."""
return f"{self.car_name} {self.entity_type}"
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return f"{self.vin}_{self.entity_type}"
@property
def device_info(self) -> DeviceInfo:
"""Return the device_info of the device."""
return DeviceInfo(
identifiers={(DOMAIN, self.vin)},
manufacturer=MANUFACTURER,
name=self.car_name,
)

View File

@ -3,7 +3,7 @@
"name": "Subaru", "name": "Subaru",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/subaru", "documentation": "https://www.home-assistant.io/integrations/subaru",
"requirements": ["subarulink==0.5.0"], "requirements": ["subarulink==0.6.0"],
"codeowners": ["@G-Two"], "codeowners": ["@G-Two"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["stdiomask", "subarulink"] "loggers": ["stdiomask", "subarulink"]

View File

@ -1,10 +1,16 @@
"""Support for Subaru sensors.""" """Support for Subaru sensors."""
from __future__ import annotations
import logging
from typing import Any
import subarulink.const as sc import subarulink.const as sc
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
DEVICE_CLASSES,
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
SensorEntityDescription,
SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -14,128 +20,133 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
PRESSURE_HPA, PRESSURE_HPA,
TEMP_CELSIUS, TEMP_CELSIUS,
TIME_MINUTES,
VOLUME_GALLONS, VOLUME_GALLONS,
VOLUME_LITERS, VOLUME_LITERS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter from homeassistant.helpers.update_coordinator import (
from homeassistant.util.unit_system import ( CoordinatorEntity,
IMPERIAL_SYSTEM, DataUpdateCoordinator,
LENGTH_UNITS,
PRESSURE_UNITS,
TEMPERATURE_UNITS,
) )
from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, LENGTH_UNITS, PRESSURE_UNITS
from . import get_device_info
from .const import ( from .const import (
API_GEN_2, API_GEN_2,
DOMAIN, DOMAIN,
ENTRY_COORDINATOR, ENTRY_COORDINATOR,
ENTRY_VEHICLES, ENTRY_VEHICLES,
ICONS,
VEHICLE_API_GEN, VEHICLE_API_GEN,
VEHICLE_HAS_EV, VEHICLE_HAS_EV,
VEHICLE_HAS_SAFETY_SERVICE, VEHICLE_HAS_SAFETY_SERVICE,
VEHICLE_STATUS, VEHICLE_STATUS,
VEHICLE_VIN,
) )
from .entity import SubaruEntity
_LOGGER = logging.getLogger(__name__)
# Fuel consumption units
FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS = "L/100km"
FUEL_CONSUMPTION_MILES_PER_GALLON = "mi/gal"
L_PER_GAL = VolumeConverter.convert(1, VOLUME_GALLONS, VOLUME_LITERS) L_PER_GAL = VolumeConverter.convert(1, VOLUME_GALLONS, VOLUME_LITERS)
KM_PER_MI = DistanceConverter.convert(1, LENGTH_MILES, LENGTH_KILOMETERS) KM_PER_MI = DistanceConverter.convert(1, LENGTH_MILES, LENGTH_KILOMETERS)
# Fuel Economy Constants # Sensor available to "Subaru Safety Plus" subscribers with Gen1 or Gen2 vehicles
FUEL_CONSUMPTION_L_PER_100KM = "L/100km"
FUEL_CONSUMPTION_MPG = "mi/gal"
FUEL_CONSUMPTION_UNITS = [FUEL_CONSUMPTION_L_PER_100KM, FUEL_CONSUMPTION_MPG]
SENSOR_TYPE = "type"
SENSOR_CLASS = "class"
SENSOR_FIELD = "field"
SENSOR_UNITS = "units"
# Sensor data available to "Subaru Safety Plus" subscribers with Gen1 or Gen2 vehicles
SAFETY_SENSORS = [ SAFETY_SENSORS = [
{ SensorEntityDescription(
SENSOR_TYPE: "Odometer", key=sc.ODOMETER,
SENSOR_CLASS: None, icon="mdi:road-variant",
SENSOR_FIELD: sc.ODOMETER, name="Odometer",
SENSOR_UNITS: LENGTH_KILOMETERS, native_unit_of_measurement=LENGTH_KILOMETERS,
}, state_class=SensorStateClass.TOTAL_INCREASING,
),
] ]
# Sensor data available to "Subaru Safety Plus" subscribers with Gen2 vehicles # Sensors available to "Subaru Safety Plus" subscribers with Gen2 vehicles
API_GEN_2_SENSORS = [ API_GEN_2_SENSORS = [
{ SensorEntityDescription(
SENSOR_TYPE: "Avg Fuel Consumption", key=sc.AVG_FUEL_CONSUMPTION,
SENSOR_CLASS: None, icon="mdi:leaf",
SENSOR_FIELD: sc.AVG_FUEL_CONSUMPTION, name="Avg Fuel Consumption",
SENSOR_UNITS: FUEL_CONSUMPTION_L_PER_100KM, native_unit_of_measurement=FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
}, state_class=SensorStateClass.MEASUREMENT,
{ ),
SENSOR_TYPE: "Range", SensorEntityDescription(
SENSOR_CLASS: None, key=sc.DIST_TO_EMPTY,
SENSOR_FIELD: sc.DIST_TO_EMPTY, icon="mdi:gas-station",
SENSOR_UNITS: LENGTH_KILOMETERS, name="Range",
}, native_unit_of_measurement=LENGTH_KILOMETERS,
{ state_class=SensorStateClass.MEASUREMENT,
SENSOR_TYPE: "Tire Pressure FL", ),
SENSOR_CLASS: SensorDeviceClass.PRESSURE, SensorEntityDescription(
SENSOR_FIELD: sc.TIRE_PRESSURE_FL, key=sc.TIRE_PRESSURE_FL,
SENSOR_UNITS: PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE,
}, name="Tire Pressure FL",
{ native_unit_of_measurement=PRESSURE_HPA,
SENSOR_TYPE: "Tire Pressure FR", state_class=SensorStateClass.MEASUREMENT,
SENSOR_CLASS: SensorDeviceClass.PRESSURE, ),
SENSOR_FIELD: sc.TIRE_PRESSURE_FR, SensorEntityDescription(
SENSOR_UNITS: PRESSURE_HPA, key=sc.TIRE_PRESSURE_FR,
}, device_class=SensorDeviceClass.PRESSURE,
{ name="Tire Pressure FR",
SENSOR_TYPE: "Tire Pressure RL", native_unit_of_measurement=PRESSURE_HPA,
SENSOR_CLASS: SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT,
SENSOR_FIELD: sc.TIRE_PRESSURE_RL, ),
SENSOR_UNITS: PRESSURE_HPA, SensorEntityDescription(
}, key=sc.TIRE_PRESSURE_RL,
{ device_class=SensorDeviceClass.PRESSURE,
SENSOR_TYPE: "Tire Pressure RR", name="Tire Pressure RL",
SENSOR_CLASS: SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_HPA,
SENSOR_FIELD: sc.TIRE_PRESSURE_RR, state_class=SensorStateClass.MEASUREMENT,
SENSOR_UNITS: PRESSURE_HPA, ),
}, SensorEntityDescription(
{ key=sc.TIRE_PRESSURE_RR,
SENSOR_TYPE: "External Temp", device_class=SensorDeviceClass.PRESSURE,
SENSOR_CLASS: SensorDeviceClass.TEMPERATURE, name="Tire Pressure RR",
SENSOR_FIELD: sc.EXTERNAL_TEMP, native_unit_of_measurement=PRESSURE_HPA,
SENSOR_UNITS: TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT,
}, ),
{ SensorEntityDescription(
SENSOR_TYPE: "12V Battery Voltage", key=sc.EXTERNAL_TEMP,
SENSOR_CLASS: SensorDeviceClass.VOLTAGE, device_class=SensorDeviceClass.TEMPERATURE,
SENSOR_FIELD: sc.BATTERY_VOLTAGE, name="External Temp",
SENSOR_UNITS: ELECTRIC_POTENTIAL_VOLT, native_unit_of_measurement=TEMP_CELSIUS,
}, state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.BATTERY_VOLTAGE,
device_class=SensorDeviceClass.VOLTAGE,
name="12V Battery Voltage",
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
] ]
# Sensor data available to "Subaru Safety Plus" subscribers with PHEV vehicles # Sensors available to "Subaru Safety Plus" subscribers with PHEV vehicles
EV_SENSORS = [ EV_SENSORS = [
{ SensorEntityDescription(
SENSOR_TYPE: "EV Range", key=sc.EV_DISTANCE_TO_EMPTY,
SENSOR_CLASS: None, icon="mdi:ev-station",
SENSOR_FIELD: sc.EV_DISTANCE_TO_EMPTY, name="EV Range",
SENSOR_UNITS: LENGTH_MILES, native_unit_of_measurement=LENGTH_MILES,
}, state_class=SensorStateClass.MEASUREMENT,
{ ),
SENSOR_TYPE: "EV Battery Level", SensorEntityDescription(
SENSOR_CLASS: SensorDeviceClass.BATTERY, key=sc.EV_STATE_OF_CHARGE_PERCENT,
SENSOR_FIELD: sc.EV_STATE_OF_CHARGE_PERCENT, device_class=SensorDeviceClass.BATTERY,
SENSOR_UNITS: PERCENTAGE, name="EV Battery Level",
}, native_unit_of_measurement=PERCENTAGE,
{ state_class=SensorStateClass.MEASUREMENT,
SENSOR_TYPE: "EV Time to Full Charge", ),
SENSOR_CLASS: SensorDeviceClass.TIMESTAMP, SensorEntityDescription(
SENSOR_FIELD: sc.EV_TIME_TO_FULLY_CHARGED, key=sc.EV_TIME_TO_FULLY_CHARGED_UTC,
SENSOR_UNITS: TIME_MINUTES, device_class=SensorDeviceClass.TIMESTAMP,
}, name="EV Time to Full Charge",
state_class=SensorStateClass.MEASUREMENT,
),
] ]
@ -145,123 +156,111 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Subaru sensors by config_entry.""" """Set up the Subaru sensors by config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR] entry = hass.data[DOMAIN][config_entry.entry_id]
vehicle_info = hass.data[DOMAIN][config_entry.entry_id][ENTRY_VEHICLES] coordinator = entry[ENTRY_COORDINATOR]
vehicle_info = entry[ENTRY_VEHICLES]
entities = [] entities = []
for vin in vehicle_info: for info in vehicle_info.values():
entities.extend(create_vehicle_sensors(vehicle_info[vin], coordinator)) entities.extend(create_vehicle_sensors(info, coordinator))
async_add_entities(entities, True) async_add_entities(entities)
def create_vehicle_sensors(vehicle_info, coordinator): def create_vehicle_sensors(
vehicle_info, coordinator: DataUpdateCoordinator
) -> list[SubaruSensor]:
"""Instantiate all available sensors for the vehicle.""" """Instantiate all available sensors for the vehicle."""
sensors_to_add = [] sensor_descriptions_to_add = []
if vehicle_info[VEHICLE_HAS_SAFETY_SERVICE]: if vehicle_info[VEHICLE_HAS_SAFETY_SERVICE]:
sensors_to_add.extend(SAFETY_SENSORS) sensor_descriptions_to_add.extend(SAFETY_SENSORS)
if vehicle_info[VEHICLE_API_GEN] == API_GEN_2: if vehicle_info[VEHICLE_API_GEN] == API_GEN_2:
sensors_to_add.extend(API_GEN_2_SENSORS) sensor_descriptions_to_add.extend(API_GEN_2_SENSORS)
if vehicle_info[VEHICLE_HAS_EV]: if vehicle_info[VEHICLE_HAS_EV]:
sensors_to_add.extend(EV_SENSORS) sensor_descriptions_to_add.extend(EV_SENSORS)
return [ return [
SubaruSensor( SubaruSensor(
vehicle_info, vehicle_info,
coordinator, coordinator,
s[SENSOR_TYPE], description,
s[SENSOR_CLASS],
s[SENSOR_FIELD],
s[SENSOR_UNITS],
) )
for s in sensors_to_add for description in sensor_descriptions_to_add
] ]
class SubaruSensor(SubaruEntity, SensorEntity): class SubaruSensor(
CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]], SensorEntity
):
"""Class for Subaru sensors.""" """Class for Subaru sensors."""
_attr_has_entity_name = True
def __init__( def __init__(
self, vehicle_info, coordinator, entity_type, sensor_class, data_field, api_unit self,
): vehicle_info: dict,
coordinator: DataUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(vehicle_info, coordinator) super().__init__(coordinator)
self.hass_type = "sensor" self.vin = vehicle_info[VEHICLE_VIN]
self.current_value = None self.entity_description = description
self.entity_type = entity_type self._attr_device_info = get_device_info(vehicle_info)
self.sensor_class = sensor_class self._attr_unique_id = f"{self.vin}_{description.name}"
self.data_field = data_field
self.api_unit = api_unit
@property @property
def device_class(self): def native_value(self) -> None | int | float:
"""Return the class of this device, from component DEVICE_CLASSES."""
if self.sensor_class in DEVICE_CLASSES:
return self.sensor_class
return None
@property
def icon(self):
"""Return the icon of the sensor."""
if not self.device_class:
return ICONS.get(self.entity_type)
return None
@property
def native_value(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
self.current_value = self.get_current_value() vehicle_data = self.coordinator.data[self.vin]
current_value = vehicle_data[VEHICLE_STATUS].get(self.entity_description.key)
unit = self.entity_description.native_unit_of_measurement
unit_system = self.hass.config.units
if self.current_value is None: if current_value is None:
return None return None
if self.api_unit in TEMPERATURE_UNITS: if unit in LENGTH_UNITS:
return round( return round(unit_system.length(current_value, unit), 1)
self.hass.config.units.temperature(self.current_value, self.api_unit), 1
)
if self.api_unit in LENGTH_UNITS: if unit in PRESSURE_UNITS and unit_system == IMPERIAL_SYSTEM:
return round( return round(
self.hass.config.units.length(self.current_value, self.api_unit), 1 unit_system.pressure(current_value, unit),
)
if (
self.api_unit in PRESSURE_UNITS
and self.hass.config.units == IMPERIAL_SYSTEM
):
return round(
self.hass.config.units.pressure(self.current_value, self.api_unit),
1, 1,
) )
if ( if (
self.api_unit in FUEL_CONSUMPTION_UNITS unit
and self.hass.config.units == IMPERIAL_SYSTEM in [
FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
FUEL_CONSUMPTION_MILES_PER_GALLON,
]
and unit_system == IMPERIAL_SYSTEM
): ):
return round((100.0 * L_PER_GAL) / (KM_PER_MI * self.current_value), 1) return round((100.0 * L_PER_GAL) / (KM_PER_MI * current_value), 1)
return self.current_value return current_value
@property @property
def native_unit_of_measurement(self): def native_unit_of_measurement(self) -> str | None:
"""Return the unit_of_measurement of the device.""" """Return the unit_of_measurement of the device."""
if self.api_unit in TEMPERATURE_UNITS: unit = self.entity_description.native_unit_of_measurement
return self.hass.config.units.temperature_unit
if self.api_unit in LENGTH_UNITS: if unit in LENGTH_UNITS:
return self.hass.config.units.length_unit return self.hass.config.units.length_unit
if self.api_unit in PRESSURE_UNITS: if unit in PRESSURE_UNITS:
if self.hass.config.units == IMPERIAL_SYSTEM: if self.hass.config.units == IMPERIAL_SYSTEM:
return self.hass.config.units.pressure_unit return self.hass.config.units.pressure_unit
return PRESSURE_HPA
if self.api_unit in FUEL_CONSUMPTION_UNITS: if unit in [
FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
FUEL_CONSUMPTION_MILES_PER_GALLON,
]:
if self.hass.config.units == IMPERIAL_SYSTEM: if self.hass.config.units == IMPERIAL_SYSTEM:
return FUEL_CONSUMPTION_MPG return FUEL_CONSUMPTION_MILES_PER_GALLON
return FUEL_CONSUMPTION_L_PER_100KM
return self.api_unit return unit
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -270,15 +269,3 @@ class SubaruSensor(SubaruEntity, SensorEntity):
if last_update_success and self.vin not in self.coordinator.data: if last_update_success and self.vin not in self.coordinator.data:
return False return False
return last_update_success return last_update_success
def get_current_value(self):
"""Get raw value from the coordinator."""
value = self.coordinator.data[self.vin][VEHICLE_STATUS].get(self.data_field)
if value in sc.BAD_SENSOR_VALUES:
value = None
if isinstance(value, str):
if "." in value:
value = float(value)
else:
value = int(value)
return value

View File

@ -2332,7 +2332,7 @@ streamlabswater==1.0.1
stringcase==1.2.0 stringcase==1.2.0
# homeassistant.components.subaru # homeassistant.components.subaru
subarulink==0.5.0 subarulink==0.6.0
# homeassistant.components.solarlog # homeassistant.components.solarlog
sunwatcher==0.2.1 sunwatcher==0.2.1

View File

@ -1611,7 +1611,7 @@ stookalert==0.1.4
stringcase==1.2.0 stringcase==1.2.0
# homeassistant.components.subaru # homeassistant.components.subaru
subarulink==0.5.0 subarulink==0.6.0
# homeassistant.components.solarlog # homeassistant.components.solarlog
sunwatcher==0.2.1 sunwatcher==0.2.1

View File

@ -1,5 +1,7 @@
"""Sample API response data for tests.""" """Sample API response data for tests."""
from datetime import datetime, timezone
from homeassistant.components.subaru.const import ( from homeassistant.components.subaru.const import (
API_GEN_1, API_GEN_1,
API_GEN_2, API_GEN_2,
@ -46,10 +48,12 @@ VEHICLE_DATA = {
}, },
} }
MOCK_DATETIME = datetime.fromtimestamp(1595560000, timezone.utc)
VEHICLE_STATUS_EV = { VEHICLE_STATUS_EV = {
"status": { "status": {
"AVG_FUEL_CONSUMPTION": 2.3, "AVG_FUEL_CONSUMPTION": 2.3,
"BATTERY_VOLTAGE": "12.0", "BATTERY_VOLTAGE": 12.0,
"DISTANCE_TO_EMPTY_FUEL": 707, "DISTANCE_TO_EMPTY_FUEL": 707,
"DOOR_BOOT_LOCK_STATUS": "UNKNOWN", "DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
"DOOR_BOOT_POSITION": "CLOSED", "DOOR_BOOT_POSITION": "CLOSED",
@ -63,21 +67,17 @@ VEHICLE_STATUS_EV = {
"DOOR_REAR_LEFT_POSITION": "CLOSED", "DOOR_REAR_LEFT_POSITION": "CLOSED",
"DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN", "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
"DOOR_REAR_RIGHT_POSITION": "CLOSED", "DOOR_REAR_RIGHT_POSITION": "CLOSED",
"EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED", "EV_CHARGER_STATE_TYPE": "CHARGING",
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
"EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
"EV_DISTANCE_TO_EMPTY": 17, "EV_DISTANCE_TO_EMPTY": 1,
"EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED",
"EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_MODE": "EV_MODE",
"EV_STATE_OF_CHARGE_PERCENT": "100", "EV_STATE_OF_CHARGE_PERCENT": 20,
"EV_TIME_TO_FULLY_CHARGED": "65535", "EV_TIME_TO_FULLY_CHARGED_UTC": MOCK_DATETIME,
"EV_VEHICLE_TIME_DAYOFWEEK": "6", "EXT_EXTERNAL_TEMP": 21.5,
"EV_VEHICLE_TIME_HOUR": "14",
"EV_VEHICLE_TIME_MINUTE": "20",
"EV_VEHICLE_TIME_SECOND": "39",
"EXT_EXTERNAL_TEMP": "21.5",
"ODOMETER": 1234, "ODOMETER": 1234,
"POSITION_HEADING_DEGREE": "150", "POSITION_HEADING_DEGREE": 150,
"POSITION_SPEED_KMPH": "0", "POSITION_SPEED_KMPH": "0",
"POSITION_TIMESTAMP": 1595560000.0, "POSITION_TIMESTAMP": 1595560000.0,
"SEAT_BELT_STATUS_FRONT_LEFT": "BELTED", "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED",
@ -100,7 +100,7 @@ VEHICLE_STATUS_EV = {
"SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN", "SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN",
"TIMESTAMP": 1595560000.0, "TIMESTAMP": 1595560000.0,
"TRANSMISSION_MODE": "UNKNOWN", "TRANSMISSION_MODE": "UNKNOWN",
"TYRE_PRESSURE_FRONT_LEFT": 2550, "TYRE_PRESSURE_FRONT_LEFT": 0,
"TYRE_PRESSURE_FRONT_RIGHT": 2550, "TYRE_PRESSURE_FRONT_RIGHT": 2550,
"TYRE_PRESSURE_REAR_LEFT": 2450, "TYRE_PRESSURE_REAR_LEFT": 2450,
"TYRE_PRESSURE_REAR_RIGHT": 2350, "TYRE_PRESSURE_REAR_RIGHT": 2350,
@ -121,10 +121,11 @@ VEHICLE_STATUS_EV = {
} }
} }
VEHICLE_STATUS_G2 = { VEHICLE_STATUS_G2 = {
"status": { "status": {
"AVG_FUEL_CONSUMPTION": 2.3, "AVG_FUEL_CONSUMPTION": 2.3,
"BATTERY_VOLTAGE": "12.0", "BATTERY_VOLTAGE": 12.0,
"DISTANCE_TO_EMPTY_FUEL": 707, "DISTANCE_TO_EMPTY_FUEL": 707,
"DOOR_BOOT_LOCK_STATUS": "UNKNOWN", "DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
"DOOR_BOOT_POSITION": "CLOSED", "DOOR_BOOT_POSITION": "CLOSED",
@ -138,9 +139,9 @@ VEHICLE_STATUS_G2 = {
"DOOR_REAR_LEFT_POSITION": "CLOSED", "DOOR_REAR_LEFT_POSITION": "CLOSED",
"DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN", "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
"DOOR_REAR_RIGHT_POSITION": "CLOSED", "DOOR_REAR_RIGHT_POSITION": "CLOSED",
"EXT_EXTERNAL_TEMP": "21.5", "EXT_EXTERNAL_TEMP": None,
"ODOMETER": 1234, "ODOMETER": 1234,
"POSITION_HEADING_DEGREE": "150", "POSITION_HEADING_DEGREE": 150,
"POSITION_SPEED_KMPH": "0", "POSITION_SPEED_KMPH": "0",
"POSITION_TIMESTAMP": 1595560000.0, "POSITION_TIMESTAMP": 1595560000.0,
"SEAT_BELT_STATUS_FRONT_LEFT": "BELTED", "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED",
@ -188,18 +189,14 @@ EXPECTED_STATE_EV_IMPERIAL = {
"AVG_FUEL_CONSUMPTION": "102.3", "AVG_FUEL_CONSUMPTION": "102.3",
"BATTERY_VOLTAGE": "12.0", "BATTERY_VOLTAGE": "12.0",
"DISTANCE_TO_EMPTY_FUEL": "439.3", "DISTANCE_TO_EMPTY_FUEL": "439.3",
"EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED", "EV_CHARGER_STATE_TYPE": "CHARGING",
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
"EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
"EV_DISTANCE_TO_EMPTY": "17", "EV_DISTANCE_TO_EMPTY": "1",
"EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED",
"EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_MODE": "EV_MODE",
"EV_STATE_OF_CHARGE_PERCENT": "100", "EV_STATE_OF_CHARGE_PERCENT": "20",
"EV_TIME_TO_FULLY_CHARGED": "unknown", "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00",
"EV_VEHICLE_TIME_DAYOFWEEK": "6",
"EV_VEHICLE_TIME_HOUR": "14",
"EV_VEHICLE_TIME_MINUTE": "20",
"EV_VEHICLE_TIME_SECOND": "39",
"EXT_EXTERNAL_TEMP": "70.7", "EXT_EXTERNAL_TEMP": "70.7",
"ODOMETER": "766.8", "ODOMETER": "766.8",
"POSITION_HEADING_DEGREE": "150", "POSITION_HEADING_DEGREE": "150",
@ -207,7 +204,7 @@ EXPECTED_STATE_EV_IMPERIAL = {
"POSITION_TIMESTAMP": 1595560000.0, "POSITION_TIMESTAMP": 1595560000.0,
"TIMESTAMP": 1595560000.0, "TIMESTAMP": 1595560000.0,
"TRANSMISSION_MODE": "UNKNOWN", "TRANSMISSION_MODE": "UNKNOWN",
"TYRE_PRESSURE_FRONT_LEFT": "37.0", "TYRE_PRESSURE_FRONT_LEFT": "0.0",
"TYRE_PRESSURE_FRONT_RIGHT": "37.0", "TYRE_PRESSURE_FRONT_RIGHT": "37.0",
"TYRE_PRESSURE_REAR_LEFT": "35.5", "TYRE_PRESSURE_REAR_LEFT": "35.5",
"TYRE_PRESSURE_REAR_RIGHT": "34.1", "TYRE_PRESSURE_REAR_RIGHT": "34.1",
@ -221,18 +218,14 @@ EXPECTED_STATE_EV_METRIC = {
"AVG_FUEL_CONSUMPTION": "2.3", "AVG_FUEL_CONSUMPTION": "2.3",
"BATTERY_VOLTAGE": "12.0", "BATTERY_VOLTAGE": "12.0",
"DISTANCE_TO_EMPTY_FUEL": "707", "DISTANCE_TO_EMPTY_FUEL": "707",
"EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED", "EV_CHARGER_STATE_TYPE": "CHARGING",
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
"EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
"EV_DISTANCE_TO_EMPTY": "27.4", "EV_DISTANCE_TO_EMPTY": "1.6",
"EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED",
"EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_MODE": "EV_MODE",
"EV_STATE_OF_CHARGE_PERCENT": "100", "EV_STATE_OF_CHARGE_PERCENT": "20",
"EV_TIME_TO_FULLY_CHARGED": "unknown", "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00",
"EV_VEHICLE_TIME_DAYOFWEEK": "6",
"EV_VEHICLE_TIME_HOUR": "14",
"EV_VEHICLE_TIME_MINUTE": "20",
"EV_VEHICLE_TIME_SECOND": "39",
"EXT_EXTERNAL_TEMP": "21.5", "EXT_EXTERNAL_TEMP": "21.5",
"ODOMETER": "1234", "ODOMETER": "1234",
"POSITION_HEADING_DEGREE": "150", "POSITION_HEADING_DEGREE": "150",
@ -240,7 +233,7 @@ EXPECTED_STATE_EV_METRIC = {
"POSITION_TIMESTAMP": 1595560000.0, "POSITION_TIMESTAMP": 1595560000.0,
"TIMESTAMP": 1595560000.0, "TIMESTAMP": 1595560000.0,
"TRANSMISSION_MODE": "UNKNOWN", "TRANSMISSION_MODE": "UNKNOWN",
"TYRE_PRESSURE_FRONT_LEFT": "2550", "TYRE_PRESSURE_FRONT_LEFT": "0",
"TYRE_PRESSURE_FRONT_RIGHT": "2550", "TYRE_PRESSURE_FRONT_RIGHT": "2550",
"TYRE_PRESSURE_REAR_LEFT": "2450", "TYRE_PRESSURE_REAR_LEFT": "2450",
"TYRE_PRESSURE_REAR_RIGHT": "2350", "TYRE_PRESSURE_REAR_RIGHT": "2350",
@ -250,6 +243,7 @@ EXPECTED_STATE_EV_METRIC = {
"longitude": -100.0, "longitude": -100.0,
} }
EXPECTED_STATE_EV_UNAVAILABLE = { EXPECTED_STATE_EV_UNAVAILABLE = {
"AVG_FUEL_CONSUMPTION": "unavailable", "AVG_FUEL_CONSUMPTION": "unavailable",
"BATTERY_VOLTAGE": "unavailable", "BATTERY_VOLTAGE": "unavailable",
@ -261,11 +255,7 @@ EXPECTED_STATE_EV_UNAVAILABLE = {
"EV_IS_PLUGGED_IN": "unavailable", "EV_IS_PLUGGED_IN": "unavailable",
"EV_STATE_OF_CHARGE_MODE": "unavailable", "EV_STATE_OF_CHARGE_MODE": "unavailable",
"EV_STATE_OF_CHARGE_PERCENT": "unavailable", "EV_STATE_OF_CHARGE_PERCENT": "unavailable",
"EV_TIME_TO_FULLY_CHARGED": "unavailable", "EV_TIME_TO_FULLY_CHARGED_UTC": "unavailable",
"EV_VEHICLE_TIME_DAYOFWEEK": "unavailable",
"EV_VEHICLE_TIME_HOUR": "unavailable",
"EV_VEHICLE_TIME_MINUTE": "unavailable",
"EV_VEHICLE_TIME_SECOND": "unavailable",
"EXT_EXTERNAL_TEMP": "unavailable", "EXT_EXTERNAL_TEMP": "unavailable",
"ODOMETER": "unavailable", "ODOMETER": "unavailable",
"POSITION_HEADING_DEGREE": "unavailable", "POSITION_HEADING_DEGREE": "unavailable",

View File

@ -71,7 +71,8 @@ TEST_OPTIONS = {
CONF_UPDATE_ENABLED: True, CONF_UPDATE_ENABLED: True,
} }
TEST_ENTITY_ID = "sensor.test_vehicle_2_odometer" TEST_DEVICE_NAME = "test_vehicle_2"
TEST_ENTITY_ID = f"sensor.{TEST_DEVICE_NAME}_odometer"
def advance_time_to_next_fetch(hass): def advance_time_to_next_fetch(hass):

View File

@ -1,13 +1,10 @@
"""Test Subaru sensors.""" """Test Subaru sensors."""
from unittest.mock import patch from unittest.mock import patch
from homeassistant.components.subaru.const import VEHICLE_NAME
from homeassistant.components.subaru.sensor import ( from homeassistant.components.subaru.sensor import (
API_GEN_2_SENSORS, API_GEN_2_SENSORS,
EV_SENSORS, EV_SENSORS,
SAFETY_SENSORS, SAFETY_SENSORS,
SENSOR_FIELD,
SENSOR_TYPE,
) )
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.util.unit_system import IMPERIAL_SYSTEM from homeassistant.util.unit_system import IMPERIAL_SYSTEM
@ -16,13 +13,14 @@ from .api_responses import (
EXPECTED_STATE_EV_IMPERIAL, EXPECTED_STATE_EV_IMPERIAL,
EXPECTED_STATE_EV_METRIC, EXPECTED_STATE_EV_METRIC,
EXPECTED_STATE_EV_UNAVAILABLE, EXPECTED_STATE_EV_UNAVAILABLE,
TEST_VIN_2_EV,
VEHICLE_DATA,
VEHICLE_STATUS_EV, VEHICLE_STATUS_EV,
) )
from .conftest import MOCK_API_FETCH, MOCK_API_GET_DATA, advance_time_to_next_fetch from .conftest import (
MOCK_API_FETCH,
VEHICLE_NAME = VEHICLE_DATA[TEST_VIN_2_EV][VEHICLE_NAME] MOCK_API_GET_DATA,
TEST_DEVICE_NAME,
advance_time_to_next_fetch,
)
async def test_sensors_ev_imperial(hass, ev_entry): async def test_sensors_ev_imperial(hass, ev_entry):
@ -59,9 +57,9 @@ def _assert_data(hass, expected_state):
expected_states = {} expected_states = {}
for item in sensor_list: for item in sensor_list:
expected_states[ expected_states[
f"sensor.{slugify(f'{VEHICLE_NAME} {item[SENSOR_TYPE]}')}" f"sensor.{slugify(f'{TEST_DEVICE_NAME} {item.name}')}"
] = expected_state[item[SENSOR_FIELD]] ] = expected_state[item.key]
for sensor in expected_states: for sensor, value in expected_states.items():
actual = hass.states.get(sensor) actual = hass.states.get(sensor)
assert actual.state == expected_states[sensor] assert actual.state == value