Remove Mazda integration (#101849)

Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
Brandon Rothweiler 2023-10-12 07:13:44 -04:00 committed by GitHub
parent b70e2f7749
commit 91cf719588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 66 additions and 3582 deletions

View File

@ -738,8 +738,6 @@ build.json @home-assistant/supervisor
/tests/components/matrix/ @PaarthShah
/homeassistant/components/matter/ @home-assistant/matter
/tests/components/matter/ @home-assistant/matter
/homeassistant/components/mazda/ @bdr99
/tests/components/mazda/ @bdr99
/homeassistant/components/meater/ @Sotolotl @emontnemery
/tests/components/meater/ @Sotolotl @emontnemery
/homeassistant/components/medcom_ble/ @elafargue

View File

@ -1,213 +1,26 @@
"""The Mazda Connected Services integration."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from pymazda import (
Client as MazdaAPI,
MazdaAccountLockedException,
MazdaAPIEncryptionException,
MazdaAuthenticationException,
MazdaException,
MazdaTokenExpiredException,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
device_registry as dr,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DATA_VEHICLES, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.DEVICE_TRACKER,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
]
DOMAIN = "mazda"
async def with_timeout(task, timeout_seconds=30):
"""Run an async task with a timeout."""
async with asyncio.timeout(timeout_seconds):
return await task
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
"""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=websession, use_cached_vehicle_list=True
)
try:
await mazda_client.validate_credentials()
except MazdaAuthenticationException as ex:
raise ConfigEntryAuthFailed from ex
except (
MazdaException,
MazdaAccountLockedException,
MazdaTokenExpiredException,
MazdaAPIEncryptionException,
) as ex:
_LOGGER.error("Error occurred during Mazda login request: %s", ex)
raise ConfigEntryNotReady from ex
async def async_handle_service_call(service_call: ServiceCall) -> None:
"""Handle a service call."""
# Get device entry from device registry
dev_reg = dr.async_get(hass)
device_id = service_call.data["device_id"]
device_entry = dev_reg.async_get(device_id)
if TYPE_CHECKING:
# For mypy: it has already been checked in validate_mazda_device_id
assert device_entry
# Get vehicle VIN from device identifiers
mazda_identifiers = (
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
)
vin_identifier = next(mazda_identifiers)
vin = vin_identifier[1]
# Get vehicle ID and API client from hass.data
vehicle_id = 0
api_client = None
for entry_data in hass.data[DOMAIN].values():
for vehicle in entry_data[DATA_VEHICLES]:
if vehicle["vin"] == vin:
vehicle_id = vehicle["id"]
api_client = entry_data[DATA_CLIENT]
break
if vehicle_id == 0 or api_client is None:
raise HomeAssistantError("Vehicle ID not found")
api_method = getattr(api_client, service_call.service)
try:
latitude = service_call.data["latitude"]
longitude = service_call.data["longitude"]
poi_name = service_call.data["poi_name"]
await api_method(vehicle_id, latitude, longitude, poi_name)
except Exception as ex:
raise HomeAssistantError(ex) from ex
def validate_mazda_device_id(device_id):
"""Check that a device ID exists in the registry and has at least one 'mazda' identifier."""
dev_reg = dr.async_get(hass)
if (device_entry := dev_reg.async_get(device_id)) is None:
raise vol.Invalid("Invalid device ID")
mazda_identifiers = [
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
]
if not mazda_identifiers:
raise vol.Invalid("Device ID is not a Mazda vehicle")
return device_id
service_schema_send_poi = vol.Schema(
{
vol.Required("device_id"): vol.All(cv.string, validate_mazda_device_id),
vol.Required("latitude"): cv.latitude,
vol.Required("longitude"): cv.longitude,
vol.Required("poi_name"): cv.string,
}
)
async def async_update_data():
"""Fetch data from Mazda API."""
try:
vehicles = await with_timeout(mazda_client.get_vehicles())
# The Mazda API can throw an error when multiple simultaneous requests are
# made for the same account, so we can only make one request at a time here
for vehicle in vehicles:
vehicle["status"] = await with_timeout(
mazda_client.get_vehicle_status(vehicle["id"])
)
# If vehicle is electric, get additional EV-specific status info
if vehicle["isElectric"]:
vehicle["evStatus"] = await with_timeout(
mazda_client.get_ev_vehicle_status(vehicle["id"])
)
vehicle["hvacSetting"] = await with_timeout(
mazda_client.get_hvac_setting(vehicle["id"])
)
hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles
return vehicles
except MazdaAuthenticationException as ex:
raise ConfigEntryAuthFailed("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(
ir.async_create_issue(
hass,
_LOGGER,
name=DOMAIN,
update_method=async_update_data,
update_interval=timedelta(seconds=180),
)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
DATA_CLIENT: mazda_client,
DATA_COORDINATOR: coordinator,
DATA_REGION: region,
DATA_VEHICLES: [],
}
# Fetch initial data so we have data when entities subscribe
await coordinator.async_config_entry_first_refresh()
# Setup components
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Register services
hass.services.async_register(
DOMAIN,
"send_poi",
async_handle_service_call,
schema=service_schema_send_poi,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"dmca": "https://github.com/github/dmca/blob/master/2023/10/2023-10-10-mazda.md",
"entries": "/config/integrations/integration/mazda",
},
)
return True
@ -215,45 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
# Only remove services if it is the last config entry
if len(hass.data[DOMAIN]) == 1:
hass.services.async_remove(DOMAIN, "send_poi")
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class MazdaEntity(CoordinatorEntity):
"""Defines a base Mazda entity."""
_attr_has_entity_name = True
def __init__(self, client, coordinator, index):
"""Initialize the Mazda entity."""
super().__init__(coordinator)
self.client = client
self.index = index
self.vin = self.data["vin"]
self.vehicle_id = self.data["id"]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.vin)},
manufacturer="Mazda",
model=f"{self.data['modelYear']} {self.data['carlineName']}",
name=self.vehicle_name,
)
@property
def data(self):
"""Shortcut to access coordinator data for the entity."""
return self.coordinator.data[self.index]
@property
def vehicle_name(self):
"""Return the vehicle name, to be used as a prefix for names of other entities."""
if "nickname" in self.data and len(self.data["nickname"]) > 0:
return self.data["nickname"]
return f"{self.data['modelYear']} {self.data['carlineName']}"
return True

View File

