diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index f6e31fa4357..c34dfa10682 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -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']}" diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py index ea05d2c8c8b..ffe36a1215e 100644 --- a/homeassistant/components/mazda/device_tracker.py +++ b/homeassistant/components/mazda/device_tracker.py @@ -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"] diff --git a/homeassistant/components/mazda/lock.py b/homeassistant/components/mazda/lock.py new file mode 100644 index 00000000000..fb485bd7b13 --- /dev/null +++ b/homeassistant/components/mazda/lock.py @@ -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() diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 9c5fb2c6b46..4ca4384e952 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -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" diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 7382347e6de..673c965544b 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -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) diff --git a/requirements_all.txt b/requirements_all.txt index bdd9b8a1d21..1eee20651cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bddd762ff90..0bef6802cc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/mazda/__init__.py b/tests/components/mazda/__init__.py index f7a267a5110..9676b2b5765 100644 --- a/tests/components/mazda/__init__.py +++ b/tests/components/mazda/__init__.py @@ -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 diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index 1b062dd84f1..c8c631b48af 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -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): diff --git a/tests/components/mazda/test_lock.py b/tests/components/mazda/test_lock.py new file mode 100644 index 00000000000..1230e624cdd --- /dev/null +++ b/tests/components/mazda/test_lock.py @@ -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()