Add electric vehicle sensors to Mazda integration (#64099)

This commit is contained in:
Brandon Rothweiler 2022-01-15 14:05:06 -05:00 committed by GitHub
parent 9d0b73bd99
commit bc17616720
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 186 additions and 10 deletions

View File

@ -155,6 +155,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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"])
)
hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles
return vehicles

View File

@ -3,7 +3,7 @@
"name": "Mazda Connected Services",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mazda",
"requirements": ["pymazda==0.2.2"],
"requirements": ["pymazda==0.3.0"],
"codeowners": ["@bdr99"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"

View File

@ -4,7 +4,12 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL,
@ -54,6 +59,20 @@ def _get_distance_unit(unit_system):
return LENGTH_KILOMETERS
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
@ -74,6 +93,22 @@ def _rear_right_tire_pressure_supported(data):
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, unit_system):
"""Get the fuel distance remaining value."""
return round(
@ -106,13 +141,28 @@ def _rear_right_tire_pressure_value(data, unit_system):
return round(data["status"]["tirePressure"]["rearRightTirePressurePsi"])
def _ev_charge_level_value(data, unit_system):
"""Get the charge level value."""
return round(data["evStatus"]["chargeInfo"]["batteryLevelPercentage"])
def _ev_remaining_range_value(data, unit_system):
"""Get the remaining range value."""
return round(
unit_system.length(
data["evStatus"]["chargeInfo"]["drivingRangeKm"], LENGTH_KILOMETERS
)
)
SENSOR_ENTITIES = [
MazdaSensorEntityDescription(
key="fuel_remaining_percentage",
name_suffix="Fuel Remaining Percentage",
icon="mdi:gas-station",
native_unit_of_measurement=PERCENTAGE,
is_supported=lambda data: data["status"]["fuelRemainingPercent"] is not None,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_fuel_remaining_percentage_supported,
value=lambda data, unit_system: data["status"]["fuelRemainingPercent"],
),
MazdaSensorEntityDescription(
@ -120,7 +170,8 @@ SENSOR_ENTITIES = [
name_suffix="Fuel Distance Remaining",
icon="mdi:gas-station",
unit=_get_distance_unit,
is_supported=lambda data: data["status"]["fuelDistanceRemainingKm"] is not None,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_fuel_distance_remaining_supported,
value=_fuel_distance_remaining_value,
),
MazdaSensorEntityDescription(
@ -128,6 +179,7 @@ SENSOR_ENTITIES = [
name_suffix="Odometer",
icon="mdi:speedometer",
unit=_get_distance_unit,
state_class=SensorStateClass.TOTAL_INCREASING,
is_supported=lambda data: data["status"]["odometerKm"] is not None,
value=_odometer_value,
),
@ -136,6 +188,7 @@ SENSOR_ENTITIES = [
name_suffix="Front Left Tire Pressure",
icon="mdi:car-tire-alert",
native_unit_of_measurement=PRESSURE_PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_front_left_tire_pressure_supported,
value=_front_left_tire_pressure_value,
),
@ -144,6 +197,7 @@ SENSOR_ENTITIES = [
name_suffix="Front Right Tire Pressure",
icon="mdi:car-tire-alert",
native_unit_of_measurement=PRESSURE_PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_front_right_tire_pressure_supported,
value=_front_right_tire_pressure_value,
),
@ -152,6 +206,7 @@ SENSOR_ENTITIES = [
name_suffix="Rear Left Tire Pressure",
icon="mdi:car-tire-alert",
native_unit_of_measurement=PRESSURE_PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_rear_left_tire_pressure_supported,
value=_rear_left_tire_pressure_value,
),
@ -160,9 +215,28 @@ SENSOR_ENTITIES = [
name_suffix="Rear Right Tire Pressure",
icon="mdi:car-tire-alert",
native_unit_of_measurement=PRESSURE_PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_rear_right_tire_pressure_supported,
value=_rear_right_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="ev_charge_level",
name_suffix="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",
name_suffix="Remaining Range",
icon="mdi:ev-station",
unit=_get_distance_unit,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_ev_remaining_range_supported,
value=_ev_remaining_range_value,
),
]

View File

@ -1654,7 +1654,7 @@ pymailgunner==1.4
pymata-express==1.19
# homeassistant.components.mazda
pymazda==0.2.2
pymazda==0.3.0
# homeassistant.components.mediaroom
pymediaroom==0.6.4.1

View File

@ -1032,7 +1032,7 @@ pymailgunner==1.4
pymata-express==1.19
# homeassistant.components.mazda
pymazda==0.2.2
pymazda==0.3.0
# homeassistant.components.melcloud
pymelcloud==2.5.6

View File

@ -19,15 +19,22 @@ FIXTURE_USER_INPUT = {
}
async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfigEntry:
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")
)
config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
config_entry.add_to_hass(hass)
@ -42,6 +49,9 @@ 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.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()

View File

@ -0,0 +1,19 @@
{
"chargeInfo": {
"lastUpdatedTimestamp": "20210807083956",
"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

@ -34,4 +34,4 @@
"rearLeftTirePressurePsi": 33.0,
"rearRightTirePressurePsi": 33.0
}
}
}

View File

@ -12,6 +12,7 @@
"interiorColorCode": "BY3",
"interiorColorName": "BLACK",
"exteriorColorCode": "42M",
"exteriorColorName": "DEEP CRYSTAL BLUE MICA"
"exteriorColorName": "DEEP CRYSTAL BLUE MICA",
"isElectric": false
}
]
]

View File

@ -158,6 +158,19 @@ async def test_unload_config_entry(hass: HomeAssistant) -> None:
assert entries[0].state is ConfigEntryState.NOT_LOADED
async def test_init_electric_vehicle(hass):
"""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):
"""Test creation of the device when vehicle has a nickname."""
await init_integration(hass, use_nickname=True)

View File

@ -1,6 +1,12 @@
"""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,
@ -30,6 +36,7 @@ async def test_sensors(hass):
)
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
@ -43,6 +50,7 @@ async def test_sensors(hass):
)
assert state.attributes.get(ATTR_ICON) == "mdi:gas-station"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_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
@ -54,6 +62,7 @@ async def test_sensors(hass):
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Odometer"
assert state.attributes.get(ATTR_ICON) == "mdi:speedometer"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING
assert state.state == "2796"
entry = entity_registry.async_get("sensor.my_mazda3_odometer")
assert entry
@ -67,6 +76,7 @@ async def test_sensors(hass):
)
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "35"
entry = entity_registry.async_get("sensor.my_mazda3_front_left_tire_pressure")
assert entry
@ -81,6 +91,7 @@ async def test_sensors(hass):
)
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "35"
entry = entity_registry.async_get("sensor.my_mazda3_front_right_tire_pressure")
assert entry
@ -94,6 +105,7 @@ async def test_sensors(hass):
)
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "33"
entry = entity_registry.async_get("sensor.my_mazda3_rear_left_tire_pressure")
assert entry
@ -107,6 +119,7 @@ async def test_sensors(hass):
)
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "33"
entry = entity_registry.async_get("sensor.my_mazda3_rear_right_tire_pressure")
assert entry
@ -130,3 +143,43 @@ async def test_sensors_imperial_units(hass):
assert state
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES
assert state.state == "1737"
async def test_electric_vehicle_sensors(hass):
"""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_UNIT_OF_MEASUREMENT) == LENGTH_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"