@ -1,131 +0,0 @@
"""Platform for Mazda binary sensor integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
@dataclass
class MazdaBinarySensorRequiredKeysMixin:
"""Mixin for required keys."""
# Function to determine the value for this binary sensor, given the coordinator data
value_fn: Callable[[dict[str, Any]], bool]
@dataclass
class MazdaBinarySensorEntityDescription(
BinarySensorEntityDescription, MazdaBinarySensorRequiredKeysMixin
):
"""Describes a Mazda binary sensor entity."""
# Function to determine whether the vehicle supports this binary sensor, given the coordinator data
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
def _plugged_in_supported(data):
"""Determine if 'plugged in' binary sensor is supported."""
return (
data["isElectric"] and data["evStatus"]["chargeInfo"]["pluggedIn"] is not None
)
BINARY_SENSOR_ENTITIES = [
MazdaBinarySensorEntityDescription(
key="driver_door",
translation_key="driver_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="passenger_door",
translation_key="passenger_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="rear_left_door",
translation_key="rear_left_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="rear_right_door",
translation_key="rear_right_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="trunk",
translation_key="trunk",
icon="mdi:car-back",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["trunkOpen"],
),
MazdaBinarySensorEntityDescription(
key="hood",
translation_key="hood",
icon="mdi:car",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["hoodOpen"],
),
MazdaBinarySensorEntityDescription(
key="ev_plugged_in",
translation_key="ev_plugged_in",
device_class=BinarySensorDeviceClass.PLUG,
is_supported=_plugged_in_supported,
value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"],
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
async_add_entities(
MazdaBinarySensorEntity(client, coordinator, index, description)
for index, data in enumerate(coordinator.data)
for description in BINARY_SENSOR_ENTITIES
if description.is_supported(data)
)
class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity):
"""Representation of a Mazda vehicle binary sensor."""
entity_description: MazdaBinarySensorEntityDescription
def __init__(self, client, coordinator, index, description):
"""Initialize Mazda binary sensor."""
super().__init__(client, coordinator, index)
self.entity_description = description
self._attr_unique_id = f"{self.vin}_{description.key}"
@property
def is_on(self):
"""Return the state of the binary sensor."""
return self.entity_description.value_fn(self.data)

View File

@ -1,150 +0,0 @@
"""Platform for Mazda button integration."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from pymazda import (
Client as MazdaAPIClient,
MazdaAccountLockedException,
MazdaAPIEncryptionException,
MazdaAuthenticationException,
MazdaException,
MazdaLoginFailedException,
MazdaTokenExpiredException,
)
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def handle_button_press(
client: MazdaAPIClient,
key: str,
vehicle_id: int,
coordinator: DataUpdateCoordinator,
) -> None:
"""Handle a press for a Mazda button entity."""
api_method = getattr(client, key)
try:
await api_method(vehicle_id)
except (
MazdaException,
MazdaAuthenticationException,
MazdaAccountLockedException,
MazdaTokenExpiredException,
MazdaAPIEncryptionException,
MazdaLoginFailedException,
) as ex:
raise HomeAssistantError(ex) from ex
async def handle_refresh_vehicle_status(
client: MazdaAPIClient,
key: str,
vehicle_id: int,
coordinator: DataUpdateCoordinator,
) -> None:
"""Handle a request to refresh the vehicle status."""
await handle_button_press(client, key, vehicle_id, coordinator)
await coordinator.async_request_refresh()
@dataclass
class MazdaButtonEntityDescription(ButtonEntityDescription):
"""Describes a Mazda button entity."""
# Function to determine whether the vehicle supports this button,
# given the coordinator data
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
async_press: Callable[
[MazdaAPIClient, str, int, DataUpdateCoordinator], Awaitable
] = handle_button_press
BUTTON_ENTITIES = [
MazdaButtonEntityDescription(
key="start_engine",
translation_key="start_engine",
icon="mdi:engine",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="stop_engine",
translation_key="stop_engine",
icon="mdi:engine-off",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="turn_on_hazard_lights",
translation_key="turn_on_hazard_lights",
icon="mdi:hazard-lights",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="turn_off_hazard_lights",
translation_key="turn_off_hazard_lights",
icon="mdi:hazard-lights",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="refresh_vehicle_status",
translation_key="refresh_vehicle_status",
icon="mdi:refresh",
async_press=handle_refresh_vehicle_status,
is_supported=lambda data: data["isElectric"],
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the button platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
async_add_entities(
MazdaButtonEntity(client, coordinator, index, description)
for index, data in enumerate(coordinator.data)
for description in BUTTON_ENTITIES
if description.is_supported(data)
)
class MazdaButtonEntity(MazdaEntity, ButtonEntity):
"""Representation of a Mazda button."""
entity_description: MazdaButtonEntityDescription
def __init__(
self,
client: MazdaAPIClient,
coordinator: DataUpdateCoordinator,
index: int,
description: MazdaButtonEntityDescription,
) -> None:
"""Initialize Mazda button."""
super().__init__(client, coordinator, index)
self.entity_description = description
self._attr_unique_id = f"{self.vin}_{description.key}"
async def async_press(self) -> None:
"""Press the button."""
await self.entity_description.async_press(
self.client, self.entity_description.key, self.vehicle_id, self.coordinator
)

View File

@ -1,187 +0,0 @@
"""Platform for Mazda climate integration."""
from __future__ import annotations
from typing import Any
from pymazda import Client as MazdaAPIClient
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_HALVES,
PRECISION_WHOLE,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.unit_conversion import TemperatureConverter
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DOMAIN
PRESET_DEFROSTER_OFF = "Defroster Off"
PRESET_DEFROSTER_FRONT = "Front Defroster"
PRESET_DEFROSTER_REAR = "Rear Defroster"
PRESET_DEFROSTER_FRONT_AND_REAR = "Front and Rear Defroster"
def _front_defroster_enabled(preset_mode: str | None) -> bool:
return preset_mode in [
PRESET_DEFROSTER_FRONT_AND_REAR,
PRESET_DEFROSTER_FRONT,
]
def _rear_defroster_enabled(preset_mode: str | None) -> bool:
return preset_mode in [
PRESET_DEFROSTER_FRONT_AND_REAR,
PRESET_DEFROSTER_REAR,
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the climate platform."""
entry_data = hass.data[DOMAIN][config_entry.entry_id]
client = entry_data[DATA_CLIENT]
coordinator = entry_data[DATA_COORDINATOR]
region = entry_data[DATA_REGION]
async_add_entities(
MazdaClimateEntity(client, coordinator, index, region)
for index, data in enumerate(coordinator.data)
if data["isElectric"]
)
class MazdaClimateEntity(MazdaEntity, ClimateEntity):
"""Class for a Mazda climate entity."""
_attr_translation_key = "climate"
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
_attr_preset_modes = [
PRESET_DEFROSTER_OFF,
PRESET_DEFROSTER_FRONT,
PRESET_DEFROSTER_REAR,
PRESET_DEFROSTER_FRONT_AND_REAR,
]
def __init__(
self,
client: MazdaAPIClient,
coordinator: DataUpdateCoordinator,
index: int,
region: str,
) -> None:
"""Initialize Mazda climate entity."""
super().__init__(client, coordinator, index)
self.region = region
self._attr_unique_id = self.vin
if self.data["hvacSetting"]["temperatureUnit"] == "F":
self._attr_precision = PRECISION_WHOLE
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
self._attr_min_temp = 61.0
self._attr_max_temp = 83.0
else:
self._attr_precision = PRECISION_HALVES
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
if region == "MJO":
self._attr_min_temp = 18.5
self._attr_max_temp = 31.5
else:
self._attr_min_temp = 15.5
self._attr_max_temp = 28.5
self._update_state_attributes()
@callback
def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator data updates."""
self._update_state_attributes()
super()._handle_coordinator_update()
def _update_state_attributes(self) -> None:
# Update the HVAC mode
hvac_on = self.client.get_assumed_hvac_mode(self.vehicle_id)
self._attr_hvac_mode = HVACMode.HEAT_COOL if hvac_on else HVACMode.OFF
# Update the target temperature
hvac_setting = self.client.get_assumed_hvac_setting(self.vehicle_id)
self._attr_target_temperature = hvac_setting.get("temperature")
# Update the current temperature
current_temperature_celsius = self.data["evStatus"]["hvacInfo"][
"interiorTemperatureCelsius"
]
if self.data["hvacSetting"]["temperatureUnit"] == "F":
self._attr_current_temperature = TemperatureConverter.convert(
current_temperature_celsius,
UnitOfTemperature.CELSIUS,
UnitOfTemperature.FAHRENHEIT,
)
else:
self._attr_current_temperature = current_temperature_celsius
# Update the preset mode based on the state of the front and rear defrosters
front_defroster = hvac_setting.get("frontDefroster")
rear_defroster = hvac_setting.get("rearDefroster")
if front_defroster and rear_defroster:
self._attr_preset_mode = PRESET_DEFROSTER_FRONT_AND_REAR
elif front_defroster:
self._attr_preset_mode = PRESET_DEFROSTER_FRONT
elif rear_defroster:
self._attr_preset_mode = PRESET_DEFROSTER_REAR
else:
self._attr_preset_mode = PRESET_DEFROSTER_OFF
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set a new HVAC mode."""
if hvac_mode == HVACMode.HEAT_COOL:
await self.client.turn_on_hvac(self.vehicle_id)
elif hvac_mode == HVACMode.OFF:
await self.client.turn_off_hvac(self.vehicle_id)
self._handle_coordinator_update()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set a new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
precision = self.precision
rounded_temperature = round(temperature / precision) * precision
await self.client.set_hvac_setting(
self.vehicle_id,
rounded_temperature,
self.data["hvacSetting"]["temperatureUnit"],
_front_defroster_enabled(self._attr_preset_mode),
_rear_defroster_enabled(self._attr_preset_mode),
)
self._handle_coordinator_update()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Turn on/off the front/rear defrosters according to the chosen preset mode."""
await self.client.set_hvac_setting(
self.vehicle_id,
self._attr_target_temperature,
self.data["hvacSetting"]["temperatureUnit"],
_front_defroster_enabled(preset_mode),
_rear_defroster_enabled(preset_mode),
)
self._handle_coordinator_update()

View File

@ -1,110 +1,11 @@
"""Config flow for Mazda Connected Services integration."""
from collections.abc import Mapping
import logging
from typing import Any
"""The Mazda Connected Services integration."""
import aiohttp
from pymazda import (
Client as MazdaAPI,
MazdaAccountLockedException,
MazdaAuthenticationException,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from .const import DOMAIN, 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),
}
)
from . import DOMAIN
class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class MazdaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Mazda Connected Services."""
VERSION = 1
def __init__(self):
"""Start the mazda config flow."""
self._reauth_entry = None
self._email = None
self._region = None
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
self._email = user_input[CONF_EMAIL]
self._region = user_input[CONF_REGION]
unique_id = user_input[CONF_EMAIL].lower()
await self.async_set_unique_id(unique_id)
if not self._reauth_entry:
self._abort_if_unique_id_configured()
websession = aiohttp_client.async_get_clientsession(self.hass)
mazda_client = MazdaAPI(
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
user_input[CONF_REGION],
websession,
)
try:
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:
if not self._reauth_entry:
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
self.hass.config_entries.async_update_entry(
self._reauth_entry, data=user_input, unique_id=unique_id
)
# Reload the config entry otherwise devices will remain unavailable
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_EMAIL, default=self._email): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_REGION, default=self._region): vol.In(
MAZDA_REGIONS
),
}
),
errors=errors,
)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Perform reauth if the user credentials have changed."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
self._email = entry_data[CONF_EMAIL]
self._region = entry_data[CONF_REGION]
return await self.async_step_user()

View File

@ -1,10 +0,0 @@
"""Constants for the Mazda Connected Services integration."""
DOMAIN = "mazda"
DATA_CLIENT = "mazda_client"
DATA_COORDINATOR = "coordinator"
DATA_REGION = "region"
DATA_VEHICLES = "vehicles"
MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"}

View File

@ -1,54 +0,0 @@
"""Platform for Mazda device tracker integration."""
from homeassistant.components.device_tracker import SourceType, TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the device tracker platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
entities = []
for index, _ in enumerate(coordinator.data):
entities.append(MazdaDeviceTracker(client, coordinator, index))
async_add_entities(entities)
class MazdaDeviceTracker(MazdaEntity, TrackerEntity):
"""Class for the device tracker."""
_attr_translation_key = "device_tracker"
_attr_icon = "mdi:car"
_attr_force_update = False
def __init__(self, client, coordinator, index) -> None:
"""Initialize Mazda device tracker."""
super().__init__(client, coordinator, index)
self._attr_unique_id = self.vin
@property
def source_type(self) -> SourceType:
"""Return the source type, eg gps or router, of the device."""
return SourceType.GPS
@property
def latitude(self):
"""Return latitude value of the device."""
return self.data["status"]["latitude"]
@property
def longitude(self):
"""Return longitude value of the device."""
return self.data["status"]["longitude"]

View File

@ -1,57 +0,0 @@
"""Diagnostics support for the Mazda integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DATA_COORDINATOR, DOMAIN
TO_REDACT_INFO = [CONF_EMAIL, CONF_PASSWORD]
TO_REDACT_DATA = ["vin", "id", "latitude", "longitude"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
diagnostics_data = {
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
"data": [
async_redact_data(vehicle, TO_REDACT_DATA) for vehicle in coordinator.data
],
}
return diagnostics_data
async def async_get_device_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
vin = next(iter(device.identifiers))[1]
target_vehicle = None
for vehicle in coordinator.data:
if vehicle["vin"] == vin:
target_vehicle = vehicle
break
if target_vehicle is None:
raise HomeAssistantError("Vehicle not found")
diagnostics_data = {
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
"data": async_redact_data(target_vehicle, TO_REDACT_DATA),
}
return diagnostics_data

View File

@ -1,58 +0,0 @@
"""Platform for Mazda lock integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the lock platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
entities = []
for index, _ in enumerate(coordinator.data):
entities.append(MazdaLock(client, coordinator, index))
async_add_entities(entities)
class MazdaLock(MazdaEntity, LockEntity):
"""Class for the lock."""
_attr_translation_key = "lock"
def __init__(self, client, coordinator, index) -> None:
"""Initialize Mazda lock."""
super().__init__(client, coordinator, index)
self._attr_unique_id = self.vin
@property
def is_locked(self) -> bool | None:
"""Return true if lock is locked."""
return self.client.get_assumed_lock_state(self.vehicle_id)
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the vehicle doors."""
await self.client.lock_doors(self.vehicle_id)
self.async_write_ha_state()
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the vehicle doors."""
await self.client.unlock_doors(self.vehicle_id)
self.async_write_ha_state()

View File

@ -1,11 +1,9 @@
{
"domain": "mazda",
"name": "Mazda Connected Services",
"codeowners": ["@bdr99"],
"config_flow": true,
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/mazda",
"integration_type": "system",
"iot_class": "cloud_polling",
"loggers": ["pymazda"],
"quality_scale": "platinum",
"requirements": ["pymazda==0.3.11"]
"requirements": []
}

View File

@ -1,263 +0,0 @@
"""Platform for Mazda sensor integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfPressure
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
@dataclass
class MazdaSensorRequiredKeysMixin:
"""Mixin for required keys."""
# Function to determine the value for this sensor, given the coordinator data
# and the configured unit system
value: Callable[[dict[str, Any]], StateType]
@dataclass
class MazdaSensorEntityDescription(
SensorEntityDescription, MazdaSensorRequiredKeysMixin
):
"""Describes a Mazda sensor entity."""
# Function to determine whether the vehicle supports this sensor,
# given the coordinator data
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
def _fuel_remaining_percentage_supported(data):
"""Determine if fuel remaining percentage is supported."""
return (not data["isElectric"]) and (
data["status"]["fuelRemainingPercent"] is not None
)
def _fuel_distance_remaining_supported(data):
"""Determine if fuel distance remaining is supported."""
return (not data["isElectric"]) and (
data["status"]["fuelDistanceRemainingKm"] is not None
)
def _front_left_tire_pressure_supported(data):
"""Determine if front left tire pressure is supported."""
return data["status"]["tirePressure"]["frontLeftTirePressurePsi"] is not None
def _front_right_tire_pressure_supported(data):
"""Determine if front right tire pressure is supported."""
return data["status"]["tirePressure"]["frontRightTirePressurePsi"] is not None
def _rear_left_tire_pressure_supported(data):
"""Determine if rear left tire pressure is supported."""
return data["status"]["tirePressure"]["rearLeftTirePressurePsi"] is not None
def _rear_right_tire_pressure_supported(data):
"""Determine if rear right tire pressure is supported."""
return data["status"]["tirePressure"]["rearRightTirePressurePsi"] is not None
def _ev_charge_level_supported(data):
"""Determine if charge level is supported."""
return (
data["isElectric"]
and data["evStatus"]["chargeInfo"]["batteryLevelPercentage"] is not None
)
def _ev_remaining_range_supported(data):
"""Determine if remaining range is supported."""
return (
data["isElectric"]
and data["evStatus"]["chargeInfo"]["drivingRangeKm"] is not None
)
def _fuel_distance_remaining_value(data):
"""Get the fuel distance remaining value."""
return round(data["status"]["fuelDistanceRemainingKm"])
def _odometer_value(data):
"""Get the odometer value."""
# In order to match the behavior of the Mazda mobile app, we always round down
return int(data["status"]["odometerKm"])
def _front_left_tire_pressure_value(data):
"""Get the front left tire pressure value."""
return round(data["status"]["tirePressure"]["frontLeftTirePressurePsi"])
def _front_right_tire_pressure_value(data):
"""Get the front right tire pressure value."""
return round(data["status"]["tirePressure"]["frontRightTirePressurePsi"])
def _rear_left_tire_pressure_value(data):
"""Get the rear left tire pressure value."""
return round(data["status"]["tirePressure"]["rearLeftTirePressurePsi"])
def _rear_right_tire_pressure_value(data):
"""Get the rear right tire pressure value."""
return round(data["status"]["tirePressure"]["rearRightTirePressurePsi"])
def _ev_charge_level_value(data):
"""Get the charge level value."""
return round(data["evStatus"]["chargeInfo"]["batteryLevelPercentage"])
def _ev_remaining_range_value(data):
"""Get the remaining range value."""
return round(data["evStatus"]["chargeInfo"]["drivingRangeKm"])
SENSOR_ENTITIES = [
MazdaSensorEntityDescription(
key="fuel_remaining_percentage",
translation_key="fuel_remaining_percentage",
icon="mdi:gas-station",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_fuel_remaining_percentage_supported,
value=lambda data: data["status"]["fuelRemainingPercent"],
),
MazdaSensorEntityDescription(
key="fuel_distance_remaining",
translation_key="fuel_distance_remaining",
icon="mdi:gas-station",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_fuel_distance_remaining_supported,
value=_fuel_distance_remaining_value,
),
MazdaSensorEntityDescription(
key="odometer",
translation_key="odometer",
icon="mdi:speedometer",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.TOTAL_INCREASING,
is_supported=lambda data: data["status"]["odometerKm"] is not None,
value=_odometer_value,
),
MazdaSensorEntityDescription(
key="front_left_tire_pressure",
translation_key="front_left_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_front_left_tire_pressure_supported,
value=_front_left_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="front_right_tire_pressure",
translation_key="front_right_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_front_right_tire_pressure_supported,
value=_front_right_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="rear_left_tire_pressure",
translation_key="rear_left_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_rear_left_tire_pressure_supported,
value=_rear_left_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="rear_right_tire_pressure",
translation_key="rear_right_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_rear_right_tire_pressure_supported,
value=_rear_right_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="ev_charge_level",
translation_key="ev_charge_level",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_ev_charge_level_supported,
value=_ev_charge_level_value,
),
MazdaSensorEntityDescription(
key="ev_remaining_range",
translation_key="ev_remaining_range",
icon="mdi:ev-station",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_ev_remaining_range_supported,
value=_ev_remaining_range_value,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
entities: list[SensorEntity] = []
for index, data in enumerate(coordinator.data):
for description in SENSOR_ENTITIES:
if description.is_supported(data):
entities.append(
MazdaSensorEntity(client, coordinator, index, description)
)
async_add_entities(entities)
class MazdaSensorEntity(MazdaEntity, SensorEntity):
"""Representation of a Mazda vehicle sensor."""
entity_description: MazdaSensorEntityDescription
def __init__(self, client, coordinator, index, description):
"""Initialize Mazda sensor."""
super().__init__(client, coordinator, index)
self.entity_description = description
self._attr_unique_id = f"{self.vin}_{description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value(self.data)

View File

@ -1,30 +0,0 @@
send_poi:
fields:
device_id:
required: true
selector:
device:
integration: mazda
latitude:
example: 12.34567
required: true
selector:
number:
min: -90
max: 90
unit_of_measurement: °
mode: box
longitude:
example: -34.56789
required: true
selector:
number:
min: -180
max: 180
unit_of_measurement: °
mode: box
poi_name:
example: Work
required: true
selector:
text:

View File

@ -1,139 +1,8 @@
{
"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": {
"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."
}
}
},
"entity": {
"binary_sensor": {
"driver_door": {
"name": "Driver door"
},
"passenger_door": {
"name": "Passenger door"
},
"rear_left_door": {
"name": "Rear left door"
},
"rear_right_door": {
"name": "Rear right door"
},
"trunk": {
"name": "Trunk"
},
"hood": {
"name": "Hood"
},
"ev_plugged_in": {
"name": "Plugged in"
}
},
"button": {
"start_engine": {
"name": "Start engine"
},
"stop_engine": {
"name": "Stop engine"
},
"turn_on_hazard_lights": {
"name": "Turn on hazard lights"
},
"turn_off_hazard_lights": {
"name": "Turn off hazard lights"
},
"refresh_vehicle_status": {
"name": "Refresh status"
}
},
"climate": {
"climate": {
"name": "[%key:component::climate::title%]"
}
},
"device_tracker": {
"device_tracker": {
"name": "[%key:component::device_tracker::title%]"
}
},
"lock": {
"lock": {
"name": "[%key:component::lock::title%]"
}
},
"sensor": {
"fuel_remaining_percentage": {
"name": "Fuel remaining percentage"
},
"fuel_distance_remaining": {
"name": "Fuel distance remaining"
},
"odometer": {
"name": "Odometer"
},
"front_left_tire_pressure": {
"name": "Front left tire pressure"
},
"front_right_tire_pressure": {
"name": "Front right tire pressure"
},
"rear_left_tire_pressure": {
"name": "Rear left tire pressure"
},
"rear_right_tire_pressure": {
"name": "Rear right tire pressure"
},
"ev_charge_level": {
"name": "Charge level"
},
"ev_remaining_range": {
"name": "Remaining range"
}
},
"switch": {
"charging": {
"name": "Charging"
}
}
},
"services": {
"send_poi": {
"name": "Send POI",
"description": "Sends a GPS location to the vehicle's navigation system as a POI (Point of Interest). Requires a navigation SD card installed in the vehicle.",
"fields": {
"device_id": {
"name": "Vehicle",
"description": "The vehicle to send the GPS location to."
},
"latitude": {
"name": "[%key:common::config_flow::data::latitude%]",
"description": "The latitude of the location to send."
},
"longitude": {
"name": "[%key:common::config_flow::data::longitude%]",
"description": "The longitude of the location to send."
},
"poi_name": {
"name": "POI name",
"description": "A friendly name for the location."
}
}
"issues": {
"integration_removed": {
"title": "The Mazda integration has been removed",
"description": "The Mazda integration has been removed from Home Assistant.\n\nThe library that Home Assistant uses to connect with their services, [has been taken offline by Mazda]({dmca}).\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Mazda integration entries]({entries})."
}
}
}

View File

@ -1,72 +0,0 @@
"""Platform for Mazda switch integration."""
from typing import Any
from pymazda import Client as MazdaAPIClient
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the switch platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
async_add_entities(
MazdaChargingSwitch(client, coordinator, index)
for index, data in enumerate(coordinator.data)
if data["isElectric"]
)
class MazdaChargingSwitch(MazdaEntity, SwitchEntity):
"""Class for the charging switch."""
_attr_translation_key = "charging"
_attr_icon = "mdi:ev-station"
def __init__(
self,
client: MazdaAPIClient,
coordinator: DataUpdateCoordinator,
index: int,
) -> None:
"""Initialize Mazda charging switch."""
super().__init__(client, coordinator, index)
self._attr_unique_id = self.vin
@property
def is_on(self):
"""Return true if the vehicle is charging."""
return self.data["evStatus"]["chargeInfo"]["charging"]
async def refresh_status_and_write_state(self):
"""Request a status update, retrieve it through the coordinator, and write the state."""
await self.client.refresh_vehicle_status(self.vehicle_id)
await self.coordinator.async_request_refresh()
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Start charging the vehicle."""
await self.client.start_charging(self.vehicle_id)
await self.refresh_status_and_write_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Stop charging the vehicle."""
await self.client.stop_charging(self.vehicle_id)
await self.refresh_status_and_write_state()

View File

@ -271,7 +271,6 @@ FLOWS = {
"lyric",
"mailgun",
"matter",
"mazda",
"meater",
"medcom_ble",
"melcloud",

View File

@ -3262,12 +3262,6 @@
"config_flow": true,
"iot_class": "local_push"
},
"mazda": {
"name": "Mazda Connected Services",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"meater": {
"name": "Meater",
"integration_type": "hub",

View File

@ -1847,9 +1847,6 @@ pymailgunner==1.4
# homeassistant.components.firmata
pymata-express==1.19
# homeassistant.components.mazda
pymazda==0.3.11
# homeassistant.components.mediaroom
pymediaroom==0.6.5.4

View File

@ -1390,9 +1390,6 @@ pymailgunner==1.4
# homeassistant.components.firmata
pymata-express==1.19
# homeassistant.components.mazda
pymazda==0.3.11
# homeassistant.components.melcloud
pymelcloud==2.5.8

View File

@ -1,80 +1 @@
"""Tests for the Mazda Connected Services integration."""
import json
from unittest.mock import AsyncMock, MagicMock, Mock, 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, electric_vehicle=False
) -> 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")
if electric_vehicle:
get_vehicles_fixture[0]["isElectric"] = True
get_vehicle_status_fixture = json.loads(
load_fixture("mazda/get_vehicle_status.json")
)
get_ev_vehicle_status_fixture = json.loads(
load_fixture("mazda/get_ev_vehicle_status.json")
)
get_hvac_setting_fixture = json.loads(load_fixture("mazda/get_hvac_setting.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)
client_mock.get_ev_vehicle_status = AsyncMock(
return_value=get_ev_vehicle_status_fixture
)
client_mock.lock_doors = AsyncMock()
client_mock.unlock_doors = AsyncMock()
client_mock.send_poi = AsyncMock()
client_mock.start_charging = AsyncMock()
client_mock.start_engine = AsyncMock()
client_mock.stop_charging = AsyncMock()
client_mock.stop_engine = AsyncMock()
client_mock.turn_off_hazard_lights = AsyncMock()
client_mock.turn_on_hazard_lights = AsyncMock()
client_mock.refresh_vehicle_status = AsyncMock()
client_mock.get_hvac_setting = AsyncMock(return_value=get_hvac_setting_fixture)
client_mock.get_assumed_hvac_setting = Mock(return_value=get_hvac_setting_fixture)
client_mock.get_assumed_hvac_mode = Mock(return_value=True)
client_mock.set_hvac_setting = AsyncMock()
client_mock.turn_on_hvac = AsyncMock()
client_mock.turn_off_hvac = AsyncMock()
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 client_mock

View File

@ -1,62 +0,0 @@
{
"info": {
"email": "**REDACTED**",
"password": "**REDACTED**",
"region": "MNAO"
},
"data": [
{
"vin": "**REDACTED**",
"id": "**REDACTED**",
"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",
"isElectric": false,
"status": {
"lastUpdatedTimestamp": "20210123143809",
"latitude": "**REDACTED**",
"longitude": "**REDACTED**",
"positionTimestamp": "20210123143808",
"fuelRemainingPercent": 87.0,
"fuelDistanceRemainingKm": 380.8,
"odometerKm": 2795.8,
"doors": {
"driverDoorOpen": false,
"passengerDoorOpen": true,
"rearLeftDoorOpen": false,
"rearRightDoorOpen": false,
"trunkOpen": false,
"hoodOpen": true,
"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
}
}
}
]
}

View File

@ -1,60 +0,0 @@
{
"info": {
"email": "**REDACTED**",
"password": "**REDACTED**",
"region": "MNAO"
},
"data": {
"vin": "**REDACTED**",
"id": "**REDACTED**",
"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",
"isElectric": false,
"status": {
"lastUpdatedTimestamp": "20210123143809",
"latitude": "**REDACTED**",
"longitude": "**REDACTED**",
"positionTimestamp": "20210123143808",
"fuelRemainingPercent": 87.0,
"fuelDistanceRemainingKm": 380.8,
"odometerKm": 2795.8,
"doors": {
"driverDoorOpen": false,
"passengerDoorOpen": true,
"rearLeftDoorOpen": false,
"rearRightDoorOpen": false,
"trunkOpen": false,
"hoodOpen": true,
"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
}
}
}
}

View File

@ -1,19 +0,0 @@
{
"lastUpdatedTimestamp": "20210807083956",
"chargeInfo": {
"batteryLevelPercentage": 80,
"drivingRangeKm": 218,
"pluggedIn": true,
"charging": true,
"basicChargeTimeMinutes": 30,
"quickChargeTimeMinutes": 15,
"batteryHeaterAuto": true,
"batteryHeaterOn": true
},
"hvacInfo": {
"hvacOn": true,
"frontDefroster": false,
"rearDefroster": false,
"interiorTemperatureCelsius": 15.1
}
}

View File

@ -1,6 +0,0 @@
{
"temperature": 20,
"temperatureUnit": "C",
"frontDefroster": true,
"rearDefroster": false
}

View File

@ -1,37 +0,0 @@
{
"lastUpdatedTimestamp": "20210123143809",
"latitude": 1.234567,
"longitude": -2.345678,
"positionTimestamp": "20210123143808",
"fuelRemainingPercent": 87.0,
"fuelDistanceRemainingKm": 380.8,
"odometerKm": 2795.8,
"doors": {
"driverDoorOpen": false,
"passengerDoorOpen": true,
"rearLeftDoorOpen": false,
"rearRightDoorOpen": false,
"trunkOpen": false,
"hoodOpen": true,
"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
}
}

View File

@ -1,18 +0,0 @@
[
{
"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",
"isElectric": false
}
]

View File

@ -1,98 +0,0 @@
"""The binary sensor tests for the Mazda Connected Services integration."""
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_binary_sensors(hass: HomeAssistant) -> None:
"""Test creation of the binary sensors."""
await init_integration(hass)
entity_registry = er.async_get(hass)
# Driver Door
state = hass.states.get("binary_sensor.my_mazda3_driver_door")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver door"
assert state.attributes.get(ATTR_ICON) == "mdi:car-door"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "off"
entry = entity_registry.async_get("binary_sensor.my_mazda3_driver_door")
assert entry
assert entry.unique_id == "JM000000000000000_driver_door"
# Passenger Door
state = hass.states.get("binary_sensor.my_mazda3_passenger_door")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger door"
assert state.attributes.get(ATTR_ICON) == "mdi:car-door"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "on"
entry = entity_registry.async_get("binary_sensor.my_mazda3_passenger_door")
assert entry
assert entry.unique_id == "JM000000000000000_passenger_door"
# Rear Left Door
state = hass.states.get("binary_sensor.my_mazda3_rear_left_door")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left door"
assert state.attributes.get(ATTR_ICON) == "mdi:car-door"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "off"
entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_left_door")
assert entry
assert entry.unique_id == "JM000000000000000_rear_left_door"
# Rear Right Door
state = hass.states.get("binary_sensor.my_mazda3_rear_right_door")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right door"
assert state.attributes.get(ATTR_ICON) == "mdi:car-door"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "off"
entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_right_door")
assert entry
assert entry.unique_id == "JM000000000000000_rear_right_door"
# Trunk
state = hass.states.get("binary_sensor.my_mazda3_trunk")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Trunk"
assert state.attributes.get(ATTR_ICON) == "mdi:car-back"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "off"
entry = entity_registry.async_get("binary_sensor.my_mazda3_trunk")
assert entry
assert entry.unique_id == "JM000000000000000_trunk"
# Hood
state = hass.states.get("binary_sensor.my_mazda3_hood")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Hood"
assert state.attributes.get(ATTR_ICON) == "mdi:car"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "on"
entry = entity_registry.async_get("binary_sensor.my_mazda3_hood")
assert entry
assert entry.unique_id == "JM000000000000000_hood"
async def test_electric_vehicle_binary_sensors(hass: HomeAssistant) -> None:
"""Test sensors which are specific to electric vehicles."""
await init_integration(hass, electric_vehicle=True)
entity_registry = er.async_get(hass)
# Plugged In
state = hass.states.get("binary_sensor.my_mazda3_plugged_in")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged in"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG
assert state.state == "on"
entry = entity_registry.async_get("binary_sensor.my_mazda3_plugged_in")
assert entry
assert entry.unique_id == "JM000000000000000_ev_plugged_in"

View File

@ -1,145 +0,0 @@
"""The button tests for the Mazda Connected Services integration."""
from pymazda import MazdaException
import pytest
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_button_setup_non_electric_vehicle(hass: HomeAssistant) -> None:
"""Test creation of button entities."""
await init_integration(hass)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("button.my_mazda3_start_engine")
assert entry
assert entry.unique_id == "JM000000000000000_start_engine"
state = hass.states.get("button.my_mazda3_start_engine")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine"
assert state.attributes.get(ATTR_ICON) == "mdi:engine"
entry = entity_registry.async_get("button.my_mazda3_stop_engine")
assert entry
assert entry.unique_id == "JM000000000000000_stop_engine"
state = hass.states.get("button.my_mazda3_stop_engine")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine"
assert state.attributes.get(ATTR_ICON) == "mdi:engine-off"
entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights")
assert entry
assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights"
state = hass.states.get("button.my_mazda3_turn_on_hazard_lights")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights"
assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights"
entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights")
assert entry
assert entry.unique_id == "JM000000000000000_turn_off_hazard_lights"
state = hass.states.get("button.my_mazda3_turn_off_hazard_lights")
assert state
assert (
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights"
)
assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights"
# Since this is a non-electric vehicle, electric vehicle buttons should not be created
entry = entity_registry.async_get("button.my_mazda3_refresh_vehicle_status")
assert entry is None
state = hass.states.get("button.my_mazda3_refresh_vehicle_status")
assert state is None
async def test_button_setup_electric_vehicle(hass: HomeAssistant) -> None:
"""Test creation of button entities for an electric vehicle."""
await init_integration(hass, electric_vehicle=True)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("button.my_mazda3_refresh_status")
assert entry
assert entry.unique_id == "JM000000000000000_refresh_vehicle_status"
state = hass.states.get("button.my_mazda3_refresh_status")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh status"
assert state.attributes.get(ATTR_ICON) == "mdi:refresh"
@pytest.mark.parametrize(
("electric_vehicle", "entity_id_suffix"),
[
(True, "start_engine"),
(True, "stop_engine"),
(True, "turn_on_hazard_lights"),
(True, "turn_off_hazard_lights"),
(False, "refresh_status"),
],
)
async def test_button_not_created(
hass: HomeAssistant, electric_vehicle, entity_id_suffix
) -> None:
"""Test that button entities are not created when they should not be."""
await init_integration(hass, electric_vehicle=electric_vehicle)
entity_registry = er.async_get(hass)
entity_id = f"button.my_mazda3_{entity_id_suffix}"
entry = entity_registry.async_get(entity_id)
assert entry is None
state = hass.states.get(entity_id)
assert state is None
@pytest.mark.parametrize(
("electric_vehicle", "entity_id_suffix", "api_method_name"),
[
(False, "start_engine", "start_engine"),
(False, "stop_engine", "stop_engine"),
(False, "turn_on_hazard_lights", "turn_on_hazard_lights"),
(False, "turn_off_hazard_lights", "turn_off_hazard_lights"),
(True, "refresh_status", "refresh_vehicle_status"),
],
)
async def test_button_press(
hass: HomeAssistant, electric_vehicle, entity_id_suffix, api_method_name
) -> None:
"""Test pressing the button entities."""
client_mock = await init_integration(hass, electric_vehicle=electric_vehicle)
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: f"button.my_mazda3_{entity_id_suffix}"},
blocking=True,
)
await hass.async_block_till_done()
api_method = getattr(client_mock, api_method_name)
api_method.assert_called_once_with(12345)
async def test_button_press_error(hass: HomeAssistant) -> None:
"""Test the Mazda API raising an error when a button entity is pressed."""
client_mock = await init_integration(hass)
client_mock.start_engine.side_effect = MazdaException("Test error")
with pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.my_mazda3_start_engine"},
blocking=True,
)
await hass.async_block_till_done()
assert str(err.value) == "Test error"

View File

@ -1,341 +0,0 @@
"""The climate tests for the Mazda Connected Services integration."""
import json
from unittest.mock import patch
import pytest
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ATTR_PRESET_MODE,
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.components.climate.const import (
ATTR_CURRENT_TEMPERATURE,
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_PRESET_MODES,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.components.mazda.climate import (
PRESET_DEFROSTER_FRONT,
PRESET_DEFROSTER_FRONT_AND_REAR,
PRESET_DEFROSTER_OFF,
PRESET_DEFROSTER_REAR,
)
from homeassistant.components.mazda.const import DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
CONF_EMAIL,
CONF_PASSWORD,
CONF_REGION,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from . import init_integration
from tests.common import MockConfigEntry, load_fixture
async def test_climate_setup(hass: HomeAssistant) -> None:
"""Test the setup of the climate entity."""
await init_integration(hass, electric_vehicle=True)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("climate.my_mazda3_climate")
assert entry
assert entry.unique_id == "JM000000000000000"
state = hass.states.get("climate.my_mazda3_climate")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Climate"
@pytest.mark.parametrize(
(
"region",
"hvac_on",
"target_temperature",
"temperature_unit",
"front_defroster",
"rear_defroster",
"current_temperature_celsius",
"expected_hvac_mode",
"expected_preset_mode",
"expected_min_temp",
"expected_max_temp",
),
[
# Test with HVAC off
(
"MNAO",
False,
20,
"C",
False,
False,
22,
HVACMode.OFF,
PRESET_DEFROSTER_OFF,
15.5,
28.5,
),
# Test with HVAC on
(
"MNAO",
True,
20,
"C",
False,
False,
22,
HVACMode.HEAT_COOL,
PRESET_DEFROSTER_OFF,
15.5,
28.5,
),
# Test with front defroster on
(
"MNAO",
False,
20,
"C",
True,
False,
22,
HVACMode.OFF,
PRESET_DEFROSTER_FRONT,
15.5,
28.5,
),
# Test with rear defroster on
(
"MNAO",
False,
20,
"C",
False,
True,
22,
HVACMode.OFF,
PRESET_DEFROSTER_REAR,
15.5,
28.5,
),
# Test with front and rear defrosters on
(
"MNAO",
False,
20,
"C",
True,
True,
22,
HVACMode.OFF,
PRESET_DEFROSTER_FRONT_AND_REAR,
15.5,
28.5,
),
# Test with temperature unit F
(
"MNAO",
False,
70,
"F",
False,
False,
22,
HVACMode.OFF,
PRESET_DEFROSTER_OFF,
61.0,
83.0,
),
# Test with Japan region (uses different min/max temp settings)
(
"MJO",
False,
20,
"C",
False,
False,
22,
HVACMode.OFF,
PRESET_DEFROSTER_OFF,
18.5,
31.5,
),
],
)
async def test_climate_state(
hass: HomeAssistant,
region,
hvac_on,
target_temperature,
temperature_unit,
front_defroster,
rear_defroster,
current_temperature_celsius,
expected_hvac_mode,
expected_preset_mode,
expected_min_temp,
expected_max_temp,
) -> None:
"""Test getting the state of the climate entity."""
if temperature_unit == "F":
hass.config.units = US_CUSTOMARY_SYSTEM
get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json"))
get_vehicles_fixture[0]["isElectric"] = True
get_vehicle_status_fixture = json.loads(
load_fixture("mazda/get_vehicle_status.json")
)
get_ev_vehicle_status_fixture = json.loads(
load_fixture("mazda/get_ev_vehicle_status.json")
)
get_ev_vehicle_status_fixture["hvacInfo"][
"interiorTemperatureCelsius"
] = current_temperature_celsius
get_hvac_setting_fixture = {
"temperature": target_temperature,
"temperatureUnit": temperature_unit,
"frontDefroster": front_defroster,
"rearDefroster": rear_defroster,
}
with patch(
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
return_value=True,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
return_value=get_vehicles_fixture,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicle_status",
return_value=get_vehicle_status_fixture,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_ev_vehicle_status",
return_value=get_ev_vehicle_status_fixture,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_assumed_hvac_mode",
return_value=hvac_on,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_assumed_hvac_setting",
return_value=get_hvac_setting_fixture,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_hvac_setting",
return_value=get_hvac_setting_fixture,
):
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_EMAIL: "example@example.com",
CONF_PASSWORD: "password",
CONF_REGION: region,
},
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.my_mazda3_climate")
assert state
assert state.state == expected_hvac_mode
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Climate"
assert (
state.attributes.get(ATTR_SUPPORTED_FEATURES)
== ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
assert state.attributes.get(ATTR_HVAC_MODES) == [HVACMode.HEAT_COOL, HVACMode.OFF]
assert state.attributes.get(ATTR_PRESET_MODES) == [
PRESET_DEFROSTER_OFF,
PRESET_DEFROSTER_FRONT,
PRESET_DEFROSTER_REAR,
PRESET_DEFROSTER_FRONT_AND_REAR,
]
assert state.attributes.get(ATTR_MIN_TEMP) == expected_min_temp
assert state.attributes.get(ATTR_MAX_TEMP) == expected_max_temp
assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == round(
hass.config.units.temperature(
current_temperature_celsius, UnitOfTemperature.CELSIUS
)
)
assert state.attributes.get(ATTR_TEMPERATURE) == target_temperature
assert state.attributes.get(ATTR_PRESET_MODE) == expected_preset_mode
@pytest.mark.parametrize(
("hvac_mode", "api_method"),
[
(HVACMode.HEAT_COOL, "turn_on_hvac"),
(HVACMode.OFF, "turn_off_hvac"),
],
)
async def test_set_hvac_mode(hass: HomeAssistant, hvac_mode, api_method) -> None:
"""Test turning on and off the HVAC system."""
client_mock = await init_integration(hass, electric_vehicle=True)
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.my_mazda3_climate", ATTR_HVAC_MODE: hvac_mode},
blocking=True,
)
await hass.async_block_till_done()
getattr(client_mock, api_method).assert_called_once_with(12345)
async def test_set_target_temperature(hass: HomeAssistant) -> None:
"""Test setting the target temperature of the climate entity."""
client_mock = await init_integration(hass, electric_vehicle=True)
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: "climate.my_mazda3_climate", ATTR_TEMPERATURE: 22},
blocking=True,
)
await hass.async_block_till_done()
client_mock.set_hvac_setting.assert_called_once_with(12345, 22, "C", True, False)
@pytest.mark.parametrize(
("preset_mode", "front_defroster", "rear_defroster"),
[
(PRESET_DEFROSTER_OFF, False, False),
(PRESET_DEFROSTER_FRONT, True, False),
(PRESET_DEFROSTER_REAR, False, True),
(PRESET_DEFROSTER_FRONT_AND_REAR, True, True),
],
)
async def test_set_preset_mode(
hass: HomeAssistant, preset_mode, front_defroster, rear_defroster
) -> None:
"""Test turning on and off the front and rear defrosters."""
client_mock = await init_integration(hass, electric_vehicle=True)
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{
ATTR_ENTITY_ID: "climate.my_mazda3_climate",
ATTR_PRESET_MODE: preset_mode,
},
blocking=True,
)
await hass.async_block_till_done()
client_mock.set_hvac_setting.assert_called_once_with(
12345, 20, "C", front_defroster, rear_defroster
)

View File

@ -1,423 +0,0 @@
"""Test the Mazda Connected Services config flow."""
from unittest.mock import patch
import aiohttp
from homeassistant import config_entries, data_entry_flow
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",
}
FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL = {
CONF_EMAIL: "example2@example.com",
CONF_PASSWORD: "password_fixed",
CONF_REGION: "MNAO",
}
async def test_form(hass: HomeAssistant) -> None:
"""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.FlowResultType.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_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_entry.mock_calls) == 1
async def test_account_already_exists(hass: HomeAssistant) -> None:
"""Test account already exists."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
return_value=True,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT,
)
await hass.async_block_till_done()
assert result2["type"] == "abort"
assert result2["reason"] == "already_configured"
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.FlowResultType.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.FlowResultType.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.FlowResultType.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.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "account_locked"}
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"""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: HomeAssistant) -> None:
"""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."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=MazdaAuthenticationException("Failed to authenticate"),
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
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": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.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_entry", return_value=True):
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.FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"
async def test_reauth_authorization_error(hass: HomeAssistant) -> None:
"""Test we show user form on authorization error."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=MazdaAuthenticationException("Failed to authenticate"),
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
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.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "invalid_auth"}
async def test_reauth_account_locked(hass: HomeAssistant) -> None:
"""Test we show user form on account_locked error."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=MazdaAccountLockedException("Account locked"),
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
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.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "account_locked"}
async def test_reauth_connection_error(hass: HomeAssistant) -> None:
"""Test we show user form on connection error."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=aiohttp.ClientError,
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
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.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_reauth_unknown_error(hass: HomeAssistant) -> None:
"""Test we show user form on unknown error."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=Exception,
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
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.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "unknown"}
async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None:
"""Test reauth with a new email address but same account."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
return_value=True,
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
# Change the email and ensure the entry and its unique id gets
# updated in the event the user has changed their email with mazda
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL,
)
await hass.async_block_till_done()
assert (
mock_config.unique_id == FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL[CONF_EMAIL]
)
assert result2["type"] == data_entry_flow.FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"

View File

@ -1,30 +0,0 @@
"""The device tracker tests for the Mazda Connected Services integration."""
from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SourceType
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_LATITUDE,
ATTR_LONGITUDE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_device_tracker(hass: HomeAssistant) -> None:
"""Test creation of the device tracker."""
await init_integration(hass)
entity_registry = er.async_get(hass)
state = hass.states.get("device_tracker.my_mazda3_device_tracker")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device tracker"
assert state.attributes.get(ATTR_ICON) == "mdi:car"
assert state.attributes.get(ATTR_LATITUDE) == 1.234567
assert state.attributes.get(ATTR_LONGITUDE) == -2.345678
assert state.attributes.get(ATTR_SOURCE_TYPE) == SourceType.GPS
entry = entity_registry.async_get("device_tracker.my_mazda3_device_tracker")
assert entry
assert entry.unique_id == "JM000000000000000"

View File

@ -1,81 +0,0 @@
"""Test Mazda diagnostics."""
import json
import pytest
from homeassistant.components.mazda.const import DATA_COORDINATOR, DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import init_integration
from tests.common import load_fixture
from tests.components.diagnostics import (
get_diagnostics_for_config_entry,
get_diagnostics_for_device,
)
from tests.typing import ClientSessionGenerator
async def test_config_entry_diagnostics(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test config entry diagnostics."""
await init_integration(hass)
assert hass.data[DOMAIN]
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
diagnostics_fixture = json.loads(
load_fixture("mazda/diagnostics_config_entry.json")
)
assert (
await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
== diagnostics_fixture
)
async def test_device_diagnostics(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test device diagnostics."""
await init_integration(hass)
assert hass.data[DOMAIN]
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
assert reg_device is not None
diagnostics_fixture = json.loads(load_fixture("mazda/diagnostics_device.json"))
assert (
await get_diagnostics_for_device(hass, hass_client, config_entry, reg_device)
== diagnostics_fixture
)
async def test_device_diagnostics_vehicle_not_found(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test device diagnostics when the vehicle cannot be found."""
await init_integration(hass)
assert hass.data[DOMAIN]
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
assert reg_device is not None
# Remove vehicle info from hass.data so that vehicle will not be found
hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR].data = []
with pytest.raises(AssertionError):
await get_diagnostics_for_device(hass, hass_client, config_entry, reg_device)

