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
import logging
from typing import Any
from subarulink import (
Controller as SubaruAPI,
@ -16,6 +17,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv
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.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."""
error = None
@ -117,7 +121,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.debug("Successfully authenticated with Subaru API")
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."""
error = None
if user_input:
@ -143,7 +149,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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."""
error = None
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
)
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."""
error = None
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."""
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."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

View file

@ -31,7 +31,7 @@ VEHICLE_STATUS = "status"
API_GEN_1 = "g1"
API_GEN_2 = "g2"
MANUFACTURER = "Subaru Corp."
MANUFACTURER = "Subaru"
PLATFORMS = [
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",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/subaru",
"requirements": ["subarulink==0.5.0"],
"requirements": ["subarulink==0.6.0"],
"codeowners": ["@G-Two"],
"iot_class": "cloud_polling",
"loggers": ["stdiomask", "subarulink"]

View file

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

View file

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

View file

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

View file

@ -71,7 +71,8 @@ TEST_OPTIONS = {
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):

View file

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