Add services for Mazda integration (#51016)
parent
a8a13da793
commit
a36935dcee
homeassistant/components/mazda
tests/components/mazda
|
@ -11,12 +11,18 @@ from pymazda import (
|
|||
MazdaException,
|
||||
MazdaTokenExpiredException,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import aiohttp_client, device_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
|
@ -24,7 +30,7 @@ from homeassistant.helpers.update_coordinator import (
|
|||
)
|
||||
from homeassistant.util.async_ import gather_with_concurrency
|
||||
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -59,6 +65,77 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
_LOGGER.error("Error occurred during Mazda login request: %s", ex)
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
async def async_handle_service_call(service_call=None):
|
||||
"""Handle a service call."""
|
||||
# Get device entry from device registry
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
device_id = service_call.data.get("device_id")
|
||||
device_entry = dev_reg.async_get(device_id)
|
||||
|
||||
# Get vehicle VIN from device identifiers
|
||||
mazda_identifiers = [
|
||||
identifier
|
||||
for identifier in device_entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
]
|
||||
vin_identifier = next(iter(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]
|
||||
|
||||
if vehicle_id == 0 or api_client is None:
|
||||
raise HomeAssistantError("Vehicle ID not found")
|
||||
|
||||
api_method = getattr(api_client, service_call.service)
|
||||
try:
|
||||
if service_call.service == "send_poi":
|
||||
latitude = service_call.data.get("latitude")
|
||||
longitude = service_call.data.get("longitude")
|
||||
poi_name = service_call.data.get("poi_name")
|
||||
await api_method(vehicle_id, latitude, longitude, poi_name)
|
||||
else:
|
||||
await api_method(vehicle_id)
|
||||
except Exception as ex:
|
||||
_LOGGER.exception("Error occurred during Mazda service call: %s", 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 = device_registry.async_get(hass)
|
||||
device_entry = dev_reg.async_get(device_id)
|
||||
|
||||
if device_entry is None:
|
||||
raise vol.Invalid("Invalid device ID")
|
||||
|
||||
mazda_identifiers = [
|
||||
identifier
|
||||
for identifier in device_entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
]
|
||||
if len(mazda_identifiers) < 1:
|
||||
raise vol.Invalid("Device ID is not a Mazda vehicle")
|
||||
|
||||
return device_id
|
||||
|
||||
service_schema = vol.Schema(
|
||||
{vol.Required("device_id"): vol.All(cv.string, validate_mazda_device_id)}
|
||||
)
|
||||
|
||||
service_schema_send_poi = service_schema.extend(
|
||||
{
|
||||
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:
|
||||
|
@ -73,6 +150,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
for vehicle, status in zip(vehicles, statuses):
|
||||
vehicle["status"] = status
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles
|
||||
|
||||
return vehicles
|
||||
except MazdaAuthenticationException as ex:
|
||||
raise ConfigEntryAuthFailed("Not authenticated with Mazda API") from ex
|
||||
|
@ -94,6 +173,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_CLIENT: mazda_client,
|
||||
DATA_COORDINATOR: coordinator,
|
||||
DATA_VEHICLES: [],
|
||||
}
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
|
@ -102,12 +182,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
# Setup components
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
# Register services
|
||||
for service in SERVICES:
|
||||
if service == "send_poi":
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
service,
|
||||
async_handle_service_call,
|
||||
schema=service_schema_send_poi,
|
||||
)
|
||||
else:
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_handle_service_call, schema=service_schema
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
# Only remove services if it is the last config entry
|
||||
if len(hass.data[DOMAIN]) == 1:
|
||||
for service in SERVICES:
|
||||
hass.services.async_remove(DOMAIN, service)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
|
|
|
@ -4,5 +4,16 @@ DOMAIN = "mazda"
|
|||
|
||||
DATA_CLIENT = "mazda_client"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
DATA_VEHICLES = "vehicles"
|
||||
|
||||
MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"}
|
||||
|
||||
SERVICES = [
|
||||
"send_poi",
|
||||
"start_charging",
|
||||
"start_engine",
|
||||
"stop_charging",
|
||||
"stop_engine",
|
||||
"turn_off_hazard_lights",
|
||||
"turn_on_hazard_lights",
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Mazda Connected Services",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||
"requirements": ["pymazda==0.1.5"],
|
||||
"requirements": ["pymazda==0.1.6"],
|
||||
"codeowners": ["@bdr99"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling"
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
start_engine:
|
||||
name: Start engine
|
||||
description: Start the vehicle engine.
|
||||
fields:
|
||||
device_id:
|
||||
name: Vehicle
|
||||
description: The vehicle to start
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: mazda
|
||||
stop_engine:
|
||||
name: Stop engine
|
||||
description: Stop the vehicle engine.
|
||||
fields:
|
||||
device_id:
|
||||
name: Vehicle
|
||||
description: The vehicle to stop
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: mazda
|
||||
turn_on_hazard_lights:
|
||||
name: Turn on hazard lights
|
||||
description: Turn on the vehicle hazard lights. The lights will flash briefly and then turn off.
|
||||
fields:
|
||||
device_id:
|
||||
name: Vehicle
|
||||
description: The vehicle to turn hazard lights on
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: mazda
|
||||
turn_off_hazard_lights:
|
||||
name: Turn off hazard lights
|
||||
description: Turn off the vehicle hazard lights if they have been manually turned on from inside the vehicle.
|
||||
fields:
|
||||
device_id:
|
||||
name: Vehicle
|
||||
description: The vehicle to turn hazard lights off
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: mazda
|
||||
send_poi:
|
||||
name: Send POI
|
||||
description: Send 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
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: mazda
|
||||
latitude:
|
||||
name: Latitude
|
||||
description: The latitude of the location to send
|
||||
example: 12.34567
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: -90
|
||||
max: 90
|
||||
unit_of_measurement: °
|
||||
mode: box
|
||||
longitude:
|
||||
name: Longitude
|
||||
description: The longitude of the location to send
|
||||
example: -34.56789
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: -180
|
||||
max: 180
|
||||
unit_of_measurement: °
|
||||
mode: box
|
||||
poi_name:
|
||||
name: POI name
|
||||
description: A friendly name for the location
|
||||
example: Work
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
start_charging:
|
||||
name: Start charging
|
||||
description: Start charging the vehicle. For electric vehicles only.
|
||||
fields:
|
||||
device_id:
|
||||
name: Vehicle
|
||||
description: The vehicle to start charging
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: mazda
|
||||
stop_charging:
|
||||
name: Stop charging
|
||||
description: Stop charging the vehicle. For electric vehicles only.
|
||||
fields:
|
||||
device_id:
|
||||
name: Vehicle
|
||||
description: The vehicle to stop charging
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: mazda
|
|
@ -1548,7 +1548,7 @@ pymailgunner==1.4
|
|||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.1.5
|
||||
pymazda==0.1.6
|
||||
|
||||
# homeassistant.components.mediaroom
|
||||
pymediaroom==0.6.4.1
|
||||
|
|
|
@ -862,7 +862,7 @@ pymailgunner==1.4
|
|||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.1.5
|
||||
pymazda==0.1.6
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
pymelcloud==2.5.2
|
||||
|
|
|
@ -44,6 +44,13 @@ async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfig
|
|||
client_mock.get_vehicle_status = AsyncMock(return_value=get_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()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mazda.config_flow.MazdaAPI",
|
||||
|
|
|
@ -4,8 +4,10 @@ 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.const import DOMAIN, SERVICES
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_EMAIL,
|
||||
|
@ -14,6 +16,7 @@ from homeassistant.const import (
|
|||
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
|
||||
|
||||
|
@ -181,3 +184,130 @@ async def test_device_no_nickname(hass):
|
|||
assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD"
|
||||
assert reg_device.manufacturer == "Mazda"
|
||||
assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD"
|
||||
|
||||
|
||||
async def test_services(hass):
|
||||
"""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")},
|
||||
)
|
||||
device_id = reg_device.id
|
||||
|
||||
for service in SERVICES:
|
||||
service_data = {"device_id": device_id}
|
||||
if service == "send_poi":
|
||||
service_data["latitude"] = 1.2345
|
||||
service_data["longitude"] = 2.3456
|
||||
service_data["poi_name"] = "Work"
|
||||
|
||||
await hass.services.async_call(DOMAIN, service, service_data, blocking=True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
api_method = getattr(client_mock, service)
|
||||
if service == "send_poi":
|
||||
api_method.assert_called_once_with(12345, 1.2345, 2.3456, "Work")
|
||||
else:
|
||||
api_method.assert_called_once_with(12345)
|
||||
|
||||
|
||||
async def test_service_invalid_device_id(hass):
|
||||
"""Test service call when the specified device ID is invalid."""
|
||||
await init_integration(hass)
|
||||
|
||||
with pytest.raises(vol.error.MultipleInvalid) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN, "start_engine", {"device_id": "invalid"}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Invalid device ID" in str(err.value)
|
||||
|
||||
|
||||
async def test_service_device_id_not_mazda_vehicle(hass):
|
||||
"""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_device = device_registry.async_get_or_create(
|
||||
config_entry_id="test_config_entry_id",
|
||||
identifiers={("OTHER_INTEGRATION", "ID_FROM_OTHER_INTEGRATION")},
|
||||
)
|
||||
|
||||
with pytest.raises(vol.error.MultipleInvalid) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN, "start_engine", {"device_id": other_device.id}, 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):
|
||||
"""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, "start_engine", {"device_id": device_id}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert str(err.value) == "Vehicle ID not found"
|
||||
|
||||
|
||||
async def test_service_mazda_api_error(hass):
|
||||
"""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.start_engine",
|
||||
side_effect=MazdaException("Test error"),
|
||||
), pytest.raises(HomeAssistantError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN, "start_engine", {"device_id": device_id}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert str(err.value) == "Test error"
|
||||
|
|
Loading…
Reference in New Issue