View File

@ -1,365 +1,50 @@
"""Tests for the Mazda Connected Services integration."""
from datetime import timedelta
import json
from unittest.mock import patch
from pymazda import MazdaAuthenticationException, MazdaException
import pytest
import voluptuous as vol
from homeassistant.components.mazda.const import DOMAIN
from homeassistant.components.mazda import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_EMAIL,
CONF_PASSWORD,
CONF_REGION,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.util import dt as dt_util
from homeassistant.helpers import issue_registry as ir
from . import init_integration
from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
FIXTURE_USER_INPUT = {
CONF_EMAIL: "example@example.com",
CONF_PASSWORD: "password",
CONF_REGION: "MNAO",
}
from tests.common import MockConfigEntry
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 is ConfigEntryState.SETUP_RETRY
async def test_init_auth_failure(hass: HomeAssistant) -> None:
"""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 is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "user"
async def test_update_auth_failure(hass: HomeAssistant) -> None:
"""Test auth failure during data update."""
get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json"))
get_vehicle_status_fixture = json.loads(
load_fixture("mazda/get_vehicle_status.json")
)
with patch(
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
return_value=True,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
return_value=get_vehicles_fixture,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicle_status",
return_value=get_vehicle_status_fixture,
):
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 is ConfigEntryState.LOADED
with patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
side_effect=MazdaAuthenticationException("Login failed"),
):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181))
await hass.async_block_till_done()
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "user"
async def test_update_general_failure(hass: HomeAssistant) -> None:
"""Test general failure during data update."""
get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json"))
get_vehicle_status_fixture = json.loads(
load_fixture("mazda/get_vehicle_status.json")
)
with patch(
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
return_value=True,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
return_value=get_vehicles_fixture,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicle_status",
return_value=get_vehicle_status_fixture,
):
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 is ConfigEntryState.LOADED
with patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
side_effect=Exception("Unknown exception"),
):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181))
await hass.async_block_till_done()
entity = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage")
assert entity is not None
assert entity.state == STATE_UNAVAILABLE
async def test_unload_config_entry(hass: HomeAssistant) -> None:
"""Test the Mazda configuration entry unloading."""
await init_integration(hass)
assert hass.data[DOMAIN]
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(entries[0].entry_id)
await hass.async_block_till_done()
assert entries[0].state is ConfigEntryState.NOT_LOADED
async def test_init_electric_vehicle(hass: HomeAssistant) -> None:
"""Test initialization of the integration with an electric vehicle."""
client_mock = await init_integration(hass, electric_vehicle=True)
client_mock.get_vehicles.assert_called_once()
client_mock.get_vehicle_status.assert_called_once()
client_mock.get_ev_vehicle_status.assert_called_once()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
async def test_device_nickname(hass: HomeAssistant) -> None:
"""Test creation of the device when vehicle has a nickname."""
await init_integration(hass, use_nickname=True)
device_registry = dr.async_get(hass)
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: HomeAssistant) -> None:
"""Test creation of the device when vehicle has no nickname."""
await init_integration(hass, use_nickname=False)
device_registry = dr.async_get(hass)
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"
@pytest.mark.parametrize(
("service", "service_data", "expected_args"),
[
(
"send_poi",
{"latitude": 1.2345, "longitude": 2.3456, "poi_name": "Work"},
[12345, 1.2345, 2.3456, "Work"],
),
],
)
async def test_services(
hass: HomeAssistant, service, service_data, expected_args
async def test_mazda_repair_issue(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test service calls."""
client_mock = await init_integration(hass)
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
"""Test the Mazda configuration entry loading/unloading handles the repair."""
config_entry_1 = MockConfigEntry(
title="Example 1",
domain=DOMAIN,
)
device_id = reg_device.id
config_entry_1.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry_1.entry_id)
await hass.async_block_till_done()
assert config_entry_1.state is ConfigEntryState.LOADED
service_data["device_id"] = device_id
await hass.services.async_call(DOMAIN, service, service_data, blocking=True)
# Add a second one
config_entry_2 = MockConfigEntry(
title="Example 2",
domain=DOMAIN,
)
config_entry_2.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry_2.entry_id)
await hass.async_block_till_done()
api_method = getattr(client_mock, service)
api_method.assert_called_once_with(*expected_args)
assert config_entry_2.state is ConfigEntryState.LOADED
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
# Remove the first one
await hass.config_entries.async_remove(config_entry_1.entry_id)
await hass.async_block_till_done()
async def test_service_invalid_device_id(hass: HomeAssistant) -> None:
"""Test service call when the specified device ID is invalid."""
await init_integration(hass)
assert config_entry_1.state is ConfigEntryState.NOT_LOADED
assert config_entry_2.state is ConfigEntryState.LOADED
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
with pytest.raises(vol.error.MultipleInvalid) as err:
await hass.services.async_call(
DOMAIN,
"send_poi",
{
"device_id": "invalid",
"latitude": 1.2345,
"longitude": 6.7890,
"poi_name": "poi_name",
},
blocking=True,
)
await hass.async_block_till_done()
# Remove the second one
await hass.config_entries.async_remove(config_entry_2.entry_id)
await hass.async_block_till_done()
assert "Invalid device ID" in str(err.value)
async def test_service_device_id_not_mazda_vehicle(hass: HomeAssistant) -> None:
"""Test service call when the specified device ID is not the device ID of a Mazda vehicle."""
await init_integration(hass)
device_registry = dr.async_get(hass)
# Create another device and pass its device ID.
# Service should fail because device is from wrong domain.
other_config_entry = MockConfigEntry()
other_config_entry.add_to_hass(hass)
other_device = device_registry.async_get_or_create(
config_entry_id=other_config_entry.entry_id,
identifiers={("OTHER_INTEGRATION", "ID_FROM_OTHER_INTEGRATION")},
)
with pytest.raises(vol.error.MultipleInvalid) as err:
await hass.services.async_call(
DOMAIN,
"send_poi",
{
"device_id": other_device.id,
"latitude": 1.2345,
"longitude": 6.7890,
"poi_name": "poi_name",
},
blocking=True,
)
await hass.async_block_till_done()
assert "Device ID is not a Mazda vehicle" in str(err.value)
async def test_service_vehicle_id_not_found(hass: HomeAssistant) -> None:
"""Test service call when the vehicle ID is not found."""
await init_integration(hass)
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
device_id = reg_device.id
entries = hass.config_entries.async_entries(DOMAIN)
entry_id = entries[0].entry_id
# Remove vehicle info from hass.data so that vehicle ID will not be found
hass.data[DOMAIN][entry_id]["vehicles"] = []
with pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
DOMAIN,
"send_poi",
{
"device_id": device_id,
"latitude": 1.2345,
"longitude": 6.7890,
"poi_name": "poi_name",
},
blocking=True,
)
await hass.async_block_till_done()
assert str(err.value) == "Vehicle ID not found"
async def test_service_mazda_api_error(hass: HomeAssistant) -> None:
"""Test the Mazda API raising an error when a service is called."""
get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json"))
get_vehicle_status_fixture = json.loads(
load_fixture("mazda/get_vehicle_status.json")
)
with patch(
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
return_value=True,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
return_value=get_vehicles_fixture,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicle_status",
return_value=get_vehicle_status_fixture,
):
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()
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
device_id = reg_device.id
with patch(
"homeassistant.components.mazda.MazdaAPI.send_poi",
side_effect=MazdaException("Test error"),
), pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
DOMAIN,
"send_poi",
{
"device_id": device_id,
"latitude": 1.2345,
"longitude": 6.7890,
"poi_name": "poi_name",
},
blocking=True,
)
await hass.async_block_till_done()
assert str(err.value) == "Test error"
assert config_entry_1.state is ConfigEntryState.NOT_LOADED
assert config_entry_2.state is ConfigEntryState.NOT_LOADED
assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None

