Add climate platform to Mazda integration (#75037)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Brandon Rothweiler 2022-12-27 15:13:36 -05:00 committed by GitHub
parent 7ef145d4ce
commit f5c56152d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 537 additions and 3 deletions

View File

@ -33,13 +33,14 @@ from homeassistant.helpers.update_coordinator import (
UpdateFailed,
)
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DATA_VEHICLES, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.DEVICE_TRACKER,
Platform.LOCK,
Platform.SENSOR,
@ -161,6 +162,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
vehicle["evStatus"] = await with_timeout(
mazda_client.get_ev_vehicle_status(vehicle["id"])
)
vehicle["hvacSetting"] = await with_timeout(
mazda_client.get_hvac_setting(vehicle["id"])
)
hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles
@ -185,6 +189,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN][entry.entry_id] = {
DATA_CLIENT: mazda_client,
DATA_COORDINATOR: coordinator,
DATA_REGION: region,
DATA_VEHICLES: [],
}

View File

@ -0,0 +1,187 @@
"""Platform for Mazda climate integration."""
from __future__ import annotations
from typing import Any
from pymazda import Client as MazdaAPIClient
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_HALVES,
PRECISION_WHOLE,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.temperature import convert as convert_temperature
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DOMAIN
PRESET_DEFROSTER_OFF = "Defroster Off"
PRESET_DEFROSTER_FRONT = "Front Defroster"
PRESET_DEFROSTER_REAR = "Rear Defroster"
PRESET_DEFROSTER_FRONT_AND_REAR = "Front and Rear Defroster"
def _front_defroster_enabled(preset_mode: str | None) -> bool:
return preset_mode in [
PRESET_DEFROSTER_FRONT_AND_REAR,
PRESET_DEFROSTER_FRONT,
]
def _rear_defroster_enabled(preset_mode: str | None) -> bool:
return preset_mode in [
PRESET_DEFROSTER_FRONT_AND_REAR,
PRESET_DEFROSTER_REAR,
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the climate platform."""
entry_data = hass.data[DOMAIN][config_entry.entry_id]
client = entry_data[DATA_CLIENT]
coordinator = entry_data[DATA_COORDINATOR]
region = entry_data[DATA_REGION]
async_add_entities(
MazdaClimateEntity(client, coordinator, index, region)
for index, data in enumerate(coordinator.data)
if data["isElectric"]
)
class MazdaClimateEntity(MazdaEntity, ClimateEntity):
"""Class for a Mazda climate entity."""
_attr_name = "Climate"
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
_attr_preset_modes = [
PRESET_DEFROSTER_OFF,
PRESET_DEFROSTER_FRONT,
PRESET_DEFROSTER_REAR,
PRESET_DEFROSTER_FRONT_AND_REAR,
]
def __init__(
self,
client: MazdaAPIClient,
coordinator: DataUpdateCoordinator,
index: int,
region: str,
) -> None:
"""Initialize Mazda climate entity."""
super().__init__(client, coordinator, index)
self.region = region
self._attr_unique_id = self.vin
if self.data["hvacSetting"]["temperatureUnit"] == "F":
self._attr_precision = PRECISION_WHOLE
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
self._attr_min_temp = 61.0
self._attr_max_temp = 83.0
else:
self._attr_precision = PRECISION_HALVES
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
if region == "MJO":
self._attr_min_temp = 18.5
self._attr_max_temp = 31.5
else:
self._attr_min_temp = 15.5
self._attr_max_temp = 28.5
self._update_state_attributes()
@callback
def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator data updates."""
self._update_state_attributes()
super()._handle_coordinator_update()
def _update_state_attributes(self) -> None:
# Update the HVAC mode
hvac_on = self.client.get_assumed_hvac_mode(self.vehicle_id)
self._attr_hvac_mode = HVACMode.HEAT_COOL if hvac_on else HVACMode.OFF
# Update the target temperature
hvac_setting = self.client.get_assumed_hvac_setting(self.vehicle_id)
self._attr_target_temperature = hvac_setting.get("temperature")
# Update the current temperature
current_temperature_celsius = self.data["evStatus"]["hvacInfo"][
"interiorTemperatureCelsius"
]
if self.data["hvacSetting"]["temperatureUnit"] == "F":
self._attr_current_temperature = convert_temperature(
current_temperature_celsius,
UnitOfTemperature.CELSIUS,
UnitOfTemperature.FAHRENHEIT,
)
else:
self._attr_current_temperature = current_temperature_celsius
# Update the preset mode based on the state of the front and rear defrosters
front_defroster = hvac_setting.get("frontDefroster")
rear_defroster = hvac_setting.get("rearDefroster")
if front_defroster and rear_defroster:
self._attr_preset_mode = PRESET_DEFROSTER_FRONT_AND_REAR
elif front_defroster:
self._attr_preset_mode = PRESET_DEFROSTER_FRONT
elif rear_defroster:
self._attr_preset_mode = PRESET_DEFROSTER_REAR
else:
self._attr_preset_mode = PRESET_DEFROSTER_OFF
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set a new HVAC mode."""
if hvac_mode == HVACMode.HEAT_COOL:
await self.client.turn_on_hvac(self.vehicle_id)
elif hvac_mode == HVACMode.OFF:
await self.client.turn_off_hvac(self.vehicle_id)
self._handle_coordinator_update()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set a new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
precision = self.precision
rounded_temperature = round(temperature / precision) * precision
await self.client.set_hvac_setting(
self.vehicle_id,
rounded_temperature,
self.data["hvacSetting"]["temperatureUnit"],
_front_defroster_enabled(self._attr_preset_mode),
_rear_defroster_enabled(self._attr_preset_mode),
)
self._handle_coordinator_update()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Turn on/off the front/rear defrosters according to the chosen preset mode."""
await self.client.set_hvac_setting(
self.vehicle_id,
self._attr_target_temperature,
self.data["hvacSetting"]["temperatureUnit"],
_front_defroster_enabled(preset_mode),
_rear_defroster_enabled(preset_mode),
)
self._handle_coordinator_update()

View File

@ -4,6 +4,7 @@ DOMAIN = "mazda"
DATA_CLIENT = "mazda_client"
DATA_COORDINATOR = "coordinator"
DATA_REGION = "region"
DATA_VEHICLES = "vehicles"
MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"}

View File

@ -1,7 +1,7 @@
"""Tests for the Mazda Connected Services integration."""
import json
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from pymazda import Client as MazdaAPI
@ -35,6 +35,7 @@ async def init_integration(
get_ev_vehicle_status_fixture = json.loads(
load_fixture("mazda/get_ev_vehicle_status.json")
)
get_hvac_setting_fixture = json.loads(load_fixture("mazda/get_hvac_setting.json"))
config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
config_entry.add_to_hass(hass)
@ -61,6 +62,13 @@ async def init_integration(
client_mock.stop_engine = AsyncMock()
client_mock.turn_off_hazard_lights = AsyncMock()
client_mock.turn_on_hazard_lights = AsyncMock()
client_mock.refresh_vehicle_status = AsyncMock()
client_mock.get_hvac_setting = AsyncMock(return_value=get_hvac_setting_fixture)
client_mock.get_assumed_hvac_setting = Mock(return_value=get_hvac_setting_fixture)
client_mock.get_assumed_hvac_mode = Mock(return_value=True)
client_mock.set_hvac_setting = AsyncMock()
client_mock.turn_on_hvac = AsyncMock()
client_mock.turn_off_hvac = AsyncMock()
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI",

View File

@ -1,6 +1,6 @@
{
"lastUpdatedTimestamp": "20210807083956",
"chargeInfo": {
"lastUpdatedTimestamp": "20210807083956",
"batteryLevelPercentage": 80,
"drivingRangeKm": 218,
"pluggedIn": true,

View File

@ -0,0 +1,6 @@
{
"temperature": 20,
"temperatureUnit": "C",
"frontDefroster": true,
"rearDefroster": false
}

View File

@ -0,0 +1,327 @@
"""The climate tests for the Mazda Connected Services integration."""
import json
from unittest.mock import patch
import pytest
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ATTR_PRESET_MODE,
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.components.climate.const import (
ATTR_CURRENT_TEMPERATURE,
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_PRESET_MODES,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.components.mazda.climate import (
PRESET_DEFROSTER_FRONT,
PRESET_DEFROSTER_FRONT_AND_REAR,
PRESET_DEFROSTER_OFF,
PRESET_DEFROSTER_REAR,
)
from homeassistant.components.mazda.const import DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
CONF_EMAIL,
CONF_PASSWORD,
CONF_REGION,
UnitOfTemperature,
)
from homeassistant.helpers import entity_registry as er
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from . import init_integration
from tests.common import MockConfigEntry, load_fixture
async def test_climate_setup(hass):
"""Test the setup of the climate entity."""
await init_integration(hass, electric_vehicle=True)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("climate.my_mazda3_climate")
assert entry
assert entry.unique_id == "JM000000000000000"
state = hass.states.get("climate.my_mazda3_climate")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Climate"
@pytest.mark.parametrize(
"region, hvac_on, target_temperature, temperature_unit, front_defroster, rear_defroster, current_temperature_celsius, expected_hvac_mode, expected_preset_mode, expected_min_temp, expected_max_temp",
[
# Test with HVAC off
(
"MNAO",
False,
20,
"C",
False,
False,
22,
HVACMode.OFF,
PRESET_DEFROSTER_OFF,
15.5,
28.5,
),
# Test with HVAC on
(
"MNAO",
True,
20,
"C",
False,
False,
22,
HVACMode.HEAT_COOL,
PRESET_DEFROSTER_OFF,
15.5,
28.5,
),
# Test with front defroster on
(
"MNAO",
False,
20,
"C",
True,
False,
22,
HVACMode.OFF,
PRESET_DEFROSTER_FRONT,
15.5,
28.5,
),
# Test with rear defroster on
(
"MNAO",
False,
20,
"C",
False,
True,
22,
HVACMode.OFF,
PRESET_DEFROSTER_REAR,
15.5,
28.5,
),
# Test with front and rear defrosters on
(
"MNAO",
False,
20,
"C",
True,
True,
22,
HVACMode.OFF,
PRESET_DEFROSTER_FRONT_AND_REAR,
15.5,
28.5,
),
# Test with temperature unit F
(
"MNAO",
False,
70,
"F",
False,
False,
22,
HVACMode.OFF,
PRESET_DEFROSTER_OFF,
61.0,
83.0,
),
# Test with Japan region (uses different min/max temp settings)
(
"MJO",
False,
20,
"C",
False,
False,
22,
HVACMode.OFF,
PRESET_DEFROSTER_OFF,
18.5,
31.5,
),
],
)
async def test_climate_state(
hass,
region,
hvac_on,
target_temperature,
temperature_unit,
front_defroster,
rear_defroster,
current_temperature_celsius,
expected_hvac_mode,
expected_preset_mode,
expected_min_temp,
expected_max_temp,
):
"""Test getting the state of the climate entity."""
if temperature_unit == "F":
hass.config.units = US_CUSTOMARY_SYSTEM
get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json"))
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")
)
get_ev_vehicle_status_fixture["hvacInfo"][
"interiorTemperatureCelsius"
] = current_temperature_celsius
get_hvac_setting_fixture = {
"temperature": target_temperature,
"temperatureUnit": temperature_unit,
"frontDefroster": front_defroster,
"rearDefroster": rear_defroster,
}
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,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_ev_vehicle_status",
return_value=get_ev_vehicle_status_fixture,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_assumed_hvac_mode",
return_value=hvac_on,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_assumed_hvac_setting",
return_value=get_hvac_setting_fixture,
), patch(
"homeassistant.components.mazda.MazdaAPI.get_hvac_setting",
return_value=get_hvac_setting_fixture,
):
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_EMAIL: "example@example.com",
CONF_PASSWORD: "password",
CONF_REGION: region,
},
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.my_mazda3_climate")
assert state
assert state.state == expected_hvac_mode
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Climate"
assert (
state.attributes.get(ATTR_SUPPORTED_FEATURES)
== ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
assert state.attributes.get(ATTR_HVAC_MODES) == [HVACMode.HEAT_COOL, HVACMode.OFF]
assert state.attributes.get(ATTR_PRESET_MODES) == [
PRESET_DEFROSTER_OFF,
PRESET_DEFROSTER_FRONT,
PRESET_DEFROSTER_REAR,
PRESET_DEFROSTER_FRONT_AND_REAR,
]
assert state.attributes.get(ATTR_MIN_TEMP) == expected_min_temp
assert state.attributes.get(ATTR_MAX_TEMP) == expected_max_temp
assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == round(
hass.config.units.temperature(
current_temperature_celsius, UnitOfTemperature.CELSIUS
)
)
assert state.attributes.get(ATTR_TEMPERATURE) == target_temperature
assert state.attributes.get(ATTR_PRESET_MODE) == expected_preset_mode
@pytest.mark.parametrize(
"hvac_mode, api_method",
[
(HVACMode.HEAT_COOL, "turn_on_hvac"),
(HVACMode.OFF, "turn_off_hvac"),
],
)
async def test_set_hvac_mode(hass, hvac_mode, api_method):
"""Test turning on and off the HVAC system."""
client_mock = await init_integration(hass, electric_vehicle=True)
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.my_mazda3_climate", ATTR_HVAC_MODE: hvac_mode},
blocking=True,
)
await hass.async_block_till_done()
getattr(client_mock, api_method).assert_called_once_with(12345)
async def test_set_target_temperature(hass):
"""Test setting the target temperature of the climate entity."""
client_mock = await init_integration(hass, electric_vehicle=True)
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: "climate.my_mazda3_climate", ATTR_TEMPERATURE: 22},
blocking=True,
)
await hass.async_block_till_done()
client_mock.set_hvac_setting.assert_called_once_with(12345, 22, "C", True, False)
@pytest.mark.parametrize(
"preset_mode, front_defroster, rear_defroster",
[
(PRESET_DEFROSTER_OFF, False, False),
(PRESET_DEFROSTER_FRONT, True, False),
(PRESET_DEFROSTER_REAR, False, True),
(PRESET_DEFROSTER_FRONT_AND_REAR, True, True),
],
)
async def test_set_preset_mode(hass, preset_mode, front_defroster, rear_defroster):
"""Test turning on and off the front and rear defrosters."""
client_mock = await init_integration(hass, electric_vehicle=True)
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{
ATTR_ENTITY_ID: "climate.my_mazda3_climate",
ATTR_PRESET_MODE: preset_mode,
},
blocking=True,
)
await hass.async_block_till_done()
client_mock.set_hvac_setting.assert_called_once_with(
12345, 20, "C", front_defroster, rear_defroster
)