diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 23f790c896b..8e25e08dc47 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -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 diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 27c6f0f5097..409ff572094 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.2.2"], + "requirements": ["pymazda==0.3.0"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index adcb7499aab..d94a4798630 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -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, + ), ] diff --git a/requirements_all.txt b/requirements_all.txt index 1399d9c8f7d..2998f33bb77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d93c4917fca..a106d6f0575 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/mazda/__init__.py b/tests/components/mazda/__init__.py index 7a81a9224d7..9d20f78bc00 100644 --- a/tests/components/mazda/__init__.py +++ b/tests/components/mazda/__init__.py @@ -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() diff --git a/tests/components/mazda/fixtures/get_ev_vehicle_status.json b/tests/components/mazda/fixtures/get_ev_vehicle_status.json new file mode 100644 index 00000000000..6aeaa1ebda0 --- /dev/null +++ b/tests/components/mazda/fixtures/get_ev_vehicle_status.json @@ -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 + } +} diff --git a/tests/components/mazda/fixtures/get_vehicle_status.json b/tests/components/mazda/fixtures/get_vehicle_status.json index f170b222b31..1e74d7202ca 100644 --- a/tests/components/mazda/fixtures/get_vehicle_status.json +++ b/tests/components/mazda/fixtures/get_vehicle_status.json @@ -34,4 +34,4 @@ "rearLeftTirePressurePsi": 33.0, "rearRightTirePressurePsi": 33.0 } -} \ No newline at end of file +} diff --git a/tests/components/mazda/fixtures/get_vehicles.json b/tests/components/mazda/fixtures/get_vehicles.json index 871eeb9d2ec..887ae1194c5 100644 --- a/tests/components/mazda/fixtures/get_vehicles.json +++ b/tests/components/mazda/fixtures/get_vehicles.json @@ -12,6 +12,7 @@ "interiorColorCode": "BY3", "interiorColorName": "BLACK", "exteriorColorCode": "42M", - "exteriorColorName": "DEEP CRYSTAL BLUE MICA" + "exteriorColorName": "DEEP CRYSTAL BLUE MICA", + "isElectric": false } -] \ No newline at end of file +] diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index 8b135f15e80..e2d4661d36f 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -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) diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index 179ad96d533..8d4085930dd 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -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"