View File

@ -1,58 +0,0 @@
"""The lock tests for the Mazda Connected Services integration."""
from homeassistant.components.lock import (
DOMAIN as LOCK_DOMAIN,
SERVICE_LOCK,
SERVICE_UNLOCK,
STATE_LOCKED,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_lock_setup(hass: HomeAssistant) -> None:
"""Test locking and unlocking the vehicle."""
await init_integration(hass)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("lock.my_mazda3_lock")
assert entry
assert entry.unique_id == "JM000000000000000"
state = hass.states.get("lock.my_mazda3_lock")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Lock"
assert state.state == STATE_LOCKED
async def test_locking(hass: HomeAssistant) -> None:
"""Test locking the vehicle."""
client_mock = await init_integration(hass)
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_LOCK,
{ATTR_ENTITY_ID: "lock.my_mazda3_lock"},
blocking=True,
)
await hass.async_block_till_done()
client_mock.lock_doors.assert_called_once()
async def test_unlocking(hass: HomeAssistant) -> None:
"""Test unlocking the vehicle."""
client_mock = await init_integration(hass)
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_UNLOCK,
{ATTR_ENTITY_ID: "lock.my_mazda3_lock"},
blocking=True,
)
await hass.async_block_till_done()
client_mock.unlock_doors.assert_called_once()

