Add lock platform to the Mazda integration (#50548)

pull/50601/head
Brandon Rothweiler 2021-05-13 20:52:52 -07:00 committed by GitHub
parent e8d7d96231
commit 122741b914
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 201 additions and 45 deletions

View File

@ -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']}"

View File

@ -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"]

View File

@ -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()

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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()