mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
Add Mazda Connected Services integration (#45768)
This commit is contained in:
parent
a584ad5ac3
commit
4b208746e5
@ -263,6 +263,7 @@ homeassistant/components/lutron_caseta/* @swails @bdraco
|
|||||||
homeassistant/components/lyric/* @timmo001
|
homeassistant/components/lyric/* @timmo001
|
||||||
homeassistant/components/mastodon/* @fabaff
|
homeassistant/components/mastodon/* @fabaff
|
||||||
homeassistant/components/matrix/* @tinloaf
|
homeassistant/components/matrix/* @tinloaf
|
||||||
|
homeassistant/components/mazda/* @bdr99
|
||||||
homeassistant/components/mcp23017/* @jardiamj
|
homeassistant/components/mcp23017/* @jardiamj
|
||||||
homeassistant/components/media_source/* @hunterjm
|
homeassistant/components/media_source/* @hunterjm
|
||||||
homeassistant/components/mediaroom/* @dgomes
|
homeassistant/components/mediaroom/* @dgomes
|
||||||
|
173
homeassistant/components/mazda/__init__.py
Normal file
173
homeassistant/components/mazda/__init__.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
"""The Mazda Connected Services integration."""
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
|
from pymazda import (
|
||||||
|
Client as MazdaAPI,
|
||||||
|
MazdaAccountLockedException,
|
||||||
|
MazdaAPIEncryptionException,
|
||||||
|
MazdaAuthenticationException,
|
||||||
|
MazdaException,
|
||||||
|
MazdaTokenExpiredException,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
DataUpdateCoordinator,
|
||||||
|
UpdateFailed,
|
||||||
|
)
|
||||||
|
from homeassistant.util.async_ import gather_with_concurrency
|
||||||
|
|
||||||
|
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS = ["sensor"]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
|
"""Set up the Mazda Connected Services component."""
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Set up Mazda Connected Services from a config entry."""
|
||||||
|
email = entry.data[CONF_EMAIL]
|
||||||
|
password = entry.data[CONF_PASSWORD]
|
||||||
|
region = entry.data[CONF_REGION]
|
||||||
|
|
||||||
|
websession = aiohttp_client.async_get_clientsession(hass)
|
||||||
|
mazda_client = MazdaAPI(email, password, region, websession)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mazda_client.validate_credentials()
|
||||||
|
except MazdaAuthenticationException:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_REAUTH},
|
||||||
|
data=entry.data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
except (
|
||||||
|
MazdaException,
|
||||||
|
MazdaAccountLockedException,
|
||||||
|
MazdaTokenExpiredException,
|
||||||
|
MazdaAPIEncryptionException,
|
||||||
|
) as ex:
|
||||||
|
_LOGGER.error("Error occurred during Mazda login request: %s", ex)
|
||||||
|
raise ConfigEntryNotReady from ex
|
||||||
|
|
||||||
|
async def async_update_data():
|
||||||
|
"""Fetch data from Mazda API."""
|
||||||
|
|
||||||
|
async def with_timeout(task):
|
||||||
|
async with async_timeout.timeout(10):
|
||||||
|
return await task
|
||||||
|
|
||||||
|
try:
|
||||||
|
vehicles = await with_timeout(mazda_client.get_vehicles())
|
||||||
|
|
||||||
|
vehicle_status_tasks = [
|
||||||
|
with_timeout(mazda_client.get_vehicle_status(vehicle["id"]))
|
||||||
|
for vehicle in vehicles
|
||||||
|
]
|
||||||
|
statuses = await gather_with_concurrency(5, *vehicle_status_tasks)
|
||||||
|
|
||||||
|
for vehicle, status in zip(vehicles, statuses):
|
||||||
|
vehicle["status"] = status
|
||||||
|
|
||||||
|
return vehicles
|
||||||
|
except MazdaAuthenticationException as ex:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_REAUTH},
|
||||||
|
data=entry.data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise UpdateFailed("Not authenticated with Mazda API") from ex
|
||||||
|
except Exception as ex:
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Unknown error occurred during Mazda update request: %s", ex
|
||||||
|
)
|
||||||
|
raise UpdateFailed(ex) from ex
|
||||||
|
|
||||||
|
coordinator = DataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_method=async_update_data,
|
||||||
|
update_interval=timedelta(seconds=60),
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
|
DATA_CLIENT: mazda_client,
|
||||||
|
DATA_COORDINATOR: coordinator,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fetch initial data so we have data when entities subscribe
|
||||||
|
await coordinator.async_refresh()
|
||||||
|
if not coordinator.last_update_success:
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
# Setup components
|
||||||
|
for component in 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 PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
class MazdaEntity(CoordinatorEntity):
|
||||||
|
"""Defines a base Mazda entity."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator, index):
|
||||||
|
"""Initialize the Mazda entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.index = index
|
||||||
|
self.vin = self.coordinator.data[self.index]["vin"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device info for the Mazda entity."""
|
||||||
|
data = self.coordinator.data[self.index]
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self.vin)},
|
||||||
|
"name": self.get_vehicle_name(),
|
||||||
|
"manufacturer": "Mazda",
|
||||||
|
"model": f"{data['modelYear']} {data['carlineName']}",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_vehicle_name(self):
|
||||||
|
"""Return the vehicle name, to be used as a prefix for names of other entities."""
|
||||||
|
data = self.coordinator.data[self.index]
|
||||||
|
if "nickname" in data and len(data["nickname"]) > 0:
|
||||||
|
return data["nickname"]
|
||||||
|
return f"{data['modelYear']} {data['carlineName']}"
|
117
homeassistant/components/mazda/config_flow.py
Normal file
117
homeassistant/components/mazda/config_flow.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
"""Config flow for Mazda Connected Services integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from pymazda import (
|
||||||
|
Client as MazdaAPI,
|
||||||
|
MazdaAccountLockedException,
|
||||||
|
MazdaAuthenticationException,
|
||||||
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
# https://github.com/PyCQA/pylint/issues/3202
|
||||||
|
from .const import DOMAIN # pylint: disable=unused-import
|
||||||
|
from .const import MAZDA_REGIONS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_EMAIL): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
vol.Required(CONF_REGION): vol.In(MAZDA_REGIONS),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Mazda Connected Services."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
await self.async_set_unique_id(user_input[CONF_EMAIL].lower())
|
||||||
|
|
||||||
|
try:
|
||||||
|
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
mazda_client = MazdaAPI(
|
||||||
|
user_input[CONF_EMAIL],
|
||||||
|
user_input[CONF_PASSWORD],
|
||||||
|
user_input[CONF_REGION],
|
||||||
|
websession,
|
||||||
|
)
|
||||||
|
await mazda_client.validate_credentials()
|
||||||
|
except MazdaAuthenticationException:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except MazdaAccountLockedException:
|
||||||
|
errors["base"] = "account_locked"
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Unknown error occurred during Mazda login request: %s", ex
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_EMAIL], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(self, user_input=None):
|
||||||
|
"""Perform reauth if the user credentials have changed."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
mazda_client = MazdaAPI(
|
||||||
|
user_input[CONF_EMAIL],
|
||||||
|
user_input[CONF_PASSWORD],
|
||||||
|
user_input[CONF_REGION],
|
||||||
|
websession,
|
||||||
|
)
|
||||||
|
await mazda_client.validate_credentials()
|
||||||
|
except MazdaAuthenticationException:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except MazdaAccountLockedException:
|
||||||
|
errors["base"] = "account_locked"
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Unknown error occurred during Mazda login request: %s", ex
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(user_input[CONF_EMAIL].lower())
|
||||||
|
|
||||||
|
for entry in self._async_current_entries():
|
||||||
|
if entry.unique_id == self.unique_id:
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
entry, data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reload the config entry otherwise devices will remain unavailable
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_abort(reason="reauth_successful")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
8
homeassistant/components/mazda/const.py
Normal file
8
homeassistant/components/mazda/const.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""Constants for the Mazda Connected Services integration."""
|
||||||
|
|
||||||
|
DOMAIN = "mazda"
|
||||||
|
|
||||||
|
DATA_CLIENT = "mazda_client"
|
||||||
|
DATA_COORDINATOR = "coordinator"
|
||||||
|
|
||||||
|
MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"}
|
9
homeassistant/components/mazda/manifest.json
Normal file
9
homeassistant/components/mazda/manifest.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"domain": "mazda",
|
||||||
|
"name": "Mazda Connected Services",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||||
|
"requirements": ["pymazda==0.0.8"],
|
||||||
|
"codeowners": ["@bdr99"],
|
||||||
|
"quality_scale": "platinum"
|
||||||
|
}
|
263
homeassistant/components/mazda/sensor.py
Normal file
263
homeassistant/components/mazda/sensor.py
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
"""Platform for Mazda sensor integration."""
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_UNIT_SYSTEM_IMPERIAL,
|
||||||
|
LENGTH_KILOMETERS,
|
||||||
|
LENGTH_MILES,
|
||||||
|
PERCENTAGE,
|
||||||
|
PRESSURE_PSI,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import MazdaEntity
|
||||||
|
from .const import DATA_COORDINATOR, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up the sensor platform."""
|
||||||
|
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
for index, _ in enumerate(coordinator.data):
|
||||||
|
entities.append(MazdaFuelRemainingSensor(coordinator, index))
|
||||||
|
entities.append(MazdaFuelDistanceSensor(coordinator, index))
|
||||||
|
entities.append(MazdaOdometerSensor(coordinator, index))
|
||||||
|
entities.append(MazdaFrontLeftTirePressureSensor(coordinator, index))
|
||||||
|
entities.append(MazdaFrontRightTirePressureSensor(coordinator, index))
|
||||||
|
entities.append(MazdaRearLeftTirePressureSensor(coordinator, index))
|
||||||
|
entities.append(MazdaRearRightTirePressureSensor(coordinator, index))
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class MazdaFuelRemainingSensor(MazdaEntity):
|
||||||
|
"""Class for the fuel remaining sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
vehicle_name = self.get_vehicle_name()
|
||||||
|
return f"{vehicle_name} Fuel Remaining Percentage"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique identifier for this entity."""
|
||||||
|
return f"{self.vin}_fuel_remaining_percentage"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
return PERCENTAGE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend."""
|
||||||
|
return "mdi:gas-station"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.coordinator.data[self.index]["status"]["fuelRemainingPercent"]
|
||||||
|
|
||||||
|
|
||||||
|
class MazdaFuelDistanceSensor(MazdaEntity):
|
||||||
|
"""Class for the fuel distance sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
vehicle_name = self.get_vehicle_name()
|
||||||
|
return f"{vehicle_name} Fuel Distance Remaining"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique identifier for this entity."""
|
||||||
|
return f"{self.vin}_fuel_distance_remaining"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
|
||||||
|
return LENGTH_MILES
|
||||||
|
return LENGTH_KILOMETERS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend."""
|
||||||
|
return "mdi:gas-station"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
fuel_distance_km = self.coordinator.data[self.index]["status"][
|
||||||
|
"fuelDistanceRemainingKm"
|
||||||
|
]
|
||||||
|
return round(self.hass.config.units.length(fuel_distance_km, LENGTH_KILOMETERS))
|
||||||
|
|
||||||
|
|
||||||
|
class MazdaOdometerSensor(MazdaEntity):
|
||||||
|
"""Class for the odometer sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
vehicle_name = self.get_vehicle_name()
|
||||||
|
return f"{vehicle_name} Odometer"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique identifier for this entity."""
|
||||||
|
return f"{self.vin}_odometer"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
|
||||||
|
return LENGTH_MILES
|
||||||
|
return LENGTH_KILOMETERS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend."""
|
||||||
|
return "mdi:speedometer"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
odometer_km = self.coordinator.data[self.index]["status"]["odometerKm"]
|
||||||
|
return round(self.hass.config.units.length(odometer_km, LENGTH_KILOMETERS))
|
||||||
|
|
||||||
|
|
||||||
|
class MazdaFrontLeftTirePressureSensor(MazdaEntity):
|
||||||
|
"""Class for the front left tire pressure sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
vehicle_name = self.get_vehicle_name()
|
||||||
|
return f"{vehicle_name} Front Left Tire Pressure"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique identifier for this entity."""
|
||||||
|
return f"{self.vin}_front_left_tire_pressure"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
return PRESSURE_PSI
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend."""
|
||||||
|
return "mdi:car-tire-alert"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return round(
|
||||||
|
self.coordinator.data[self.index]["status"]["tirePressure"][
|
||||||
|
"frontLeftTirePressurePsi"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MazdaFrontRightTirePressureSensor(MazdaEntity):
|
||||||
|
"""Class for the front right tire pressure sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
vehicle_name = self.get_vehicle_name()
|
||||||
|
return f"{vehicle_name} Front Right Tire Pressure"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique identifier for this entity."""
|
||||||
|
return f"{self.vin}_front_right_tire_pressure"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
return PRESSURE_PSI
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend."""
|
||||||
|
return "mdi:car-tire-alert"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return round(
|
||||||
|
self.coordinator.data[self.index]["status"]["tirePressure"][
|
||||||
|
"frontRightTirePressurePsi"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MazdaRearLeftTirePressureSensor(MazdaEntity):
|
||||||
|
"""Class for the rear left tire pressure sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
vehicle_name = self.get_vehicle_name()
|
||||||
|
return f"{vehicle_name} Rear Left Tire Pressure"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique identifier for this entity."""
|
||||||
|
return f"{self.vin}_rear_left_tire_pressure"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
return PRESSURE_PSI
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend."""
|
||||||
|
return "mdi:car-tire-alert"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return round(
|
||||||
|
self.coordinator.data[self.index]["status"]["tirePressure"][
|
||||||
|
"rearLeftTirePressurePsi"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MazdaRearRightTirePressureSensor(MazdaEntity):
|
||||||
|
"""Class for the rear right tire pressure sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
vehicle_name = self.get_vehicle_name()
|
||||||
|
return f"{vehicle_name} Rear Right Tire Pressure"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique identifier for this entity."""
|
||||||
|
return f"{self.vin}_rear_right_tire_pressure"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
return PRESSURE_PSI
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend."""
|
||||||
|
return "mdi:car-tire-alert"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return round(
|
||||||
|
self.coordinator.data[self.index]["status"]["tirePressure"][
|
||||||
|
"rearRightTirePressurePsi"
|
||||||
|
]
|
||||||
|
)
|
35
homeassistant/components/mazda/strings.json
Normal file
35
homeassistant/components/mazda/strings.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"account_locked": "Account locked. Please try again later.",
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"region": "Region"
|
||||||
|
},
|
||||||
|
"description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.",
|
||||||
|
"title": "Mazda Connected Services - Authentication Failed"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"region": "Region"
|
||||||
|
},
|
||||||
|
"description": "Please enter the email address and password you use to log into the MyMazda mobile app.",
|
||||||
|
"title": "Mazda Connected Services - Add Account"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Mazda Connected Services"
|
||||||
|
}
|
35
homeassistant/components/mazda/translations/en.json
Normal file
35
homeassistant/components/mazda/translations/en.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Account is already configured",
|
||||||
|
"reauth_successful": "Re-authentication was successful"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"account_locked": "Account locked. Please try again later.",
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"reauth": {
|
||||||
|
"data": {
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Password",
|
||||||
|
"region": "Region"
|
||||||
|
},
|
||||||
|
"description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.",
|
||||||
|
"title": "Mazda Connected Services - Authentication Failed"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Password",
|
||||||
|
"region": "Region"
|
||||||
|
},
|
||||||
|
"description": "Please enter the email address and password you use to log into the MyMazda mobile app.",
|
||||||
|
"title": "Mazda Connected Services - Add Account"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Mazda Connected Services"
|
||||||
|
}
|
@ -123,6 +123,7 @@ FLOWS = [
|
|||||||
"lutron_caseta",
|
"lutron_caseta",
|
||||||
"lyric",
|
"lyric",
|
||||||
"mailgun",
|
"mailgun",
|
||||||
|
"mazda",
|
||||||
"melcloud",
|
"melcloud",
|
||||||
"met",
|
"met",
|
||||||
"meteo_france",
|
"meteo_france",
|
||||||
|
@ -1517,6 +1517,9 @@ pymailgunner==1.4
|
|||||||
# homeassistant.components.firmata
|
# homeassistant.components.firmata
|
||||||
pymata-express==1.19
|
pymata-express==1.19
|
||||||
|
|
||||||
|
# homeassistant.components.mazda
|
||||||
|
pymazda==0.0.8
|
||||||
|
|
||||||
# homeassistant.components.mediaroom
|
# homeassistant.components.mediaroom
|
||||||
pymediaroom==0.6.4.1
|
pymediaroom==0.6.4.1
|
||||||
|
|
||||||
|
@ -789,6 +789,9 @@ pymailgunner==1.4
|
|||||||
# homeassistant.components.firmata
|
# homeassistant.components.firmata
|
||||||
pymata-express==1.19
|
pymata-express==1.19
|
||||||
|
|
||||||
|
# homeassistant.components.mazda
|
||||||
|
pymazda==0.0.8
|
||||||
|
|
||||||
# homeassistant.components.melcloud
|
# homeassistant.components.melcloud
|
||||||
pymelcloud==2.5.2
|
pymelcloud==2.5.2
|
||||||
|
|
||||||
|
53
tests/components/mazda/__init__.py
Normal file
53
tests/components/mazda/__init__.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""Tests for the Mazda Connected Services integration."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from pymazda import Client as MazdaAPI
|
||||||
|
|
||||||
|
from homeassistant.components.mazda.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
FIXTURE_USER_INPUT = {
|
||||||
|
CONF_EMAIL: "example@example.com",
|
||||||
|
CONF_PASSWORD: "password",
|
||||||
|
CONF_REGION: "MNAO",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfigEntry:
|
||||||
|
"""Set up the Mazda Connected Services integration in Home Assistant."""
|
||||||
|
get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json"))
|
||||||
|
if not use_nickname:
|
||||||
|
get_vehicles_fixture[0].pop("nickname")
|
||||||
|
|
||||||
|
get_vehicle_status_fixture = json.loads(
|
||||||
|
load_fixture("mazda/get_vehicle_status.json")
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
client_mock = MagicMock(
|
||||||
|
MazdaAPI(
|
||||||
|
FIXTURE_USER_INPUT[CONF_EMAIL],
|
||||||
|
FIXTURE_USER_INPUT[CONF_PASSWORD],
|
||||||
|
FIXTURE_USER_INPUT[CONF_REGION],
|
||||||
|
aiohttp_client.async_get_clientsession(hass),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
client_mock.get_vehicles = AsyncMock(return_value=get_vehicles_fixture)
|
||||||
|
client_mock.get_vehicle_status = AsyncMock(return_value=get_vehicle_status_fixture)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI",
|
||||||
|
return_value=client_mock,
|
||||||
|
), patch("homeassistant.components.mazda.MazdaAPI", return_value=client_mock):
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return config_entry
|
310
tests/components/mazda/test_config_flow.py
Normal file
310
tests/components/mazda/test_config_flow.py
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
"""Test the Mazda Connected Services config flow."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from homeassistant import config_entries, data_entry_flow, setup
|
||||||
|
from homeassistant.components.mazda.config_flow import (
|
||||||
|
MazdaAccountLockedException,
|
||||||
|
MazdaAuthenticationException,
|
||||||
|
)
|
||||||
|
from homeassistant.components.mazda.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
FIXTURE_USER_INPUT = {
|
||||||
|
CONF_EMAIL: "example@example.com",
|
||||||
|
CONF_PASSWORD: "password",
|
||||||
|
CONF_REGION: "MNAO",
|
||||||
|
}
|
||||||
|
FIXTURE_USER_INPUT_REAUTH = {
|
||||||
|
CONF_EMAIL: "example@example.com",
|
||||||
|
CONF_PASSWORD: "password_fixed",
|
||||||
|
CONF_REGION: "MNAO",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass):
|
||||||
|
"""Test the entire flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
return_value=True,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.mazda.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.mazda.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == FIXTURE_USER_INPUT[CONF_EMAIL]
|
||||||
|
assert result2["data"] == FIXTURE_USER_INPUT
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we handle invalid auth."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=MazdaAuthenticationException("Failed to authenticate"),
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["step_id"] == "user"
|
||||||
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_account_locked(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we handle account locked error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=MazdaAccountLockedException("Account locked"),
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["step_id"] == "user"
|
||||||
|
assert result2["errors"] == {"base": "account_locked"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=aiohttp.ClientError,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_unknown_error(hass):
|
||||||
|
"""Test we handle unknown error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=Exception,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reauth_flow(hass: HomeAssistant) -> None:
|
||||||
|
"""Test reauth works."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=MazdaAuthenticationException("Failed to authenticate"),
|
||||||
|
):
|
||||||
|
mock_config = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
|
||||||
|
data=FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
mock_config.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_config.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "reauth"
|
||||||
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": "reauth", "unique_id": FIXTURE_USER_INPUT[CONF_EMAIL]},
|
||||||
|
data=FIXTURE_USER_INPUT_REAUTH,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result2["reason"] == "reauth_successful"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reauth_authorization_error(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we show user form on authorization error."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=MazdaAuthenticationException("Failed to authenticate"),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "reauth"
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT_REAUTH,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["step_id"] == "reauth"
|
||||||
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reauth_account_locked(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we show user form on account_locked error."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=MazdaAccountLockedException("Account locked"),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "reauth"
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT_REAUTH,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["step_id"] == "reauth"
|
||||||
|
assert result2["errors"] == {"base": "account_locked"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reauth_connection_error(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we show user form on connection error."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=aiohttp.ClientError,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "reauth"
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT_REAUTH,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["step_id"] == "reauth"
|
||||||
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reauth_unknown_error(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we show user form on unknown error."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=Exception,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "reauth"
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT_REAUTH,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["step_id"] == "reauth"
|
||||||
|
assert result2["errors"] == {"base": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reauth_unique_id_not_found(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we show user form when unique id not found during reauth."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "reauth"
|
||||||
|
|
||||||
|
# Change the unique_id of the flow in order to cause a mismatch
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
flows[0]["context"]["unique_id"] = "example2@example.com"
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
FIXTURE_USER_INPUT_REAUTH,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["step_id"] == "reauth"
|
||||||
|
assert result2["errors"] == {"base": "unknown"}
|
100
tests/components/mazda/test_init.py
Normal file
100
tests/components/mazda/test_init.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
"""Tests for the Mazda Connected Services integration."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pymazda import MazdaAuthenticationException, MazdaException
|
||||||
|
|
||||||
|
from homeassistant.components.mazda.const import DATA_COORDINATOR, DOMAIN
|
||||||
|
from homeassistant.config_entries import (
|
||||||
|
ENTRY_STATE_LOADED,
|
||||||
|
ENTRY_STATE_SETUP_ERROR,
|
||||||
|
ENTRY_STATE_SETUP_RETRY,
|
||||||
|
)
|
||||||
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.components.mazda import init_integration
|
||||||
|
|
||||||
|
FIXTURE_USER_INPUT = {
|
||||||
|
CONF_EMAIL: "example@example.com",
|
||||||
|
CONF_PASSWORD: "password",
|
||||||
|
CONF_REGION: "MNAO",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_entry_not_ready(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the Mazda configuration entry not ready."""
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=MazdaException("Unknown error"),
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_init_auth_failure(hass: HomeAssistant):
|
||||||
|
"""Test auth failure during setup."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=MazdaAuthenticationException("Login failed"),
|
||||||
|
):
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0].state == ENTRY_STATE_SETUP_ERROR
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
assert len(flows) == 1
|
||||||
|
assert flows[0]["step_id"] == "reauth"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_auth_failure(hass: HomeAssistant):
|
||||||
|
"""Test auth failure during data update."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
|
||||||
|
return_value=True,
|
||||||
|
), patch("homeassistant.components.mazda.MazdaAPI.get_vehicles", return_value={}):
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0].state == ENTRY_STATE_LOADED
|
||||||
|
|
||||||
|
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
|
||||||
|
side_effect=MazdaAuthenticationException("Login failed"),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
|
||||||
|
side_effect=MazdaAuthenticationException("Login failed"),
|
||||||
|
):
|
||||||
|
await coordinator.async_refresh()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
assert len(flows) == 1
|
||||||
|
assert flows[0]["step_id"] == "reauth"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unload_config_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the Mazda configuration entry unloading."""
|
||||||
|
entry = await init_integration(hass)
|
||||||
|
assert hass.data[DOMAIN]
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not hass.data.get(DOMAIN)
|
160
tests/components/mazda/test_sensor.py
Normal file
160
tests/components/mazda/test_sensor.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
"""The sensor tests for the Mazda Connected Services integration."""
|
||||||
|
|
||||||
|
from homeassistant.components.mazda.const import DOMAIN
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_FRIENDLY_NAME,
|
||||||
|
ATTR_ICON,
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
|
LENGTH_KILOMETERS,
|
||||||
|
LENGTH_MILES,
|
||||||
|
PERCENTAGE,
|
||||||
|
PRESSURE_PSI,
|
||||||
|
)
|
||||||
|
from homeassistant.util.unit_system import IMPERIAL_SYSTEM
|
||||||
|
|
||||||
|
from tests.components.mazda import init_integration
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_nickname(hass):
|
||||||
|
"""Test creation of the device when vehicle has a nickname."""
|
||||||
|
await init_integration(hass, use_nickname=True)
|
||||||
|
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
reg_device = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, "JM000000000000000")},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD"
|
||||||
|
assert reg_device.manufacturer == "Mazda"
|
||||||
|
assert reg_device.name == "My Mazda3"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_no_nickname(hass):
|
||||||
|
"""Test creation of the device when vehicle has no nickname."""
|
||||||
|
await init_integration(hass, use_nickname=False)
|
||||||
|
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
reg_device = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, "JM000000000000000")},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD"
|
||||||
|
assert reg_device.manufacturer == "Mazda"
|
||||||
|
assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensors(hass):
|
||||||
|
"""Test creation of the sensors."""
|
||||||
|
await init_integration(hass)
|
||||||
|
|
||||||
|
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
|
# Fuel Remaining Percentage
|
||||||
|
state = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage")
|
||||||
|
assert state
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||||
|
== "My Mazda3 Fuel Remaining Percentage"
|
||||||
|
)
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:gas-station"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
||||||
|
assert state.state == "87.0"
|
||||||
|
entry = entity_registry.async_get("sensor.my_mazda3_fuel_remaining_percentage")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "JM000000000000000_fuel_remaining_percentage"
|
||||||
|
|
||||||
|
# Fuel Distance Remaining
|
||||||
|
state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining")
|
||||||
|
assert state
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel Distance Remaining"
|
||||||
|
)
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:gas-station"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS
|
||||||
|
assert state.state == "381"
|
||||||
|
entry = entity_registry.async_get("sensor.my_mazda3_fuel_distance_remaining")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "JM000000000000000_fuel_distance_remaining"
|
||||||
|
|
||||||
|
# Odometer
|
||||||
|
state = hass.states.get("sensor.my_mazda3_odometer")
|
||||||
|
assert state
|
||||||
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Odometer"
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:speedometer"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS
|
||||||
|
assert state.state == "2796"
|
||||||
|
entry = entity_registry.async_get("sensor.my_mazda3_odometer")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "JM000000000000000_odometer"
|
||||||
|
|
||||||
|
# Front Left Tire Pressure
|
||||||
|
state = hass.states.get("sensor.my_mazda3_front_left_tire_pressure")
|
||||||
|
assert state
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front Left Tire Pressure"
|
||||||
|
)
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
|
||||||
|
assert state.state == "35"
|
||||||
|
entry = entity_registry.async_get("sensor.my_mazda3_front_left_tire_pressure")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "JM000000000000000_front_left_tire_pressure"
|
||||||
|
|
||||||
|
# Front Right Tire Pressure
|
||||||
|
state = hass.states.get("sensor.my_mazda3_front_right_tire_pressure")
|
||||||
|
assert state
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||||
|
== "My Mazda3 Front Right Tire Pressure"
|
||||||
|
)
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
|
||||||
|
assert state.state == "35"
|
||||||
|
entry = entity_registry.async_get("sensor.my_mazda3_front_right_tire_pressure")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "JM000000000000000_front_right_tire_pressure"
|
||||||
|
|
||||||
|
# Rear Left Tire Pressure
|
||||||
|
state = hass.states.get("sensor.my_mazda3_rear_left_tire_pressure")
|
||||||
|
assert state
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Tire Pressure"
|
||||||
|
)
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
|
||||||
|
assert state.state == "33"
|
||||||
|
entry = entity_registry.async_get("sensor.my_mazda3_rear_left_tire_pressure")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "JM000000000000000_rear_left_tire_pressure"
|
||||||
|
|
||||||
|
# Rear Right Tire Pressure
|
||||||
|
state = hass.states.get("sensor.my_mazda3_rear_right_tire_pressure")
|
||||||
|
assert state
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Tire Pressure"
|
||||||
|
)
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
|
||||||
|
assert state.state == "33"
|
||||||
|
entry = entity_registry.async_get("sensor.my_mazda3_rear_right_tire_pressure")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "JM000000000000000_rear_right_tire_pressure"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensors_imperial_units(hass):
|
||||||
|
"""Test that the sensors work properly with imperial units."""
|
||||||
|
hass.config.units = IMPERIAL_SYSTEM
|
||||||
|
|
||||||
|
await init_integration(hass)
|
||||||
|
|
||||||
|
# Fuel Distance Remaining
|
||||||
|
state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining")
|
||||||
|
assert state
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES
|
||||||
|
assert state.state == "237"
|
||||||
|
|
||||||
|
# Odometer
|
||||||
|
state = hass.states.get("sensor.my_mazda3_odometer")
|
||||||
|
assert state
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES
|
||||||
|
assert state.state == "1737"
|
37
tests/fixtures/mazda/get_vehicle_status.json
vendored
Normal file
37
tests/fixtures/mazda/get_vehicle_status.json
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"lastUpdatedTimestamp": "20210123143809",
|
||||||
|
"latitude": 1.234567,
|
||||||
|
"longitude": -2.345678,
|
||||||
|
"positionTimestamp": "20210123143808",
|
||||||
|
"fuelRemainingPercent": 87.0,
|
||||||
|
"fuelDistanceRemainingKm": 380.8,
|
||||||
|
"odometerKm": 2795.8,
|
||||||
|
"doors": {
|
||||||
|
"driverDoorOpen": false,
|
||||||
|
"passengerDoorOpen": false,
|
||||||
|
"rearLeftDoorOpen": false,
|
||||||
|
"rearRightDoorOpen": false,
|
||||||
|
"trunkOpen": false,
|
||||||
|
"hoodOpen": false,
|
||||||
|
"fuelLidOpen": false
|
||||||
|
},
|
||||||
|
"doorLocks": {
|
||||||
|
"driverDoorUnlocked": false,
|
||||||
|
"passengerDoorUnlocked": false,
|
||||||
|
"rearLeftDoorUnlocked": false,
|
||||||
|
"rearRightDoorUnlocked": false
|
||||||
|
},
|
||||||
|
"windows": {
|
||||||
|
"driverWindowOpen": false,
|
||||||
|
"passengerWindowOpen": false,
|
||||||
|
"rearLeftWindowOpen": false,
|
||||||
|
"rearRightWindowOpen": false
|
||||||
|
},
|
||||||
|
"hazardLightsOn": false,
|
||||||
|
"tirePressure": {
|
||||||
|
"frontLeftTirePressurePsi": 35.0,
|
||||||
|
"frontRightTirePressurePsi": 35.0,
|
||||||
|
"rearLeftTirePressurePsi": 33.0,
|
||||||
|
"rearRightTirePressurePsi": 33.0
|
||||||
|
}
|
||||||
|
}
|
17
tests/fixtures/mazda/get_vehicles.json
vendored
Normal file
17
tests/fixtures/mazda/get_vehicles.json
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"vin": "JM000000000000000",
|
||||||
|
"id": 12345,
|
||||||
|
"nickname": "My Mazda3",
|
||||||
|
"carlineCode": "M3S",
|
||||||
|
"carlineName": "MAZDA3 2.5 S SE AWD",
|
||||||
|
"modelYear": "2021",
|
||||||
|
"modelCode": "M3S SE XA",
|
||||||
|
"modelName": "W/ SELECT PKG AWD SDN",
|
||||||
|
"automaticTransmission": true,
|
||||||
|
"interiorColorCode": "BY3",
|
||||||
|
"interiorColorName": "BLACK",
|
||||||
|
"exteriorColorCode": "42M",
|
||||||
|
"exteriorColorName": "DEEP CRYSTAL BLUE MICA"
|
||||||
|
}
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user