View File

@ -1,195 +0,0 @@
"""The sensor tests for the Mazda Connected Services integration."""
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
UnitOfLength,
UnitOfPressure,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from . import init_integration
async def test_sensors(hass: HomeAssistant) -> None:
"""Test creation of the sensors."""
await init_integration(hass)
entity_registry = er.async_get(hass)
# 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.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
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_DEVICE_CLASS) == SensorDeviceClass.DISTANCE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
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_DEVICE_CLASS) == SensorDeviceClass.DISTANCE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING
assert state.state == "2795"
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_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "241"
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_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "241"
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_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "228"
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_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "228"
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_us_customary_units(hass: HomeAssistant) -> None:
"""Test that the sensors work properly with US customary units."""
hass.config.units = US_CUSTOMARY_SYSTEM
await init_integration(hass)
# In the US, miles are used for vehicle odometers.
# These tests verify that the unit conversion logic for the distance
# sensor device class automatically converts the unit to miles.
# Fuel Distance Remaining
state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining")
assert state
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES
assert state.state == "237"
# Odometer
state = hass.states.get("sensor.my_mazda3_odometer")
assert state
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES
assert state.state == "1737"
async def test_electric_vehicle_sensors(hass: HomeAssistant) -> None:
"""Test sensors which are specific to electric vehicles."""
await init_integration(hass, electric_vehicle=True)
entity_registry = er.async_get(hass)
# Fuel Remaining Percentage should not exist for an electric vehicle
entry = entity_registry.async_get("sensor.my_mazda3_fuel_remaining_percentage")
assert entry is None
# Fuel Distance Remaining should not exist for an electric vehicle
entry = entity_registry.async_get("sensor.my_mazda3_fuel_distance_remaining")
assert entry is None
# Charge Level
state = hass.states.get("sensor.my_mazda3_charge_level")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charge level"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "80"
entry = entity_registry.async_get("sensor.my_mazda3_charge_level")
assert entry
assert entry.unique_id == "JM000000000000000_ev_charge_level"
# Remaining Range
state = hass.states.get("sensor.my_mazda3_remaining_range")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Remaining range"
assert state.attributes.get(ATTR_ICON) == "mdi:ev-station"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "218"
entry = entity_registry.async_get("sensor.my_mazda3_remaining_range")
assert entry
assert entry.unique_id == "JM000000000000000_ev_remaining_range"

