mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +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/mastodon/* @fabaff
|
||||
homeassistant/components/matrix/* @tinloaf
|
||||
homeassistant/components/mazda/* @bdr99
|
||||
homeassistant/components/mcp23017/* @jardiamj
|
||||
homeassistant/components/media_source/* @hunterjm
|
||||
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",
|
||||
"lyric",
|
||||
"mailgun",
|
||||
"mazda",
|
||||
"melcloud",
|
||||
"met",
|
||||
"meteo_france",
|
||||
|
@ -1517,6 +1517,9 @@ pymailgunner==1.4
|
||||
# homeassistant.components.firmata
|
||||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.0.8
|
||||
|
||||
# homeassistant.components.mediaroom
|
||||
pymediaroom==0.6.4.1
|
||||
|
||||
|
@ -789,6 +789,9 @@ pymailgunner==1.4
|
||||
# homeassistant.components.firmata
|
||||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.0.8
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
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