mirror of https://github.com/home-assistant/core
Add new Subaru integration (#35760)
Co-authored-by: On Freund <onfreund@gmail.com> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
efa339ca54
commit
3ad207a499
|
@ -450,6 +450,7 @@ homeassistant/components/stiebel_eltron/* @fucm
|
|||
homeassistant/components/stookalert/* @fwestenberg
|
||||
homeassistant/components/stream/* @hunterjm @uvjustin
|
||||
homeassistant/components/stt/* @pvizeli
|
||||
homeassistant/components/subaru/* @G-Two
|
||||
homeassistant/components/suez_water/* @ooii
|
||||
homeassistant/components/sun/* @Swamp-Ig
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
"""The Subaru integration."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import time
|
||||
|
||||
from subarulink import Controller as SubaruAPI, InvalidCredentials, SubaruException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
CONF_COUNTRY,
|
||||
CONF_UPDATE_ENABLED,
|
||||
COORDINATOR_NAME,
|
||||
DOMAIN,
|
||||
ENTRY_CONTROLLER,
|
||||
ENTRY_COORDINATOR,
|
||||
ENTRY_VEHICLES,
|
||||
FETCH_INTERVAL,
|
||||
SUPPORTED_PLATFORMS,
|
||||
UPDATE_INTERVAL,
|
||||
VEHICLE_API_GEN,
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_HAS_REMOTE_SERVICE,
|
||||
VEHICLE_HAS_REMOTE_START,
|
||||
VEHICLE_HAS_SAFETY_SERVICE,
|
||||
VEHICLE_LAST_UPDATE,
|
||||
VEHICLE_NAME,
|
||||
VEHICLE_VIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass, base_config):
|
||||
"""Do nothing since this integration does not support configuration.yml setup."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up Subaru from a config entry."""
|
||||
config = entry.data
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
try:
|
||||
controller = SubaruAPI(
|
||||
websession,
|
||||
config[CONF_USERNAME],
|
||||
config[CONF_PASSWORD],
|
||||
config[CONF_DEVICE_ID],
|
||||
config[CONF_PIN],
|
||||
None,
|
||||
config[CONF_COUNTRY],
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
fetch_interval=FETCH_INTERVAL,
|
||||
)
|
||||
_LOGGER.debug("Using subarulink %s", controller.version)
|
||||
await controller.connect()
|
||||
except InvalidCredentials:
|
||||
_LOGGER.error("Invalid account")
|
||||
return False
|
||||
except SubaruException as err:
|
||||
raise ConfigEntryNotReady(err.message) from err
|
||||
|
||||
vehicle_info = {}
|
||||
for vin in controller.get_vehicles():
|
||||
vehicle_info[vin] = get_vehicle_info(controller, vin)
|
||||
|
||||
async def async_update_data():
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
return await refresh_subaru_data(entry, vehicle_info, controller)
|
||||
except SubaruException as err:
|
||||
raise UpdateFailed(err.message) from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=COORDINATOR_NAME,
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=FETCH_INTERVAL),
|
||||
)
|
||||
|
||||
await coordinator.async_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
ENTRY_CONTROLLER: controller,
|
||||
ENTRY_COORDINATOR: coordinator,
|
||||
ENTRY_VEHICLES: vehicle_info,
|
||||
}
|
||||
|
||||
for component in SUPPORTED_PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in SUPPORTED_PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def refresh_subaru_data(config_entry, vehicle_info, controller):
|
||||
"""
|
||||
Refresh local data with data fetched via Subaru API.
|
||||
|
||||
Subaru API calls assume a server side vehicle context
|
||||
Data fetch/update must be done for each vehicle
|
||||
"""
|
||||
data = {}
|
||||
|
||||
for vehicle in vehicle_info.values():
|
||||
vin = vehicle[VEHICLE_VIN]
|
||||
|
||||
# Active subscription required
|
||||
if not vehicle[VEHICLE_HAS_SAFETY_SERVICE]:
|
||||
continue
|
||||
|
||||
# Optionally send an "update" remote command to vehicle (throttled with update_interval)
|
||||
if config_entry.options.get(CONF_UPDATE_ENABLED, False):
|
||||
await update_subaru(vehicle, controller)
|
||||
|
||||
# Fetch data from Subaru servers
|
||||
await controller.fetch(vin, force=True)
|
||||
|
||||
# Update our local data that will go to entity states
|
||||
received_data = await controller.get_data(vin)
|
||||
if received_data:
|
||||
data[vin] = received_data
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def update_subaru(vehicle, controller):
|
||||
"""Commands remote vehicle update (polls the vehicle to update subaru API cache)."""
|
||||
cur_time = time.time()
|
||||
last_update = vehicle[VEHICLE_LAST_UPDATE]
|
||||
|
||||
if cur_time - last_update > controller.get_update_interval():
|
||||
await controller.update(vehicle[VEHICLE_VIN], force=True)
|
||||
vehicle[VEHICLE_LAST_UPDATE] = cur_time
|
||||
|
||||
|
||||
def get_vehicle_info(controller, vin):
|
||||
"""Obtain vehicle identifiers and capabilities."""
|
||||
info = {
|
||||
VEHICLE_VIN: vin,
|
||||
VEHICLE_NAME: controller.vin_to_name(vin),
|
||||
VEHICLE_HAS_EV: controller.get_ev_status(vin),
|
||||
VEHICLE_API_GEN: controller.get_api_gen(vin),
|
||||
VEHICLE_HAS_REMOTE_START: controller.get_res_status(vin),
|
||||
VEHICLE_HAS_REMOTE_SERVICE: controller.get_remote_status(vin),
|
||||
VEHICLE_HAS_SAFETY_SERVICE: controller.get_safety_status(vin),
|
||||
VEHICLE_LAST_UPDATE: 0,
|
||||
}
|
||||
return info
|
|
@ -0,0 +1,157 @@
|
|||
"""Config flow for Subaru integration."""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from subarulink import (
|
||||
Controller as SubaruAPI,
|
||||
InvalidCredentials,
|
||||
InvalidPIN,
|
||||
SubaruException,
|
||||
)
|
||||
from subarulink.const import COUNTRY_CAN, COUNTRY_USA
|
||||
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.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str})
|
||||
|
||||
|
||||
class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Subaru."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
config_data = {CONF_PIN: None}
|
||||
controller = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the start of the config flow."""
|
||||
error = None
|
||||
|
||||
if user_input:
|
||||
if user_input[CONF_USERNAME] in [
|
||||
entry.data[CONF_USERNAME] for entry in self._async_current_entries()
|
||||
]:
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
try:
|
||||
await self.validate_login_creds(user_input)
|
||||
except InvalidCredentials:
|
||||
error = {"base": "invalid_auth"}
|
||||
except SubaruException as ex:
|
||||
_LOGGER.error("Unable to communicate with Subaru API: %s", ex.message)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
else:
|
||||
if self.controller.is_pin_required():
|
||||
return await self.async_step_pin()
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME], data=self.config_data
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_USERNAME,
|
||||
default=user_input.get(CONF_USERNAME) if user_input else "",
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_PASSWORD,
|
||||
default=user_input.get(CONF_PASSWORD) if user_input else "",
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_COUNTRY,
|
||||
default=user_input.get(CONF_COUNTRY)
|
||||
if user_input
|
||||
else COUNTRY_USA,
|
||||
): vol.In([COUNTRY_CAN, COUNTRY_USA]),
|
||||
}
|
||||
),
|
||||
errors=error,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
async def validate_login_creds(self, data):
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
data: contains values provided by the user.
|
||||
"""
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
now = datetime.now()
|
||||
if not data.get(CONF_DEVICE_ID):
|
||||
data[CONF_DEVICE_ID] = int(now.timestamp())
|
||||
date = now.strftime("%Y-%m-%d")
|
||||
device_name = "Home Assistant: Added " + date
|
||||
|
||||
self.controller = SubaruAPI(
|
||||
websession,
|
||||
username=data[CONF_USERNAME],
|
||||
password=data[CONF_PASSWORD],
|
||||
device_id=data[CONF_DEVICE_ID],
|
||||
pin=None,
|
||||
device_name=device_name,
|
||||
country=data[CONF_COUNTRY],
|
||||
)
|
||||
_LOGGER.debug("Using subarulink %s", self.controller.version)
|
||||
_LOGGER.debug(
|
||||
"Setting up first time connection to Subuaru API. This may take up to 20 seconds."
|
||||
)
|
||||
if await self.controller.connect():
|
||||
_LOGGER.debug("Successfully authenticated and authorized with Subaru API")
|
||||
self.config_data.update(data)
|
||||
|
||||
async def async_step_pin(self, user_input=None):
|
||||
"""Handle second part of config flow, if required."""
|
||||
error = None
|
||||
if user_input:
|
||||
if self.controller.update_saved_pin(user_input[CONF_PIN]):
|
||||
try:
|
||||
vol.Match(r"[0-9]{4}")(user_input[CONF_PIN])
|
||||
await self.controller.test_pin()
|
||||
except vol.Invalid:
|
||||
error = {"base": "bad_pin_format"}
|
||||
except InvalidPIN:
|
||||
error = {"base": "incorrect_pin"}
|
||||
else:
|
||||
_LOGGER.debug("PIN successfully tested")
|
||||
self.config_data.update(user_input)
|
||||
return self.async_create_entry(
|
||||
title=self.config_data[CONF_USERNAME], data=self.config_data
|
||||
)
|
||||
return self.async_show_form(step_id="pin", data_schema=PIN_SCHEMA, errors=error)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle a option flow for Subaru."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle options flow."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_UPDATE_ENABLED,
|
||||
default=self.config_entry.options.get(CONF_UPDATE_ENABLED, False),
|
||||
): cv.boolean,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="init", data_schema=data_schema)
|
|
@ -0,0 +1,47 @@
|
|||
"""Constants for the Subaru integration."""
|
||||
|
||||
DOMAIN = "subaru"
|
||||
FETCH_INTERVAL = 300
|
||||
UPDATE_INTERVAL = 7200
|
||||
CONF_UPDATE_ENABLED = "update_enabled"
|
||||
CONF_COUNTRY = "country"
|
||||
|
||||
# entry fields
|
||||
ENTRY_CONTROLLER = "controller"
|
||||
ENTRY_COORDINATOR = "coordinator"
|
||||
ENTRY_VEHICLES = "vehicles"
|
||||
|
||||
# update coordinator name
|
||||
COORDINATOR_NAME = "subaru_data"
|
||||
|
||||
# info fields
|
||||
VEHICLE_VIN = "vin"
|
||||
VEHICLE_NAME = "display_name"
|
||||
VEHICLE_HAS_EV = "is_ev"
|
||||
VEHICLE_API_GEN = "api_gen"
|
||||
VEHICLE_HAS_REMOTE_START = "has_res"
|
||||
VEHICLE_HAS_REMOTE_SERVICE = "has_remote"
|
||||
VEHICLE_HAS_SAFETY_SERVICE = "has_safety"
|
||||
VEHICLE_LAST_UPDATE = "last_update"
|
||||
VEHICLE_STATUS = "status"
|
||||
|
||||
|
||||
API_GEN_1 = "g1"
|
||||
API_GEN_2 = "g2"
|
||||
MANUFACTURER = "Subaru Corp."
|
||||
|
||||
SUPPORTED_PLATFORMS = [
|
||||
"sensor",
|
||||
]
|
||||
|
||||
ICONS = {
|
||||
"Avg Fuel Consumption": "mdi:leaf",
|
||||
"EV Time to Full Charge": "mdi:car-electric",
|
||||
"EV Range": "mdi:ev-station",
|
||||
"Odometer": "mdi:road-variant",
|
||||
"Range": "mdi:gas-station",
|
||||
"Tire Pressure FL": "mdi:gauge",
|
||||
"Tire Pressure FR": "mdi:gauge",
|
||||
"Tire Pressure RL": "mdi:gauge",
|
||||
"Tire Pressure RR": "mdi:gauge",
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
"""Base class for all Subaru Entities."""
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, ICONS, 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 icon(self):
|
||||
"""Return the icon of the sensor."""
|
||||
return ICONS.get(self.entity_type)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device_info of the device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.vin)},
|
||||
"name": self.car_name,
|
||||
"manufacturer": MANUFACTURER,
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"domain": "subaru",
|
||||
"name": "Subaru",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/subaru",
|
||||
"requirements": ["subarulink==0.3.12"],
|
||||
"codeowners": ["@G-Two"]
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
"""Support for Subaru sensors."""
|
||||
import subarulink.const as sc
|
||||
|
||||
from homeassistant.components.sensor import DEVICE_CLASSES
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
LENGTH_KILOMETERS,
|
||||
LENGTH_MILES,
|
||||
PERCENTAGE,
|
||||
PRESSURE_HPA,
|
||||
TEMP_CELSIUS,
|
||||
TIME_MINUTES,
|
||||
VOLT,
|
||||
VOLUME_GALLONS,
|
||||
VOLUME_LITERS,
|
||||
)
|
||||
from homeassistant.util.distance import convert as dist_convert
|
||||
from homeassistant.util.unit_system import (
|
||||
IMPERIAL_SYSTEM,
|
||||
LENGTH_UNITS,
|
||||
PRESSURE_UNITS,
|
||||
TEMPERATURE_UNITS,
|
||||
)
|
||||
from homeassistant.util.volume import convert as vol_convert
|
||||
|
||||
from .const import (
|
||||
API_GEN_2,
|
||||
DOMAIN,
|
||||
ENTRY_COORDINATOR,
|
||||
ENTRY_VEHICLES,
|
||||
VEHICLE_API_GEN,
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_HAS_SAFETY_SERVICE,
|
||||
VEHICLE_STATUS,
|
||||
)
|
||||
from .entity import SubaruEntity
|
||||
|
||||
L_PER_GAL = vol_convert(1, VOLUME_GALLONS, VOLUME_LITERS)
|
||||
KM_PER_MI = dist_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
|
||||
SAFETY_SENSORS = [
|
||||
{
|
||||
SENSOR_TYPE: "Odometer",
|
||||
SENSOR_CLASS: None,
|
||||
SENSOR_FIELD: sc.ODOMETER,
|
||||
SENSOR_UNITS: LENGTH_KILOMETERS,
|
||||
},
|
||||
]
|
||||
|
||||
# Sensor data 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: None,
|
||||
SENSOR_FIELD: sc.TIRE_PRESSURE_FL,
|
||||
SENSOR_UNITS: PRESSURE_HPA,
|
||||
},
|
||||
{
|
||||
SENSOR_TYPE: "Tire Pressure FR",
|
||||
SENSOR_CLASS: None,
|
||||
SENSOR_FIELD: sc.TIRE_PRESSURE_FR,
|
||||
SENSOR_UNITS: PRESSURE_HPA,
|
||||
},
|
||||
{
|
||||
SENSOR_TYPE: "Tire Pressure RL",
|
||||
SENSOR_CLASS: None,
|
||||
SENSOR_FIELD: sc.TIRE_PRESSURE_RL,
|
||||
SENSOR_UNITS: PRESSURE_HPA,
|
||||
},
|
||||
{
|
||||
SENSOR_TYPE: "Tire Pressure RR",
|
||||
SENSOR_CLASS: None,
|
||||
SENSOR_FIELD: sc.TIRE_PRESSURE_RR,
|
||||
SENSOR_UNITS: PRESSURE_HPA,
|
||||
},
|
||||
{
|
||||
SENSOR_TYPE: "External Temp",
|
||||
SENSOR_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
SENSOR_FIELD: sc.EXTERNAL_TEMP,
|
||||
SENSOR_UNITS: TEMP_CELSIUS,
|
||||
},
|
||||
{
|
||||
SENSOR_TYPE: "12V Battery Voltage",
|
||||
SENSOR_CLASS: DEVICE_CLASS_VOLTAGE,
|
||||
SENSOR_FIELD: sc.BATTERY_VOLTAGE,
|
||||
SENSOR_UNITS: VOLT,
|
||||
},
|
||||
]
|
||||
|
||||
# Sensor data 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: DEVICE_CLASS_BATTERY,
|
||||
SENSOR_FIELD: sc.EV_STATE_OF_CHARGE_PERCENT,
|
||||
SENSOR_UNITS: PERCENTAGE,
|
||||
},
|
||||
{
|
||||
SENSOR_TYPE: "EV Time to Full Charge",
|
||||
SENSOR_CLASS: None,
|
||||
SENSOR_FIELD: sc.EV_TIME_TO_FULLY_CHARGED,
|
||||
SENSOR_UNITS: TIME_MINUTES,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""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]
|
||||
entities = []
|
||||
for vin in vehicle_info.keys():
|
||||
entities.extend(create_vehicle_sensors(vehicle_info[vin], coordinator))
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
def create_vehicle_sensors(vehicle_info, coordinator):
|
||||
"""Instantiate all available sensors for the vehicle."""
|
||||
sensors_to_add = []
|
||||
if vehicle_info[VEHICLE_HAS_SAFETY_SERVICE]:
|
||||
sensors_to_add.extend(SAFETY_SENSORS)
|
||||
|
||||
if vehicle_info[VEHICLE_API_GEN] == API_GEN_2:
|
||||
sensors_to_add.extend(API_GEN_2_SENSORS)
|
||||
|
||||
if vehicle_info[VEHICLE_HAS_EV]:
|
||||
sensors_to_add.extend(EV_SENSORS)
|
||||
|
||||
return [
|
||||
SubaruSensor(
|
||||
vehicle_info,
|
||||
coordinator,
|
||||
s[SENSOR_TYPE],
|
||||
s[SENSOR_CLASS],
|
||||
s[SENSOR_FIELD],
|
||||
s[SENSOR_UNITS],
|
||||
)
|
||||
for s in sensors_to_add
|
||||
]
|
||||
|
||||
|
||||
class SubaruSensor(SubaruEntity):
|
||||
"""Class for Subaru sensors."""
|
||||
|
||||
def __init__(
|
||||
self, vehicle_info, coordinator, entity_type, sensor_class, data_field, api_unit
|
||||
):
|
||||
"""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
|
||||
|
||||
@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 super().device_class
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
self.current_value = self.get_current_value()
|
||||
|
||||
if self.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 self.api_unit in LENGTH_UNITS:
|
||||
return round(
|
||||
self.hass.config.units.length(self.current_value, self.api_unit), 1
|
||||
)
|
||||
|
||||
if self.api_unit in PRESSURE_UNITS:
|
||||
if self.hass.config.units == IMPERIAL_SYSTEM:
|
||||
return round(
|
||||
self.hass.config.units.pressure(self.current_value, self.api_unit),
|
||||
1,
|
||||
)
|
||||
|
||||
if self.api_unit in FUEL_CONSUMPTION_UNITS:
|
||||
if self.hass.config.units == IMPERIAL_SYSTEM:
|
||||
return round((100.0 * L_PER_GAL) / (KM_PER_MI * self.current_value), 1)
|
||||
|
||||
return self.current_value
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit_of_measurement of the device."""
|
||||
if self.api_unit in TEMPERATURE_UNITS:
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
if self.api_unit in LENGTH_UNITS:
|
||||
return self.hass.config.units.length_unit
|
||||
|
||||
if self.api_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 self.hass.config.units == IMPERIAL_SYSTEM:
|
||||
return FUEL_CONSUMPTION_MPG
|
||||
return FUEL_CONSUMPTION_L_PER_100KM
|
||||
|
||||
return self.api_unit
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if entity is available."""
|
||||
last_update_success = super().available
|
||||
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
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Subaru Starlink Configuration",
|
||||
"description": "Please enter your MySubaru credentials\nNOTE: Initial setup may take up to 30 seconds",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"country": "Select country"
|
||||
}
|
||||
},
|
||||
"pin": {
|
||||
"title": "Subaru Starlink Configuration",
|
||||
"description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN",
|
||||
"data": {
|
||||
"pin": "PIN"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"incorrect_pin": "Incorrect PIN",
|
||||
"bad_pin_format": "PIN should be 4 digits",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Subaru Starlink Options",
|
||||
"description": "When enabled, vehicle polling will send a remote command to your vehicle every 2 hours to obtain new sensor data. Without vehicle polling, new sensor data is only received when the vehicle automatically pushes data (normally after engine shutdown).",
|
||||
"data": {
|
||||
"update_enabled": "Enable vehicle polling"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -216,6 +216,7 @@ FLOWS = [
|
|||
"squeezebox",
|
||||
"srp_energy",
|
||||
"starline",
|
||||
"subaru",
|
||||
"syncthru",
|
||||
"synology_dsm",
|
||||
"tado",
|
||||
|
|
|
@ -2142,6 +2142,9 @@ streamlabswater==1.0.1
|
|||
# homeassistant.components.traccar
|
||||
stringcase==1.2.0
|
||||
|
||||
# homeassistant.components.subaru
|
||||
subarulink==0.3.12
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
sucks==0.9.4
|
||||
|
||||
|
|
|
@ -1105,6 +1105,9 @@ statsd==3.2.1
|
|||
# homeassistant.components.traccar
|
||||
stringcase==1.2.0
|
||||
|
||||
# homeassistant.components.subaru
|
||||
subarulink==0.3.12
|
||||
|
||||
# homeassistant.components.solarlog
|
||||
sunwatcher==0.2.1
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Subaru integration."""
|
|
@ -0,0 +1,284 @@
|
|||
"""Sample API response data for tests."""
|
||||
|
||||
from homeassistant.components.subaru.const import (
|
||||
API_GEN_1,
|
||||
API_GEN_2,
|
||||
VEHICLE_API_GEN,
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_HAS_REMOTE_SERVICE,
|
||||
VEHICLE_HAS_REMOTE_START,
|
||||
VEHICLE_HAS_SAFETY_SERVICE,
|
||||
VEHICLE_NAME,
|
||||
VEHICLE_VIN,
|
||||
)
|
||||
|
||||
TEST_VIN_1_G1 = "JF2ABCDE6L0000001"
|
||||
TEST_VIN_2_EV = "JF2ABCDE6L0000002"
|
||||
TEST_VIN_3_G2 = "JF2ABCDE6L0000003"
|
||||
|
||||
VEHICLE_DATA = {
|
||||
TEST_VIN_1_G1: {
|
||||
VEHICLE_VIN: TEST_VIN_1_G1,
|
||||
VEHICLE_NAME: "test_vehicle_1",
|
||||
VEHICLE_HAS_EV: False,
|
||||
VEHICLE_API_GEN: API_GEN_1,
|
||||
VEHICLE_HAS_REMOTE_START: True,
|
||||
VEHICLE_HAS_REMOTE_SERVICE: True,
|
||||
VEHICLE_HAS_SAFETY_SERVICE: False,
|
||||
},
|
||||
TEST_VIN_2_EV: {
|
||||
VEHICLE_VIN: TEST_VIN_2_EV,
|
||||
VEHICLE_NAME: "test_vehicle_2",
|
||||
VEHICLE_HAS_EV: True,
|
||||
VEHICLE_API_GEN: API_GEN_2,
|
||||
VEHICLE_HAS_REMOTE_START: True,
|
||||
VEHICLE_HAS_REMOTE_SERVICE: True,
|
||||
VEHICLE_HAS_SAFETY_SERVICE: True,
|
||||
},
|
||||
TEST_VIN_3_G2: {
|
||||
VEHICLE_VIN: TEST_VIN_3_G2,
|
||||
VEHICLE_NAME: "test_vehicle_3",
|
||||
VEHICLE_HAS_EV: False,
|
||||
VEHICLE_API_GEN: API_GEN_2,
|
||||
VEHICLE_HAS_REMOTE_START: True,
|
||||
VEHICLE_HAS_REMOTE_SERVICE: True,
|
||||
VEHICLE_HAS_SAFETY_SERVICE: True,
|
||||
},
|
||||
}
|
||||
|
||||
VEHICLE_STATUS_EV = {
|
||||
"status": {
|
||||
"AVG_FUEL_CONSUMPTION": 2.3,
|
||||
"BATTERY_VOLTAGE": "12.0",
|
||||
"DISTANCE_TO_EMPTY_FUEL": 707,
|
||||
"DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_BOOT_POSITION": "CLOSED",
|
||||
"DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_ENGINE_HOOD_POSITION": "CLOSED",
|
||||
"DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_FRONT_LEFT_POSITION": "CLOSED",
|
||||
"DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_FRONT_RIGHT_POSITION": "CLOSED",
|
||||
"DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_REAR_LEFT_POSITION": "CLOSED",
|
||||
"DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_REAR_RIGHT_POSITION": "CLOSED",
|
||||
"EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED",
|
||||
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
|
||||
"EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
|
||||
"EV_DISTANCE_TO_EMPTY": 17,
|
||||
"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",
|
||||
"ODOMETER": 1234,
|
||||
"POSITION_HEADING_DEGREE": "150",
|
||||
"POSITION_SPEED_KMPH": "0",
|
||||
"POSITION_TIMESTAMP": 1595560000.0,
|
||||
"SEAT_BELT_STATUS_FRONT_LEFT": "BELTED",
|
||||
"SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
|
||||
"SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED",
|
||||
"SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
|
||||
"SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN",
|
||||
"TIMESTAMP": 1595560000.0,
|
||||
"TRANSMISSION_MODE": "UNKNOWN",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": 2550,
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": 2550,
|
||||
"TYRE_PRESSURE_REAR_LEFT": 2450,
|
||||
"TYRE_PRESSURE_REAR_RIGHT": 2350,
|
||||
"TYRE_STATUS_FRONT_LEFT": "UNKNOWN",
|
||||
"TYRE_STATUS_FRONT_RIGHT": "UNKNOWN",
|
||||
"TYRE_STATUS_REAR_LEFT": "UNKNOWN",
|
||||
"TYRE_STATUS_REAR_RIGHT": "UNKNOWN",
|
||||
"VEHICLE_STATE_TYPE": "IGNITION_OFF",
|
||||
"WINDOW_BACK_STATUS": "UNKNOWN",
|
||||
"WINDOW_FRONT_LEFT_STATUS": "VENTED",
|
||||
"WINDOW_FRONT_RIGHT_STATUS": "VENTED",
|
||||
"WINDOW_REAR_LEFT_STATUS": "UNKNOWN",
|
||||
"WINDOW_REAR_RIGHT_STATUS": "UNKNOWN",
|
||||
"WINDOW_SUNROOF_STATUS": "UNKNOWN",
|
||||
"heading": 170,
|
||||
"latitude": 40.0,
|
||||
"longitude": -100.0,
|
||||
}
|
||||
}
|
||||
|
||||
VEHICLE_STATUS_G2 = {
|
||||
"status": {
|
||||
"AVG_FUEL_CONSUMPTION": 2.3,
|
||||
"BATTERY_VOLTAGE": "12.0",
|
||||
"DISTANCE_TO_EMPTY_FUEL": 707,
|
||||
"DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_BOOT_POSITION": "CLOSED",
|
||||
"DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_ENGINE_HOOD_POSITION": "CLOSED",
|
||||
"DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_FRONT_LEFT_POSITION": "CLOSED",
|
||||
"DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_FRONT_RIGHT_POSITION": "CLOSED",
|
||||
"DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_REAR_LEFT_POSITION": "CLOSED",
|
||||
"DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_REAR_RIGHT_POSITION": "CLOSED",
|
||||
"EXT_EXTERNAL_TEMP": "21.5",
|
||||
"ODOMETER": 1234,
|
||||
"POSITION_HEADING_DEGREE": "150",
|
||||
"POSITION_SPEED_KMPH": "0",
|
||||
"POSITION_TIMESTAMP": 1595560000.0,
|
||||
"SEAT_BELT_STATUS_FRONT_LEFT": "BELTED",
|
||||
"SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
|
||||
"SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED",
|
||||
"SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN",
|
||||
"SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
|
||||
"SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN",
|
||||
"SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN",
|
||||
"TIMESTAMP": 1595560000.0,
|
||||
"TRANSMISSION_MODE": "UNKNOWN",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": 2550,
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": 2550,
|
||||
"TYRE_PRESSURE_REAR_LEFT": 2450,
|
||||
"TYRE_PRESSURE_REAR_RIGHT": 2350,
|
||||
"TYRE_STATUS_FRONT_LEFT": "UNKNOWN",
|
||||
"TYRE_STATUS_FRONT_RIGHT": "UNKNOWN",
|
||||
"TYRE_STATUS_REAR_LEFT": "UNKNOWN",
|
||||
"TYRE_STATUS_REAR_RIGHT": "UNKNOWN",
|
||||
"VEHICLE_STATE_TYPE": "IGNITION_OFF",
|
||||
"WINDOW_BACK_STATUS": "UNKNOWN",
|
||||
"WINDOW_FRONT_LEFT_STATUS": "VENTED",
|
||||
"WINDOW_FRONT_RIGHT_STATUS": "VENTED",
|
||||
"WINDOW_REAR_LEFT_STATUS": "UNKNOWN",
|
||||
"WINDOW_REAR_RIGHT_STATUS": "UNKNOWN",
|
||||
"WINDOW_SUNROOF_STATUS": "UNKNOWN",
|
||||
"heading": 170,
|
||||
"latitude": 40.0,
|
||||
"longitude": -100.0,
|
||||
}
|
||||
}
|
||||
|
||||
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_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
|
||||
"EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
|
||||
"EV_DISTANCE_TO_EMPTY": "17",
|
||||
"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",
|
||||
"EXT_EXTERNAL_TEMP": "70.7",
|
||||
"ODOMETER": "766.8",
|
||||
"POSITION_HEADING_DEGREE": "150",
|
||||
"POSITION_SPEED_KMPH": "0",
|
||||
"POSITION_TIMESTAMP": 1595560000.0,
|
||||
"TIMESTAMP": 1595560000.0,
|
||||
"TRANSMISSION_MODE": "UNKNOWN",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": "37.0",
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": "37.0",
|
||||
"TYRE_PRESSURE_REAR_LEFT": "35.5",
|
||||
"TYRE_PRESSURE_REAR_RIGHT": "34.1",
|
||||
"VEHICLE_STATE_TYPE": "IGNITION_OFF",
|
||||
"heading": 170,
|
||||
"latitude": 40.0,
|
||||
"longitude": -100.0,
|
||||
}
|
||||
|
||||
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_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
|
||||
"EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
|
||||
"EV_DISTANCE_TO_EMPTY": "27.4",
|
||||
"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",
|
||||
"EXT_EXTERNAL_TEMP": "21.5",
|
||||
"ODOMETER": "1234",
|
||||
"POSITION_HEADING_DEGREE": "150",
|
||||
"POSITION_SPEED_KMPH": "0",
|
||||
"POSITION_TIMESTAMP": 1595560000.0,
|
||||
"TIMESTAMP": 1595560000.0,
|
||||
"TRANSMISSION_MODE": "UNKNOWN",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": "2550",
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": "2550",
|
||||
"TYRE_PRESSURE_REAR_LEFT": "2450",
|
||||
"TYRE_PRESSURE_REAR_RIGHT": "2350",
|
||||
"VEHICLE_STATE_TYPE": "IGNITION_OFF",
|
||||
"heading": 170,
|
||||
"latitude": 40.0,
|
||||
"longitude": -100.0,
|
||||
}
|
||||
|
||||
EXPECTED_STATE_EV_UNAVAILABLE = {
|
||||
"AVG_FUEL_CONSUMPTION": "unavailable",
|
||||
"BATTERY_VOLTAGE": "unavailable",
|
||||
"DISTANCE_TO_EMPTY_FUEL": "unavailable",
|
||||
"EV_CHARGER_STATE_TYPE": "unavailable",
|
||||
"EV_CHARGE_SETTING_AMPERE_TYPE": "unavailable",
|
||||
"EV_CHARGE_VOLT_TYPE": "unavailable",
|
||||
"EV_DISTANCE_TO_EMPTY": "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",
|
||||
"EXT_EXTERNAL_TEMP": "unavailable",
|
||||
"ODOMETER": "unavailable",
|
||||
"POSITION_HEADING_DEGREE": "unavailable",
|
||||
"POSITION_SPEED_KMPH": "unavailable",
|
||||
"POSITION_TIMESTAMP": "unavailable",
|
||||
"TIMESTAMP": "unavailable",
|
||||
"TRANSMISSION_MODE": "unavailable",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": "unavailable",
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": "unavailable",
|
||||
"TYRE_PRESSURE_REAR_LEFT": "unavailable",
|
||||
"TYRE_PRESSURE_REAR_RIGHT": "unavailable",
|
||||
"VEHICLE_STATE_TYPE": "unavailable",
|
||||
"heading": "unavailable",
|
||||
"latitude": "unavailable",
|
||||
"longitude": "unavailable",
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
"""Common functions needed to setup tests for Subaru component."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from subarulink.const import COUNTRY_USA
|
||||
|
||||
from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.components.subaru.const import (
|
||||
CONF_COUNTRY,
|
||||
CONF_UPDATE_ENABLED,
|
||||
DOMAIN,
|
||||
VEHICLE_API_GEN,
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_HAS_REMOTE_SERVICE,
|
||||
VEHICLE_HAS_REMOTE_START,
|
||||
VEHICLE_HAS_SAFETY_SERVICE,
|
||||
VEHICLE_NAME,
|
||||
)
|
||||
from homeassistant.config_entries import ENTRY_STATE_LOADED
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .api_responses import TEST_VIN_2_EV, VEHICLE_DATA, VEHICLE_STATUS_EV
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MOCK_API = "homeassistant.components.subaru.SubaruAPI."
|
||||
MOCK_API_CONNECT = f"{MOCK_API}connect"
|
||||
MOCK_API_IS_PIN_REQUIRED = f"{MOCK_API}is_pin_required"
|
||||
MOCK_API_TEST_PIN = f"{MOCK_API}test_pin"
|
||||
MOCK_API_UPDATE_SAVED_PIN = f"{MOCK_API}update_saved_pin"
|
||||
MOCK_API_GET_VEHICLES = f"{MOCK_API}get_vehicles"
|
||||
MOCK_API_VIN_TO_NAME = f"{MOCK_API}vin_to_name"
|
||||
MOCK_API_GET_API_GEN = f"{MOCK_API}get_api_gen"
|
||||
MOCK_API_GET_EV_STATUS = f"{MOCK_API}get_ev_status"
|
||||
MOCK_API_GET_RES_STATUS = f"{MOCK_API}get_res_status"
|
||||
MOCK_API_GET_REMOTE_STATUS = f"{MOCK_API}get_remote_status"
|
||||
MOCK_API_GET_SAFETY_STATUS = f"{MOCK_API}get_safety_status"
|
||||
MOCK_API_GET_GET_DATA = f"{MOCK_API}get_data"
|
||||
MOCK_API_UPDATE = f"{MOCK_API}update"
|
||||
MOCK_API_FETCH = f"{MOCK_API}fetch"
|
||||
|
||||
TEST_USERNAME = "user@email.com"
|
||||
TEST_PASSWORD = "password"
|
||||
TEST_PIN = "1234"
|
||||
TEST_DEVICE_ID = 1613183362
|
||||
TEST_COUNTRY = COUNTRY_USA
|
||||
|
||||
TEST_CREDS = {
|
||||
CONF_USERNAME: TEST_USERNAME,
|
||||
CONF_PASSWORD: TEST_PASSWORD,
|
||||
CONF_COUNTRY: TEST_COUNTRY,
|
||||
}
|
||||
|
||||
TEST_CONFIG = {
|
||||
CONF_USERNAME: TEST_USERNAME,
|
||||
CONF_PASSWORD: TEST_PASSWORD,
|
||||
CONF_COUNTRY: TEST_COUNTRY,
|
||||
CONF_PIN: TEST_PIN,
|
||||
CONF_DEVICE_ID: TEST_DEVICE_ID,
|
||||
}
|
||||
|
||||
TEST_OPTIONS = {
|
||||
CONF_UPDATE_ENABLED: True,
|
||||
}
|
||||
|
||||
TEST_ENTITY_ID = "sensor.test_vehicle_2_odometer"
|
||||
|
||||
|
||||
async def setup_subaru_integration(
|
||||
hass,
|
||||
vehicle_list=None,
|
||||
vehicle_data=None,
|
||||
vehicle_status=None,
|
||||
connect_effect=None,
|
||||
fetch_effect=None,
|
||||
):
|
||||
"""Create Subaru entry."""
|
||||
assert await async_setup_component(hass, HA_DOMAIN, {})
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=TEST_CONFIG,
|
||||
options=TEST_OPTIONS,
|
||||
entry_id=1,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
MOCK_API_CONNECT,
|
||||
return_value=connect_effect is None,
|
||||
side_effect=connect_effect,
|
||||
), patch(MOCK_API_GET_VEHICLES, return_value=vehicle_list,), patch(
|
||||
MOCK_API_VIN_TO_NAME,
|
||||
return_value=vehicle_data[VEHICLE_NAME],
|
||||
), patch(
|
||||
MOCK_API_GET_API_GEN,
|
||||
return_value=vehicle_data[VEHICLE_API_GEN],
|
||||
), patch(
|
||||
MOCK_API_GET_EV_STATUS,
|
||||
return_value=vehicle_data[VEHICLE_HAS_EV],
|
||||
), patch(
|
||||
MOCK_API_GET_RES_STATUS,
|
||||
return_value=vehicle_data[VEHICLE_HAS_REMOTE_START],
|
||||
), patch(
|
||||
MOCK_API_GET_REMOTE_STATUS,
|
||||
return_value=vehicle_data[VEHICLE_HAS_REMOTE_SERVICE],
|
||||
), patch(
|
||||
MOCK_API_GET_SAFETY_STATUS,
|
||||
return_value=vehicle_data[VEHICLE_HAS_SAFETY_SERVICE],
|
||||
), patch(
|
||||
MOCK_API_GET_GET_DATA,
|
||||
return_value=vehicle_status,
|
||||
), patch(
|
||||
MOCK_API_UPDATE,
|
||||
), patch(
|
||||
MOCK_API_FETCH, side_effect=fetch_effect
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def ev_entry(hass):
|
||||
"""Create a Subaru entry representing an EV vehicle with full STARLINK subscription."""
|
||||
entry = await setup_subaru_integration(
|
||||
hass,
|
||||
vehicle_list=[TEST_VIN_2_EV],
|
||||
vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
|
||||
vehicle_status=VEHICLE_STATUS_EV,
|
||||
)
|
||||
assert DOMAIN in hass.config_entries.async_domains()
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert hass.config_entries.async_get_entry(entry.entry_id)
|
||||
assert entry.state == ENTRY_STATE_LOADED
|
||||
return entry
|
|
@ -0,0 +1,250 @@
|
|||
"""Tests for the Subaru component config flow."""
|
||||
# pylint: disable=redefined-outer-name
|
||||
from copy import deepcopy
|
||||
from unittest import mock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from subarulink.exceptions import InvalidCredentials, InvalidPIN, SubaruException
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.subaru import config_flow
|
||||
from homeassistant.components.subaru.const import CONF_UPDATE_ENABLED, DOMAIN
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_PIN
|
||||
|
||||
from .conftest import (
|
||||
MOCK_API_CONNECT,
|
||||
MOCK_API_IS_PIN_REQUIRED,
|
||||
MOCK_API_TEST_PIN,
|
||||
MOCK_API_UPDATE_SAVED_PIN,
|
||||
TEST_CONFIG,
|
||||
TEST_CREDS,
|
||||
TEST_DEVICE_ID,
|
||||
TEST_PIN,
|
||||
TEST_USERNAME,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_user_form_init(user_form):
|
||||
"""Test the initial user form for first step of the config flow."""
|
||||
expected = {
|
||||
"data_schema": mock.ANY,
|
||||
"description_placeholders": None,
|
||||
"errors": None,
|
||||
"flow_id": mock.ANY,
|
||||
"handler": DOMAIN,
|
||||
"step_id": "user",
|
||||
"type": "form",
|
||||
}
|
||||
assert expected == user_form
|
||||
|
||||
|
||||
async def test_user_form_repeat_identifier(hass, user_form):
|
||||
"""Test we handle repeat identifiers."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, title=TEST_USERNAME, data=TEST_CREDS, options=None
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
MOCK_API_CONNECT,
|
||||
return_value=True,
|
||||
) as mock_connect:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
user_form["flow_id"],
|
||||
TEST_CREDS,
|
||||
)
|
||||
assert len(mock_connect.mock_calls) == 0
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_user_form_cannot_connect(hass, user_form):
|
||||
"""Test we handle cannot connect error."""
|
||||
with patch(
|
||||
MOCK_API_CONNECT,
|
||||
side_effect=SubaruException(None),
|
||||
) as mock_connect:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
user_form["flow_id"],
|
||||
TEST_CREDS,
|
||||
)
|
||||
assert len(mock_connect.mock_calls) == 1
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_user_form_invalid_auth(hass, user_form):
|
||||
"""Test we handle invalid auth."""
|
||||
with patch(
|
||||
MOCK_API_CONNECT,
|
||||
side_effect=InvalidCredentials("invalidAccount"),
|
||||
) as mock_connect:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
user_form["flow_id"],
|
||||
TEST_CREDS,
|
||||
)
|
||||
assert len(mock_connect.mock_calls) == 1
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_user_form_pin_not_required(hass, user_form):
|
||||
"""Test successful login when no PIN is required."""
|
||||
with patch(MOCK_API_CONNECT, return_value=True,) as mock_connect, patch(
|
||||
MOCK_API_IS_PIN_REQUIRED,
|
||||
return_value=False,
|
||||
) as mock_is_pin_required:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
user_form["flow_id"],
|
||||
TEST_CREDS,
|
||||
)
|
||||
assert len(mock_connect.mock_calls) == 2
|
||||
assert len(mock_is_pin_required.mock_calls) == 1
|
||||
|
||||
expected = {
|
||||
"title": TEST_USERNAME,
|
||||
"description": None,
|
||||
"description_placeholders": None,
|
||||
"flow_id": mock.ANY,
|
||||
"result": mock.ANY,
|
||||
"handler": DOMAIN,
|
||||
"type": "create_entry",
|
||||
"version": 1,
|
||||
"data": deepcopy(TEST_CONFIG),
|
||||
}
|
||||
expected["data"][CONF_PIN] = None
|
||||
result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID
|
||||
assert expected == result
|
||||
|
||||
|
||||
async def test_pin_form_init(pin_form):
|
||||
"""Test the pin entry form for second step of the config flow."""
|
||||
expected = {
|
||||
"data_schema": config_flow.PIN_SCHEMA,
|
||||
"description_placeholders": None,
|
||||
"errors": None,
|
||||
"flow_id": mock.ANY,
|
||||
"handler": DOMAIN,
|
||||
"step_id": "pin",
|
||||
"type": "form",
|
||||
}
|
||||
assert expected == pin_form
|
||||
|
||||
|
||||
async def test_pin_form_bad_pin_format(hass, pin_form):
|
||||
"""Test we handle invalid pin."""
|
||||
with patch(MOCK_API_TEST_PIN,) as mock_test_pin, patch(
|
||||
MOCK_API_UPDATE_SAVED_PIN,
|
||||
return_value=True,
|
||||
) as mock_update_saved_pin:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
pin_form["flow_id"], user_input={CONF_PIN: "abcd"}
|
||||
)
|
||||
assert len(mock_test_pin.mock_calls) == 0
|
||||
assert len(mock_update_saved_pin.mock_calls) == 1
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {"base": "bad_pin_format"}
|
||||
|
||||
|
||||
async def test_pin_form_success(hass, pin_form):
|
||||
"""Test successful PIN entry."""
|
||||
with patch(MOCK_API_TEST_PIN, return_value=True,) as mock_test_pin, patch(
|
||||
MOCK_API_UPDATE_SAVED_PIN,
|
||||
return_value=True,
|
||||
) as mock_update_saved_pin:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN}
|
||||
)
|
||||
|
||||
assert len(mock_test_pin.mock_calls) == 1
|
||||
assert len(mock_update_saved_pin.mock_calls) == 1
|
||||
expected = {
|
||||
"title": TEST_USERNAME,
|
||||
"description": None,
|
||||
"description_placeholders": None,
|
||||
"flow_id": mock.ANY,
|
||||
"result": mock.ANY,
|
||||
"handler": DOMAIN,
|
||||
"type": "create_entry",
|
||||
"version": 1,
|
||||
"data": TEST_CONFIG,
|
||||
}
|
||||
result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID
|
||||
assert result == expected
|
||||
|
||||
|
||||
async def test_pin_form_incorrect_pin(hass, pin_form):
|
||||
"""Test we handle invalid pin."""
|
||||
with patch(
|
||||
MOCK_API_TEST_PIN,
|
||||
side_effect=InvalidPIN("invalidPin"),
|
||||
) as mock_test_pin, patch(
|
||||
MOCK_API_UPDATE_SAVED_PIN,
|
||||
return_value=True,
|
||||
) as mock_update_saved_pin:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN}
|
||||
)
|
||||
assert len(mock_test_pin.mock_calls) == 1
|
||||
assert len(mock_update_saved_pin.mock_calls) == 1
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {"base": "incorrect_pin"}
|
||||
|
||||
|
||||
async def test_option_flow_form(options_form):
|
||||
"""Test config flow options form."""
|
||||
expected = {
|
||||
"data_schema": mock.ANY,
|
||||
"description_placeholders": None,
|
||||
"errors": None,
|
||||
"flow_id": mock.ANY,
|
||||
"handler": mock.ANY,
|
||||
"step_id": "init",
|
||||
"type": "form",
|
||||
}
|
||||
assert expected == options_form
|
||||
|
||||
|
||||
async def test_option_flow(hass, options_form):
|
||||
"""Test config flow options."""
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
options_form["flow_id"],
|
||||
user_input={
|
||||
CONF_UPDATE_ENABLED: False,
|
||||
},
|
||||
)
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["data"] == {
|
||||
CONF_UPDATE_ENABLED: False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def user_form(hass):
|
||||
"""Return initial form for Subaru config flow."""
|
||||
return await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def pin_form(hass, user_form):
|
||||
"""Return second form (PIN input) for Subaru config flow."""
|
||||
with patch(MOCK_API_CONNECT, return_value=True,), patch(
|
||||
MOCK_API_IS_PIN_REQUIRED,
|
||||
return_value=True,
|
||||
):
|
||||
return await hass.config_entries.flow.async_configure(
|
||||
user_form["flow_id"], user_input=TEST_CREDS
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def options_form(hass):
|
||||
"""Return options form for Subaru config flow."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={}, options=None)
|
||||
entry.add_to_hass(hass)
|
||||
return await hass.config_entries.options.async_init(entry.entry_id)
|
|
@ -0,0 +1,153 @@
|
|||
"""Test Subaru component setup and updates."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from subarulink import InvalidCredentials, SubaruException
|
||||
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
)
|
||||
from homeassistant.components.subaru.const import DOMAIN
|
||||
from homeassistant.config_entries import (
|
||||
ENTRY_STATE_LOADED,
|
||||
ENTRY_STATE_NOT_LOADED,
|
||||
ENTRY_STATE_SETUP_ERROR,
|
||||
ENTRY_STATE_SETUP_RETRY,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .api_responses import (
|
||||
TEST_VIN_1_G1,
|
||||
TEST_VIN_2_EV,
|
||||
TEST_VIN_3_G2,
|
||||
VEHICLE_DATA,
|
||||
VEHICLE_STATUS_EV,
|
||||
VEHICLE_STATUS_G2,
|
||||
)
|
||||
from .conftest import (
|
||||
MOCK_API_FETCH,
|
||||
MOCK_API_UPDATE,
|
||||
TEST_ENTITY_ID,
|
||||
setup_subaru_integration,
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_with_no_config(hass):
|
||||
"""Test DOMAIN is empty if there is no config."""
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
assert DOMAIN not in hass.config_entries.async_domains()
|
||||
|
||||
|
||||
async def test_setup_ev(hass, ev_entry):
|
||||
"""Test setup with an EV vehicle."""
|
||||
check_entry = hass.config_entries.async_get_entry(ev_entry.entry_id)
|
||||
assert check_entry
|
||||
assert check_entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
|
||||
async def test_setup_g2(hass):
|
||||
"""Test setup with a G2 vehcile ."""
|
||||
entry = await setup_subaru_integration(
|
||||
hass,
|
||||
vehicle_list=[TEST_VIN_3_G2],
|
||||
vehicle_data=VEHICLE_DATA[TEST_VIN_3_G2],
|
||||
vehicle_status=VEHICLE_STATUS_G2,
|
||||
)
|
||||
check_entry = hass.config_entries.async_get_entry(entry.entry_id)
|
||||
assert check_entry
|
||||
assert check_entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
|
||||
async def test_setup_g1(hass):
|
||||
"""Test setup with a G1 vehicle."""
|
||||
entry = await setup_subaru_integration(
|
||||
hass, vehicle_list=[TEST_VIN_1_G1], vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1]
|
||||
)
|
||||
check_entry = hass.config_entries.async_get_entry(entry.entry_id)
|
||||
assert check_entry
|
||||
assert check_entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
|
||||
async def test_unsuccessful_connect(hass):
|
||||
"""Test unsuccessful connect due to connectivity."""
|
||||
entry = await setup_subaru_integration(
|
||||
hass,
|
||||
connect_effect=SubaruException("Service Unavailable"),
|
||||
vehicle_list=[TEST_VIN_2_EV],
|
||||
vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
|
||||
vehicle_status=VEHICLE_STATUS_EV,
|
||||
)
|
||||
check_entry = hass.config_entries.async_get_entry(entry.entry_id)
|
||||
assert check_entry
|
||||
assert check_entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
|
||||
|
||||
async def test_invalid_credentials(hass):
|
||||
"""Test invalid credentials."""
|
||||
entry = await setup_subaru_integration(
|
||||
hass,
|
||||
connect_effect=InvalidCredentials("Invalid Credentials"),
|
||||
vehicle_list=[TEST_VIN_2_EV],
|
||||
vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
|
||||
vehicle_status=VEHICLE_STATUS_EV,
|
||||
)
|
||||
check_entry = hass.config_entries.async_get_entry(entry.entry_id)
|
||||
assert check_entry
|
||||
assert check_entry.state == ENTRY_STATE_SETUP_ERROR
|
||||
|
||||
|
||||
async def test_update_skip_unsubscribed(hass):
|
||||
"""Test update function skips vehicles without subscription."""
|
||||
await setup_subaru_integration(
|
||||
hass, vehicle_list=[TEST_VIN_1_G1], vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1]
|
||||
)
|
||||
|
||||
with patch(MOCK_API_FETCH) as mock_fetch:
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
mock_fetch.assert_not_called()
|
||||
|
||||
|
||||
async def test_update_disabled(hass, ev_entry):
|
||||
"""Test update function disable option."""
|
||||
with patch(MOCK_API_FETCH, side_effect=SubaruException("403 Error"),), patch(
|
||||
MOCK_API_UPDATE,
|
||||
) as mock_update:
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_update.assert_not_called()
|
||||
|
||||
|
||||
async def test_fetch_failed(hass):
|
||||
"""Tests when fetch fails."""
|
||||
await setup_subaru_integration(
|
||||
hass,
|
||||
vehicle_list=[TEST_VIN_2_EV],
|
||||
vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
|
||||
vehicle_status=VEHICLE_STATUS_EV,
|
||||
fetch_effect=SubaruException("403 Error"),
|
||||
)
|
||||
|
||||
test_entity = hass.states.get(TEST_ENTITY_ID)
|
||||
assert test_entity.state == "unavailable"
|
||||
|
||||
|
||||
async def test_unload_entry(hass, ev_entry):
|
||||
"""Test that entry is unloaded."""
|
||||
assert ev_entry.state == ENTRY_STATE_LOADED
|
||||
assert await hass.config_entries.async_unload(ev_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert ev_entry.state == ENTRY_STATE_NOT_LOADED
|
|
@ -0,0 +1,67 @@
|
|||
"""Test Subaru sensors."""
|
||||
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
|
||||
|
||||
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 tests.components.subaru.conftest import setup_subaru_integration
|
||||
|
||||
VEHICLE_NAME = VEHICLE_DATA[TEST_VIN_2_EV][VEHICLE_NAME]
|
||||
|
||||
|
||||
async def test_sensors_ev_imperial(hass):
|
||||
"""Test sensors supporting imperial units."""
|
||||
hass.config.units = IMPERIAL_SYSTEM
|
||||
await setup_subaru_integration(
|
||||
hass,
|
||||
vehicle_list=[TEST_VIN_2_EV],
|
||||
vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
|
||||
vehicle_status=VEHICLE_STATUS_EV,
|
||||
)
|
||||
_assert_data(hass, EXPECTED_STATE_EV_IMPERIAL)
|
||||
|
||||
|
||||
async def test_sensors_ev_metric(hass, ev_entry):
|
||||
"""Test sensors supporting metric units."""
|
||||
_assert_data(hass, EXPECTED_STATE_EV_METRIC)
|
||||
|
||||
|
||||
async def test_sensors_missing_vin_data(hass):
|
||||
"""Test for missing VIN dataset."""
|
||||
await setup_subaru_integration(
|
||||
hass,
|
||||
vehicle_list=[TEST_VIN_2_EV],
|
||||
vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
|
||||
vehicle_status=None,
|
||||
)
|
||||
_assert_data(hass, EXPECTED_STATE_EV_UNAVAILABLE)
|
||||
|
||||
|
||||
def _assert_data(hass, expected_state):
|
||||
sensor_list = EV_SENSORS
|
||||
sensor_list.extend(API_GEN_2_SENSORS)
|
||||
sensor_list.extend(SAFETY_SENSORS)
|
||||
expected_states = {}
|
||||
for item in sensor_list:
|
||||
expected_states[
|
||||
f"sensor.{slugify(f'{VEHICLE_NAME} {item[SENSOR_TYPE]}')}"
|
||||
] = expected_state[item[SENSOR_FIELD]]
|
||||
|
||||
for sensor in expected_states:
|
||||
actual = hass.states.get(sensor)
|
||||
assert actual.state == expected_states[sensor]
|
Loading…
Reference in New Issue