View File

@ -1,69 +0,0 @@
"""The switch tests for the Mazda Connected Services integration."""
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_switch_setup(hass: HomeAssistant) -> None:
"""Test setup of the switch entity."""
await init_integration(hass, electric_vehicle=True)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("switch.my_mazda3_charging")
assert entry
assert entry.unique_id == "JM000000000000000"
state = hass.states.get("switch.my_mazda3_charging")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charging"
assert state.attributes.get(ATTR_ICON) == "mdi:ev-station"
assert state.state == STATE_ON
async def test_start_charging(hass: HomeAssistant) -> None:
"""Test turning on the charging switch."""
client_mock = await init_integration(hass, electric_vehicle=True)
client_mock.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.my_mazda3_charging"},
blocking=True,
)
await hass.async_block_till_done()
client_mock.start_charging.assert_called_once()
client_mock.refresh_vehicle_status.assert_called_once()
client_mock.get_vehicle_status.assert_called_once()
client_mock.get_ev_vehicle_status.assert_called_once()
async def test_stop_charging(hass: HomeAssistant) -> None:
"""Test turning off the charging switch."""
client_mock = await init_integration(hass, electric_vehicle=True)
client_mock.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.my_mazda3_charging"},
blocking=True,
)
await hass.async_block_till_done()
client_mock.stop_charging.assert_called_once()
client_mock.refresh_vehicle_status.assert_called_once()
client_mock.get_vehicle_status.assert_called_once()
client_mock.get_ev_vehicle_status.assert_called_once()