Add lock platform to the Mazda integration (#50548)
parent
e8d7d96231
commit
122741b914
|
@ -28,7 +28,7 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["device_tracker", "sensor"]
|
||||
PLATFORMS = ["device_tracker", "lock", "sensor"]
|
||||
|
||||
|
||||
async def with_timeout(task, timeout_seconds=10):
|
||||
|
@ -117,26 +117,31 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
class MazdaEntity(CoordinatorEntity):
|
||||
"""Defines a base Mazda entity."""
|
||||
|
||||
def __init__(self, coordinator, index):
|
||||
def __init__(self, client, coordinator, index):
|
||||
"""Initialize the Mazda entity."""
|
||||
super().__init__(coordinator)
|
||||
self.client = client
|
||||
self.index = index
|
||||
self.vin = self.coordinator.data[self.index]["vin"]
|
||||
self.vehicle_id = self.coordinator.data[self.index]["id"]
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""Shortcut to access coordinator data for the entity."""
|
||||
return self.coordinator.data[self.index]
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info for the Mazda entity."""
|
||||
data = self.coordinator.data[self.index]
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.vin)},
|
||||
"name": self.get_vehicle_name(),
|
||||
"manufacturer": "Mazda",
|
||||
"model": f"{data['modelYear']} {data['carlineName']}",
|
||||
"model": f"{self.data['modelYear']} {self.data['carlineName']}",
|
||||
}
|
||||
|
||||
def get_vehicle_name(self):
|
||||
"""Return the vehicle name, to be used as a prefix for names of other entities."""
|
||||
data = self.coordinator.data[self.index]
|
||||
if "nickname" in data and len(data["nickname"]) > 0:
|
||||
return data["nickname"]
|
||||
return f"{data['modelYear']} {data['carlineName']}"
|
||||
if "nickname" in self.data and len(self.data["nickname"]) > 0:
|
||||
return self.data["nickname"]
|
||||
return f"{self.data['modelYear']} {self.data['carlineName']}"
|
||||
|
|
|
@ -3,17 +3,18 @@ from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
|||
from homeassistant.components.device_tracker.config_entry import TrackerEntity
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_COORDINATOR, DOMAIN
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""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(coordinator, index))
|
||||
entities.append(MazdaDeviceTracker(client, coordinator, index))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
@ -50,9 +51,9 @@ class MazdaDeviceTracker(MazdaEntity, TrackerEntity):
|
|||
@property
|
||||
def latitude(self):
|
||||
"""Return latitude value of the device."""
|
||||
return self.coordinator.data[self.index]["status"]["latitude"]
|
||||
return self.data["status"]["latitude"]
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
"""Return longitude value of the device."""
|
||||
return self.coordinator.data[self.index]["status"]["longitude"]
|
||||
return self.data["status"]["longitude"]
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
"""Platform for Mazda lock integration."""
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""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."""
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
vehicle_name = self.get_vehicle_name()
|
||||
return f"{vehicle_name} Lock"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this entity."""
|
||||
return self.vin
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if lock is locked."""
|
||||
return self.client.get_assumed_lock_state(self.vehicle_id)
|
||||
|
||||
async def async_lock(self, **kwargs):
|
||||
"""Lock the vehicle doors."""
|
||||
await self.client.lock_doors(self.vehicle_id)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_unlock(self, **kwargs):
|
||||
"""Unlock the vehicle doors."""
|
||||
await self.client.unlock_doors(self.vehicle_id)
|
||||
|
||||
self.async_write_ha_state()
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Mazda Connected Services",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||
"requirements": ["pymazda==0.0.9"],
|
||||
"requirements": ["pymazda==0.1.5"],
|
||||
"codeowners": ["@bdr99"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling"
|
||||
|
|
|
@ -9,23 +9,24 @@ from homeassistant.const import (
|
|||
)
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_COORDINATOR, DOMAIN
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""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 = []
|
||||
|
||||
for index, _ in enumerate(coordinator.data):
|
||||
entities.append(MazdaFuelRemainingSensor(coordinator, index))
|
||||
entities.append(MazdaFuelDistanceSensor(coordinator, index))
|
||||
entities.append(MazdaOdometerSensor(coordinator, index))
|
||||
entities.append(MazdaFrontLeftTirePressureSensor(coordinator, index))
|
||||
entities.append(MazdaFrontRightTirePressureSensor(coordinator, index))
|
||||
entities.append(MazdaRearLeftTirePressureSensor(coordinator, index))
|
||||
entities.append(MazdaRearRightTirePressureSensor(coordinator, index))
|
||||
entities.append(MazdaFuelRemainingSensor(client, coordinator, index))
|
||||
entities.append(MazdaFuelDistanceSensor(client, coordinator, index))
|
||||
entities.append(MazdaOdometerSensor(client, coordinator, index))
|
||||
entities.append(MazdaFrontLeftTirePressureSensor(client, coordinator, index))
|
||||
entities.append(MazdaFrontRightTirePressureSensor(client, coordinator, index))
|
||||
entities.append(MazdaRearLeftTirePressureSensor(client, coordinator, index))
|
||||
entities.append(MazdaRearRightTirePressureSensor(client, coordinator, index))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
@ -57,7 +58,7 @@ class MazdaFuelRemainingSensor(MazdaEntity, SensorEntity):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.data[self.index]["status"]["fuelRemainingPercent"]
|
||||
return self.data["status"]["fuelRemainingPercent"]
|
||||
|
||||
|
||||
class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity):
|
||||
|
@ -89,9 +90,7 @@ class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
fuel_distance_km = self.coordinator.data[self.index]["status"][
|
||||
"fuelDistanceRemainingKm"
|
||||
]
|
||||
fuel_distance_km = self.data["status"]["fuelDistanceRemainingKm"]
|
||||
return (
|
||||
None
|
||||
if fuel_distance_km is None
|
||||
|
@ -130,7 +129,7 @@ class MazdaOdometerSensor(MazdaEntity, SensorEntity):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
odometer_km = self.coordinator.data[self.index]["status"]["odometerKm"]
|
||||
odometer_km = self.data["status"]["odometerKm"]
|
||||
return (
|
||||
None
|
||||
if odometer_km is None
|
||||
|
@ -165,9 +164,7 @@ class MazdaFrontLeftTirePressureSensor(MazdaEntity, SensorEntity):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][
|
||||
"frontLeftTirePressurePsi"
|
||||
]
|
||||
tire_pressure = self.data["status"]["tirePressure"]["frontLeftTirePressurePsi"]
|
||||
return None if tire_pressure is None else round(tire_pressure)
|
||||
|
||||
|
||||
|
@ -198,9 +195,7 @@ class MazdaFrontRightTirePressureSensor(MazdaEntity, SensorEntity):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][
|
||||
"frontRightTirePressurePsi"
|
||||
]
|
||||
tire_pressure = self.data["status"]["tirePressure"]["frontRightTirePressurePsi"]
|
||||
return None if tire_pressure is None else round(tire_pressure)
|
||||
|
||||
|
||||
|
@ -231,9 +226,7 @@ class MazdaRearLeftTirePressureSensor(MazdaEntity, SensorEntity):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][
|
||||
"rearLeftTirePressurePsi"
|
||||
]
|
||||
tire_pressure = self.data["status"]["tirePressure"]["rearLeftTirePressurePsi"]
|
||||
return None if tire_pressure is None else round(tire_pressure)
|
||||
|
||||
|
||||
|
@ -264,7 +257,5 @@ class MazdaRearRightTirePressureSensor(MazdaEntity, SensorEntity):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][
|
||||
"rearRightTirePressurePsi"
|
||||
]
|
||||
tire_pressure = self.data["status"]["tirePressure"]["rearRightTirePressurePsi"]
|
||||
return None if tire_pressure is None else round(tire_pressure)
|
||||
|
|
|
@ -1539,7 +1539,7 @@ pymailgunner==1.4
|
|||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.0.9
|
||||
pymazda==0.1.5
|
||||
|
||||
# homeassistant.components.mediaroom
|
||||
pymediaroom==0.6.4.1
|
||||
|
|
|
@ -853,7 +853,7 @@ pymailgunner==1.4
|
|||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.0.9
|
||||
pymazda==0.1.5
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
pymelcloud==2.5.2
|
||||
|
|
|
@ -42,6 +42,8 @@ async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfig
|
|||
)
|
||||
client_mock.get_vehicles = AsyncMock(return_value=get_vehicles_fixture)
|
||||
client_mock.get_vehicle_status = AsyncMock(return_value=get_vehicle_status_fixture)
|
||||
client_mock.lock_doors = AsyncMock()
|
||||
client_mock.unlock_doors = AsyncMock()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mazda.config_flow.MazdaAPI",
|
||||
|
@ -50,4 +52,4 @@ async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfig
|
|||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
return client_mock
|
||||
|
|
|
@ -12,7 +12,12 @@ from homeassistant.config_entries import (
|
|||
ENTRY_STATE_SETUP_ERROR,
|
||||
ENTRY_STATE_SETUP_RETRY,
|
||||
)
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
|
||||
from homeassistant.const import (
|
||||
CONF_EMAIL,
|
||||
CONF_PASSWORD,
|
||||
CONF_REGION,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
@ -102,14 +107,57 @@ async def test_update_auth_failure(hass: HomeAssistant):
|
|||
assert flows[0]["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_update_general_failure(hass: HomeAssistant):
|
||||
"""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 == ENTRY_STATE_LOADED
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
|
||||
side_effect=Exception("Unknown exception"),
|
||||
):
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=61))
|
||||
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."""
|
||||
entry = await init_integration(hass)
|
||||
await init_integration(hass)
|
||||
assert hass.data[DOMAIN]
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state == ENTRY_STATE_LOADED
|
||||
|
||||
await hass.config_entries.async_unload(entries[0].entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state == ENTRY_STATE_NOT_LOADED
|
||||
assert entries[0].state == ENTRY_STATE_NOT_LOADED
|
||||
|
||||
|
||||
async def test_device_nickname(hass):
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
"""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.helpers import entity_registry as er
|
||||
|
||||
from tests.components.mazda import init_integration
|
||||
|
||||
|
||||
async def test_lock_setup(hass):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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()
|
Loading…
Reference in New Issue