This commit is contained in:
Franck Nijhof 2023-10-12 15:26:56 +02:00 committed by GitHub
commit f5b5215247
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 1016 additions and 3907 deletions

View File

@ -738,8 +738,6 @@ build.json @home-assistant/supervisor
/tests/components/matrix/ @PaarthShah
/homeassistant/components/matter/ @home-assistant/matter
/tests/components/matter/ @home-assistant/matter
/homeassistant/components/mazda/ @bdr99
/tests/components/mazda/ @bdr99
/homeassistant/components/meater/ @Sotolotl @emontnemery
/tests/components/meater/ @Sotolotl @emontnemery
/homeassistant/components/medcom_ble/ @elafargue

View File

@ -217,8 +217,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
if ATTR_TEMPERATURE in kwargs:
params[API_SET_POINT] = kwargs[ATTR_TEMPERATURE]
if ATTR_TARGET_TEMP_LOW in kwargs and ATTR_TARGET_TEMP_HIGH in kwargs:
params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
await self._async_update_hvac_params(params)
@callback
@ -248,8 +248,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
self._attr_target_temperature_high = self.get_airzone_value(
AZD_HEAT_TEMP_SET
)
self._attr_target_temperature_low = self.get_airzone_value(
AZD_COOL_TEMP_SET
)
self._attr_target_temperature_low = self.get_airzone_value(
AZD_HEAT_TEMP_SET
)

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected==0.14.0"]
"requirements": ["bimmer-connected==0.14.1"]
}

View File

@ -528,7 +528,9 @@ class CalendarEntity(Entity):
the current or upcoming event.
"""
super().async_write_ha_state()
_LOGGER.debug(
"Clearing %s alarms (%s)", self.entity_id, len(self._alarm_unsubs)
)
for unsub in self._alarm_unsubs:
unsub()
self._alarm_unsubs.clear()
@ -536,6 +538,7 @@ class CalendarEntity(Entity):
now = dt_util.now()
event = self.event
if event is None or now >= event.end_datetime_local:
_LOGGER.debug("No alarms needed for %s (event=%s)", self.entity_id, event)
return
@callback

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/duotecno",
"iot_class": "local_push",
"requirements": ["pyDuotecno==2023.9.0"]
"requirements": ["pyDuotecno==2023.10.0"]
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.5.37"]
"requirements": ["env-canada==0.6.0"]
}

View File

@ -327,7 +327,10 @@ class ESPHomeManager:
) -> int | None:
"""Start a voice assistant pipeline."""
if self.voice_assistant_udp_server is not None:
return None
_LOGGER.warning("Voice assistant UDP server was not stopped")
self.voice_assistant_udp_server.stop()
self.voice_assistant_udp_server.close()
self.voice_assistant_udp_server = None
hass = self.hass
self.voice_assistant_udp_server = VoiceAssistantUDPServer(

View File

@ -24,5 +24,5 @@
"documentation": "https://www.home-assistant.io/integrations/eufylife_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["eufylife-ble-client==0.1.7"]
"requirements": ["eufylife-ble-client==0.1.8"]
}

View File

@ -12,5 +12,5 @@
"iot_class": "local_polling",
"loggers": ["pyfronius"],
"quality_scale": "platinum",
"requirements": ["PyFronius==0.7.1"]
"requirements": ["PyFronius==0.7.2"]
}

View File

@ -240,6 +240,7 @@ async def async_setup_entry(
SERVICE_CREATE_EVENT,
CREATE_EVENT_SCHEMA,
async_create_event,
required_features=CalendarEntityFeature.CREATE_EVENT,
)

View File

@ -4,9 +4,9 @@ from __future__ import annotations
import logging
from typing import Any
from bleak import BleakError
from bleak.exc import BleakError
from bluetooth_data_tools import human_readable_name
from idasen_ha import Desk
from idasen_ha import AuthFailedError, Desk
import voluptuous as vol
from homeassistant import config_entries
@ -64,6 +64,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
desk = Desk(None)
try:
await desk.connect(discovery_info.device, monitor_height=False)
except AuthFailedError as err:
_LOGGER.exception("AuthFailedError", exc_info=err)
errors["base"] = "auth_failed"
except TimeoutError as err:
_LOGGER.exception("TimeoutError", exc_info=err)
errors["base"] = "cannot_connect"

View File

@ -11,5 +11,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
"iot_class": "local_push",
"requirements": ["idasen-ha==1.4"]
"requirements": ["idasen-ha==1.4.1"]
}

View File

@ -9,7 +9,8 @@
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"auth_failed": "Unable to authenticate with the desk. This is usually solved by using an ESPHome Bluetooth Proxy. Please check the integration documentation for alternative workarounds.",
"cannot_connect": "Cannot connect. Make sure that the desk is in Bluetooth pairing mode. If not already, you can also use an ESPHome Bluetooth Proxy, as it provides a better connection.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {

View File

@ -9,7 +9,7 @@
},
"iot_class": "local_push",
"loggers": ["pylutron_caseta"],
"requirements": ["pylutron-caseta==0.18.2"],
"requirements": ["pylutron-caseta==0.18.3"],
"zeroconf": [
{
"type": "_lutron._tcp.local.",

View File

@ -1,213 +1,26 @@
"""The Mazda Connected Services integration."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from pymazda import (
Client as MazdaAPI,
MazdaAccountLockedException,
MazdaAPIEncryptionException,
MazdaAuthenticationException,
MazdaException,
MazdaTokenExpiredException,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
device_registry as dr,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
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,
Platform.SWITCH,
]
DOMAIN = "mazda"
async def with_timeout(task, timeout_seconds=30):
"""Run an async task with a timeout."""
async with asyncio.timeout(timeout_seconds):
return await task
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
"""Set up Mazda Connected Services from a config entry."""
email = entry.data[CONF_EMAIL]
password = entry.data[CONF_PASSWORD]
region = entry.data[CONF_REGION]
websession = aiohttp_client.async_get_clientsession(hass)
mazda_client = MazdaAPI(
email, password, region, websession=websession, use_cached_vehicle_list=True
)
try:
await mazda_client.validate_credentials()
except MazdaAuthenticationException as ex:
raise ConfigEntryAuthFailed from ex
except (
MazdaException,
MazdaAccountLockedException,
MazdaTokenExpiredException,
MazdaAPIEncryptionException,
) as ex:
_LOGGER.error("Error occurred during Mazda login request: %s", ex)
raise ConfigEntryNotReady from ex
async def async_handle_service_call(service_call: ServiceCall) -> None:
"""Handle a service call."""
# Get device entry from device registry
dev_reg = dr.async_get(hass)
device_id = service_call.data["device_id"]
device_entry = dev_reg.async_get(device_id)
if TYPE_CHECKING:
# For mypy: it has already been checked in validate_mazda_device_id
assert device_entry
# Get vehicle VIN from device identifiers
mazda_identifiers = (
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
)
vin_identifier = next(mazda_identifiers)
vin = vin_identifier[1]
# Get vehicle ID and API client from hass.data
vehicle_id = 0
api_client = None
for entry_data in hass.data[DOMAIN].values():
for vehicle in entry_data[DATA_VEHICLES]:
if vehicle["vin"] == vin:
vehicle_id = vehicle["id"]
api_client = entry_data[DATA_CLIENT]
break
if vehicle_id == 0 or api_client is None:
raise HomeAssistantError("Vehicle ID not found")
api_method = getattr(api_client, service_call.service)
try:
latitude = service_call.data["latitude"]
longitude = service_call.data["longitude"]
poi_name = service_call.data["poi_name"]
await api_method(vehicle_id, latitude, longitude, poi_name)
except Exception as ex:
raise HomeAssistantError(ex) from ex
def validate_mazda_device_id(device_id):
"""Check that a device ID exists in the registry and has at least one 'mazda' identifier."""
dev_reg = dr.async_get(hass)
if (device_entry := dev_reg.async_get(device_id)) is None:
raise vol.Invalid("Invalid device ID")
mazda_identifiers = [
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
]
if not mazda_identifiers:
raise vol.Invalid("Device ID is not a Mazda vehicle")
return device_id
service_schema_send_poi = vol.Schema(
{
vol.Required("device_id"): vol.All(cv.string, validate_mazda_device_id),
vol.Required("latitude"): cv.latitude,
vol.Required("longitude"): cv.longitude,
vol.Required("poi_name"): cv.string,
}
)
async def async_update_data():
"""Fetch data from Mazda API."""
try:
vehicles = await with_timeout(mazda_client.get_vehicles())
# The Mazda API can throw an error when multiple simultaneous requests are
# made for the same account, so we can only make one request at a time here
for vehicle in vehicles:
vehicle["status"] = await with_timeout(
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"])
)
vehicle["hvacSetting"] = await with_timeout(
mazda_client.get_hvac_setting(vehicle["id"])
)
hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles
return vehicles
except MazdaAuthenticationException as ex:
raise ConfigEntryAuthFailed("Not authenticated with Mazda API") from ex
except Exception as ex:
_LOGGER.exception(
"Unknown error occurred during Mazda update request: %s", ex
)
raise UpdateFailed(ex) from ex
coordinator = DataUpdateCoordinator(
ir.async_create_issue(
hass,
_LOGGER,
name=DOMAIN,
update_method=async_update_data,
update_interval=timedelta(seconds=180),
)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
DATA_CLIENT: mazda_client,
DATA_COORDINATOR: coordinator,
DATA_REGION: region,
DATA_VEHICLES: [],
}
# Fetch initial data so we have data when entities subscribe
await coordinator.async_config_entry_first_refresh()
# Setup components
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Register services
hass.services.async_register(
DOMAIN,
"send_poi",
async_handle_service_call,
schema=service_schema_send_poi,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"dmca": "https://github.com/github/dmca/blob/master/2023/10/2023-10-10-mazda.md",
"entries": "/config/integrations/integration/mazda",
},
)
return True
@ -215,45 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
# Only remove services if it is the last config entry
if len(hass.data[DOMAIN]) == 1:
hass.services.async_remove(DOMAIN, "send_poi")
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class MazdaEntity(CoordinatorEntity):
"""Defines a base Mazda entity."""
_attr_has_entity_name = True
def __init__(self, client, coordinator, index):
"""Initialize the Mazda entity."""
super().__init__(coordinator)
self.client = client
self.index = index
self.vin = self.data["vin"]
self.vehicle_id = self.data["id"]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.vin)},
manufacturer="Mazda",
model=f"{self.data['modelYear']} {self.data['carlineName']}",
name=self.vehicle_name,
)
@property
def data(self):
"""Shortcut to access coordinator data for the entity."""
return self.coordinator.data[self.index]
@property
def vehicle_name(self):
"""Return the vehicle name, to be used as a prefix for names of other entities."""
if "nickname" in self.data and len(self.data["nickname"]) > 0:
return self.data["nickname"]
return f"{self.data['modelYear']} {self.data['carlineName']}"
return True

View File

@ -1,131 +0,0 @@
"""Platform for Mazda binary sensor integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
@dataclass
class MazdaBinarySensorRequiredKeysMixin:
"""Mixin for required keys."""
# Function to determine the value for this binary sensor, given the coordinator data
value_fn: Callable[[dict[str, Any]], bool]
@dataclass
class MazdaBinarySensorEntityDescription(
BinarySensorEntityDescription, MazdaBinarySensorRequiredKeysMixin
):
"""Describes a Mazda binary sensor entity."""
# Function to determine whether the vehicle supports this binary sensor, given the coordinator data
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
def _plugged_in_supported(data):
"""Determine if 'plugged in' binary sensor is supported."""
return (
data["isElectric"] and data["evStatus"]["chargeInfo"]["pluggedIn"] is not None
)
BINARY_SENSOR_ENTITIES = [
MazdaBinarySensorEntityDescription(
key="driver_door",
translation_key="driver_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="passenger_door",
translation_key="passenger_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="rear_left_door",
translation_key="rear_left_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="rear_right_door",
translation_key="rear_right_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="trunk",
translation_key="trunk",
icon="mdi:car-back",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["trunkOpen"],
),
MazdaBinarySensorEntityDescription(
key="hood",
translation_key="hood",
icon="mdi:car",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["hoodOpen"],
),
MazdaBinarySensorEntityDescription(
key="ev_plugged_in",
translation_key="ev_plugged_in",
device_class=BinarySensorDeviceClass.PLUG,
is_supported=_plugged_in_supported,
value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"],
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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]
async_add_entities(
MazdaBinarySensorEntity(client, coordinator, index, description)
for index, data in enumerate(coordinator.data)
for description in BINARY_SENSOR_ENTITIES
if description.is_supported(data)
)
class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity):
"""Representation of a Mazda vehicle binary sensor."""
entity_description: MazdaBinarySensorEntityDescription
def __init__(self, client, coordinator, index, description):
"""Initialize Mazda binary sensor."""
super().__init__(client, coordinator, index)
self.entity_description = description
self._attr_unique_id = f"{self.vin}_{description.key}"
@property
def is_on(self):
"""Return the state of the binary sensor."""
return self.entity_description.value_fn(self.data)

View File

@ -1,150 +0,0 @@
"""Platform for Mazda button integration."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from pymazda import (
Client as MazdaAPIClient,
MazdaAccountLockedException,
MazdaAPIEncryptionException,
MazdaAuthenticationException,
MazdaException,
MazdaLoginFailedException,
MazdaTokenExpiredException,
)
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def handle_button_press(
client: MazdaAPIClient,
key: str,
vehicle_id: int,
coordinator: DataUpdateCoordinator,
) -> None:
"""Handle a press for a Mazda button entity."""
api_method = getattr(client, key)
try:
await api_method(vehicle_id)
except (
MazdaException,
MazdaAuthenticationException,
MazdaAccountLockedException,
MazdaTokenExpiredException,
MazdaAPIEncryptionException,
MazdaLoginFailedException,
) as ex:
raise HomeAssistantError(ex) from ex
async def handle_refresh_vehicle_status(
client: MazdaAPIClient,
key: str,
vehicle_id: int,
coordinator: DataUpdateCoordinator,
) -> None:
"""Handle a request to refresh the vehicle status."""
await handle_button_press(client, key, vehicle_id, coordinator)
await coordinator.async_request_refresh()
@dataclass
class MazdaButtonEntityDescription(ButtonEntityDescription):
"""Describes a Mazda button entity."""
# Function to determine whether the vehicle supports this button,
# given the coordinator data
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
async_press: Callable[
[MazdaAPIClient, str, int, DataUpdateCoordinator], Awaitable
] = handle_button_press
BUTTON_ENTITIES = [
MazdaButtonEntityDescription(
key="start_engine",
translation_key="start_engine",
icon="mdi:engine",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="stop_engine",
translation_key="stop_engine",
icon="mdi:engine-off",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="turn_on_hazard_lights",
translation_key="turn_on_hazard_lights",
icon="mdi:hazard-lights",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="turn_off_hazard_lights",
translation_key="turn_off_hazard_lights",
icon="mdi:hazard-lights",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="refresh_vehicle_status",
translation_key="refresh_vehicle_status",
icon="mdi:refresh",
async_press=handle_refresh_vehicle_status,
is_supported=lambda data: data["isElectric"],
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the button platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
async_add_entities(
MazdaButtonEntity(client, coordinator, index, description)
for index, data in enumerate(coordinator.data)
for description in BUTTON_ENTITIES
if description.is_supported(data)
)
class MazdaButtonEntity(MazdaEntity, ButtonEntity):
"""Representation of a Mazda button."""
entity_description: MazdaButtonEntityDescription
def __init__(
self,
client: MazdaAPIClient,
coordinator: DataUpdateCoordinator,
index: int,
description: MazdaButtonEntityDescription,
) -> None:
"""Initialize Mazda button."""
super().__init__(client, coordinator, index)
self.entity_description = description
self._attr_unique_id = f"{self.vin}_{description.key}"
async def async_press(self) -> None:
"""Press the button."""
await self.entity_description.async_press(
self.client, self.entity_description.key, self.vehicle_id, self.coordinator
)

View File

@ -1,187 +0,0 @@
"""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.unit_conversion import TemperatureConverter
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_translation_key = "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 = TemperatureConverter.convert(
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

@ -1,110 +1,11 @@
"""Config flow for Mazda Connected Services integration."""
from collections.abc import Mapping
import logging
from typing import Any
"""The Mazda Connected Services integration."""
import aiohttp
from pymazda import (
Client as MazdaAPI,
MazdaAccountLockedException,
MazdaAuthenticationException,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from .const import DOMAIN, MAZDA_REGIONS
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_REGION): vol.In(MAZDA_REGIONS),
}
)
from . import DOMAIN
class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class MazdaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Mazda Connected Services."""
VERSION = 1
def __init__(self):
"""Start the mazda config flow."""
self._reauth_entry = None
self._email = None
self._region = None
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
self._email = user_input[CONF_EMAIL]
self._region = user_input[CONF_REGION]
unique_id = user_input[CONF_EMAIL].lower()
await self.async_set_unique_id(unique_id)
if not self._reauth_entry:
self._abort_if_unique_id_configured()
websession = aiohttp_client.async_get_clientsession(self.hass)
mazda_client = MazdaAPI(
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
user_input[CONF_REGION],
websession,
)
try:
await mazda_client.validate_credentials()
except MazdaAuthenticationException:
errors["base"] = "invalid_auth"
except MazdaAccountLockedException:
errors["base"] = "account_locked"
except aiohttp.ClientError:
errors["base"] = "cannot_connect"
except Exception as ex: # pylint: disable=broad-except
errors["base"] = "unknown"
_LOGGER.exception(
"Unknown error occurred during Mazda login request: %s", ex
)
else:
if not self._reauth_entry:
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
self.hass.config_entries.async_update_entry(
self._reauth_entry, data=user_input, unique_id=unique_id
)
# Reload the config entry otherwise devices will remain unavailable
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_EMAIL, default=self._email): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_REGION, default=self._region): vol.In(
MAZDA_REGIONS
),
}
),
errors=errors,
)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Perform reauth if the user credentials have changed."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
self._email = entry_data[CONF_EMAIL]
self._region = entry_data[CONF_REGION]
return await self.async_step_user()

View File

@ -1,10 +0,0 @@
"""Constants for the Mazda Connected Services integration."""
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,54 +0,0 @@
"""Platform for Mazda device tracker integration."""
from homeassistant.components.device_tracker import SourceType, TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(client, coordinator, index))
async_add_entities(entities)
class MazdaDeviceTracker(MazdaEntity, TrackerEntity):
"""Class for the device tracker."""
_attr_translation_key = "device_tracker"
_attr_icon = "mdi:car"
_attr_force_update = False
def __init__(self, client, coordinator, index) -> None:
"""Initialize Mazda device tracker."""
super().__init__(client, coordinator, index)
self._attr_unique_id = self.vin
@property
def source_type(self) -> SourceType:
"""Return the source type, eg gps or router, of the device."""
return SourceType.GPS
@property
def latitude(self):
"""Return latitude value of the device."""
return self.data["status"]["latitude"]
@property
def longitude(self):
"""Return longitude value of the device."""
return self.data["status"]["longitude"]

View File

@ -1,57 +0,0 @@
"""Diagnostics support for the Mazda integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DATA_COORDINATOR, DOMAIN
TO_REDACT_INFO = [CONF_EMAIL, CONF_PASSWORD]
TO_REDACT_DATA = ["vin", "id", "latitude", "longitude"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
diagnostics_data = {
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
"data": [
async_redact_data(vehicle, TO_REDACT_DATA) for vehicle in coordinator.data
],
}
return diagnostics_data
async def async_get_device_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
vin = next(iter(device.identifiers))[1]
target_vehicle = None
for vehicle in coordinator.data:
if vehicle["vin"] == vin:
target_vehicle = vehicle
break
if target_vehicle is None:
raise HomeAssistantError("Vehicle not found")
diagnostics_data = {
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
"data": async_redact_data(target_vehicle, TO_REDACT_DATA),
}
return diagnostics_data

View File

@ -1,58 +0,0 @@
"""Platform for Mazda lock integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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."""
_attr_translation_key = "lock"
def __init__(self, client, coordinator, index) -> None:
"""Initialize Mazda lock."""
super().__init__(client, coordinator, index)
self._attr_unique_id = self.vin
@property
def is_locked(self) -> bool | None:
"""Return true if lock is locked."""
return self.client.get_assumed_lock_state(self.vehicle_id)
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the vehicle doors."""
await self.client.lock_doors(self.vehicle_id)
self.async_write_ha_state()
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the vehicle doors."""
await self.client.unlock_doors(self.vehicle_id)
self.async_write_ha_state()

View File

@ -1,11 +1,9 @@
{
"domain": "mazda",
"name": "Mazda Connected Services",
"codeowners": ["@bdr99"],
"config_flow": true,
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/mazda",
"integration_type": "system",
"iot_class": "cloud_polling",
"loggers": ["pymazda"],
"quality_scale": "platinum",
"requirements": ["pymazda==0.3.11"]
"requirements": []
}

View File

@ -1,263 +0,0 @@
"""Platform for Mazda sensor integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfPressure
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
@dataclass
class MazdaSensorRequiredKeysMixin:
"""Mixin for required keys."""
# Function to determine the value for this sensor, given the coordinator data
# and the configured unit system
value: Callable[[dict[str, Any]], StateType]
@dataclass
class MazdaSensorEntityDescription(
SensorEntityDescription, MazdaSensorRequiredKeysMixin
):
"""Describes a Mazda sensor entity."""
# Function to determine whether the vehicle supports this sensor,
# given the coordinator data
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
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
def _front_right_tire_pressure_supported(data):
"""Determine if front right tire pressure is supported."""
return data["status"]["tirePressure"]["frontRightTirePressurePsi"] is not None
def _rear_left_tire_pressure_supported(data):
"""Determine if rear left tire pressure is supported."""
return data["status"]["tirePressure"]["rearLeftTirePressurePsi"] is not None
def _rear_right_tire_pressure_supported(data):
"""Determine if rear right tire pressure is supported."""
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):
"""Get the fuel distance remaining value."""
return round(data["status"]["fuelDistanceRemainingKm"])
def _odometer_value(data):
"""Get the odometer value."""
# In order to match the behavior of the Mazda mobile app, we always round down
return int(data["status"]["odometerKm"])
def _front_left_tire_pressure_value(data):
"""Get the front left tire pressure value."""
return round(data["status"]["tirePressure"]["frontLeftTirePressurePsi"])
def _front_right_tire_pressure_value(data):
"""Get the front right tire pressure value."""
return round(data["status"]["tirePressure"]["frontRightTirePressurePsi"])
def _rear_left_tire_pressure_value(data):
"""Get the rear left tire pressure value."""
return round(data["status"]["tirePressure"]["rearLeftTirePressurePsi"])
def _rear_right_tire_pressure_value(data):
"""Get the rear right tire pressure value."""
return round(data["status"]["tirePressure"]["rearRightTirePressurePsi"])
def _ev_charge_level_value(data):
"""Get the charge level value."""
return round(data["evStatus"]["chargeInfo"]["batteryLevelPercentage"])
def _ev_remaining_range_value(data):
"""Get the remaining range value."""
return round(data["evStatus"]["chargeInfo"]["drivingRangeKm"])
SENSOR_ENTITIES = [
MazdaSensorEntityDescription(
key="fuel_remaining_percentage",
translation_key="fuel_remaining_percentage",
icon="mdi:gas-station",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_fuel_remaining_percentage_supported,
value=lambda data: data["status"]["fuelRemainingPercent"],
),
MazdaSensorEntityDescription(
key="fuel_distance_remaining",
translation_key="fuel_distance_remaining",
icon="mdi:gas-station",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_fuel_distance_remaining_supported,
value=_fuel_distance_remaining_value,
),
MazdaSensorEntityDescription(
key="odometer",
translation_key="odometer",
icon="mdi:speedometer",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.TOTAL_INCREASING,
is_supported=lambda data: data["status"]["odometerKm"] is not None,
value=_odometer_value,
),
MazdaSensorEntityDescription(
key="front_left_tire_pressure",
translation_key="front_left_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_front_left_tire_pressure_supported,
value=_front_left_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="front_right_tire_pressure",
translation_key="front_right_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_front_right_tire_pressure_supported,
value=_front_right_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="rear_left_tire_pressure",
translation_key="rear_left_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_rear_left_tire_pressure_supported,
value=_rear_left_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="rear_right_tire_pressure",
translation_key="rear_right_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_rear_right_tire_pressure_supported,
value=_rear_right_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="ev_charge_level",
translation_key="ev_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",
translation_key="ev_remaining_range",
icon="mdi:ev-station",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_ev_remaining_range_supported,
value=_ev_remaining_range_value,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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: list[SensorEntity] = []
for index, data in enumerate(coordinator.data):
for description in SENSOR_ENTITIES:
if description.is_supported(data):
entities.append(
MazdaSensorEntity(client, coordinator, index, description)
)
async_add_entities(entities)
class MazdaSensorEntity(MazdaEntity, SensorEntity):
"""Representation of a Mazda vehicle sensor."""
entity_description: MazdaSensorEntityDescription
def __init__(self, client, coordinator, index, description):
"""Initialize Mazda sensor."""
super().__init__(client, coordinator, index)
self.entity_description = description
self._attr_unique_id = f"{self.vin}_{description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value(self.data)

View File

@ -1,30 +0,0 @@
send_poi:
fields:
device_id:
required: true
selector:
device:
integration: mazda
latitude:
example: 12.34567
required: true
selector:
number:
min: -90
max: 90
unit_of_measurement: °
mode: box
longitude:
example: -34.56789
required: true
selector:
number:
min: -180
max: 180
unit_of_measurement: °
mode: box
poi_name:
example: Work
required: true
selector:
text:

View File

@ -1,139 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"account_locked": "Account locked. Please try again later.",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]",
"region": "Region"
},
"description": "Please enter the email address and password you use to log into the MyMazda mobile app."
}
}
},
"entity": {
"binary_sensor": {
"driver_door": {
"name": "Driver door"
},
"passenger_door": {
"name": "Passenger door"
},
"rear_left_door": {
"name": "Rear left door"
},
"rear_right_door": {
"name": "Rear right door"
},
"trunk": {
"name": "Trunk"
},
"hood": {
"name": "Hood"
},
"ev_plugged_in": {
"name": "Plugged in"
}
},
"button": {
"start_engine": {
"name": "Start engine"
},
"stop_engine": {
"name": "Stop engine"
},
"turn_on_hazard_lights": {
"name": "Turn on hazard lights"
},
"turn_off_hazard_lights": {
"name": "Turn off hazard lights"
},
"refresh_vehicle_status": {
"name": "Refresh status"
}
},
"climate": {
"climate": {
"name": "[%key:component::climate::title%]"
}
},
"device_tracker": {
"device_tracker": {
"name": "[%key:component::device_tracker::title%]"
}
},
"lock": {
"lock": {
"name": "[%key:component::lock::title%]"
}
},
"sensor": {
"fuel_remaining_percentage": {
"name": "Fuel remaining percentage"
},
"fuel_distance_remaining": {
"name": "Fuel distance remaining"
},
"odometer": {
"name": "Odometer"
},
"front_left_tire_pressure": {
"name": "Front left tire pressure"
},
"front_right_tire_pressure": {
"name": "Front right tire pressure"
},
"rear_left_tire_pressure": {
"name": "Rear left tire pressure"
},
"rear_right_tire_pressure": {
"name": "Rear right tire pressure"
},
"ev_charge_level": {
"name": "Charge level"
},
"ev_remaining_range": {
"name": "Remaining range"
}
},
"switch": {
"charging": {
"name": "Charging"
}
}
},
"services": {
"send_poi": {
"name": "Send POI",
"description": "Sends a GPS location to the vehicle's navigation system as a POI (Point of Interest). Requires a navigation SD card installed in the vehicle.",
"fields": {
"device_id": {
"name": "Vehicle",
"description": "The vehicle to send the GPS location to."
},
"latitude": {
"name": "[%key:common::config_flow::data::latitude%]",
"description": "The latitude of the location to send."
},
"longitude": {
"name": "[%key:common::config_flow::data::longitude%]",
"description": "The longitude of the location to send."
},
"poi_name": {
"name": "POI name",
"description": "A friendly name for the location."
}
}
"issues": {
"integration_removed": {
"title": "The Mazda integration has been removed",
"description": "The Mazda integration has been removed from Home Assistant.\n\nThe library that Home Assistant uses to connect with their services, [has been taken offline by Mazda]({dmca}).\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Mazda integration entries]({entries})."
}
}
}

View File

@ -1,72 +0,0 @@
"""Platform for Mazda switch integration."""
from typing import Any
from pymazda import Client as MazdaAPIClient
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the switch platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
async_add_entities(
MazdaChargingSwitch(client, coordinator, index)
for index, data in enumerate(coordinator.data)
if data["isElectric"]
)
class MazdaChargingSwitch(MazdaEntity, SwitchEntity):
"""Class for the charging switch."""
_attr_translation_key = "charging"
_attr_icon = "mdi:ev-station"
def __init__(
self,
client: MazdaAPIClient,
coordinator: DataUpdateCoordinator,
index: int,
) -> None:
"""Initialize Mazda charging switch."""
super().__init__(client, coordinator, index)
self._attr_unique_id = self.vin
@property
def is_on(self):
"""Return true if the vehicle is charging."""
return self.data["evStatus"]["chargeInfo"]["charging"]
async def refresh_status_and_write_state(self):
"""Request a status update, retrieve it through the coordinator, and write the state."""
await self.client.refresh_vehicle_status(self.vehicle_id)
await self.coordinator.async_request_refresh()
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Start charging the vehicle."""
await self.client.start_charging(self.vehicle_id)
await self.refresh_status_and_write_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Stop charging the vehicle."""
await self.client.stop_charging(self.vehicle_id)
await self.refresh_status_and_write_state()

View File

@ -180,7 +180,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
@callback
@log_messages(self.hass, self.entity_id)
@write_state_on_attr_change(self, {"_attr_is_on"})
@write_state_on_attr_change(self, {"_attr_is_on", "_expired"})
def state_message_received(msg: ReceiveMessage) -> None:
"""Handle a new received MQTT state message."""
# auto-expire enabled?

View File

@ -277,7 +277,9 @@ class MqttSensor(MqttEntity, RestoreSensor):
)
@callback
@write_state_on_attr_change(self, {"_attr_native_value", "_attr_last_reset"})
@write_state_on_attr_change(
self, {"_attr_native_value", "_attr_last_reset", "_expired"}
)
@log_messages(self.hass, self.entity_id)
def message_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages."""

View File

@ -14,5 +14,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["pkce", "pymyq"],
"requirements": ["python-myq==3.1.11"]
"requirements": ["python-myq==3.1.13"]
}

View File

@ -8,7 +8,7 @@ from typing import Any
from mysensors import BaseAsyncGateway, Sensor
from mysensors.sensor import ChildSensor
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
@ -212,6 +212,8 @@ class MySensorsChildEntity(MySensorNodeEntity):
attr[ATTR_CHILD_ID] = self.child_id
attr[ATTR_DESCRIPTION] = self._child.description
# We should deprecate the battery level attribute in the future.
attr[ATTR_BATTERY_LEVEL] = self._node.battery_level
set_req = self.gateway.const.SetReq
for value_type, value in self._values.items():

View File

@ -1,7 +1,7 @@
{
"services": {
"submit_movie_request": {
"name": "Sumbit movie request",
"name": "Submit movie request",
"description": "Searches for a movie and requests the first result.",
"fields": {
"name": {

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/philips_js",
"iot_class": "local_polling",
"loggers": ["haphilipsjs"],
"requirements": ["ha-philipsjs==3.1.0"]
"requirements": ["ha-philipsjs==3.1.1"]
}

View File

@ -259,7 +259,7 @@
"name": "DHW comfort mode"
},
"lock": {
"name": "[%key:component::lock::entity_component::_::name%]"
"name": "[%key:component::lock::title%]"
},
"relay": {
"name": "Relay"

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
"iot_class": "local_polling",
"loggers": ["aioqsw"],
"requirements": ["aioqsw==0.3.4"]
"requirements": ["aioqsw==0.3.5"]
}

View File

@ -48,7 +48,7 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], BinarySensorE
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self.entity_description = description
if coordinator.unique_id:
if coordinator.unique_id is not None:
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
self._attr_device_info = coordinator.device_info
else:

View File

@ -84,7 +84,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
@property
def device_info(self) -> DeviceInfo | None:
"""Return information about the device."""
if not self._unique_id:
if self._unique_id is None:
return None
return DeviceInfo(
name=self.device_name,

View File

@ -526,7 +526,7 @@ def _compile_statistics(
):
continue
compiled: PlatformCompiledStatistics = platform_compile_statistics(
instance.hass, start, end
instance.hass, session, start, end
)
_LOGGER.debug(
"Statistics for %s during %s-%s: %s",
@ -1871,7 +1871,7 @@ def get_latest_short_term_statistics_by_ids(
return list(
cast(
Sequence[Row],
execute_stmt_lambda_element(session, stmt, orm_rows=False),
execute_stmt_lambda_element(session, stmt),
)
)
@ -1887,14 +1887,14 @@ def _latest_short_term_statistics_by_ids_stmt(
)
def get_latest_short_term_statistics(
def get_latest_short_term_statistics_with_session(
hass: HomeAssistant,
session: Session,
statistic_ids: set[str],
types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]],
metadata: dict[str, tuple[int, StatisticMetaData]] | None = None,
) -> dict[str, list[StatisticsRow]]:
"""Return the latest short term statistics for a list of statistic_ids."""
with session_scope(hass=hass, read_only=True) as session:
"""Return the latest short term statistics for a list of statistic_ids with a session."""
# Fetch metadata for the given statistic_ids
if not metadata:
metadata = get_instance(hass).statistics_meta_manager.get_many(
@ -1924,21 +1924,15 @@ def get_latest_short_term_statistics(
for metadata_id in missing_metadata_ids
if (
latest_id := cache_latest_short_term_statistic_id_for_metadata_id(
# orm_rows=False is used here because we are in
# a read-only session, and there will never be
# any pending inserts in the session.
run_cache,
session,
metadata_id,
orm_rows=False,
)
)
is not None
}
):
stats.extend(
get_latest_short_term_statistics_by_ids(session, found_latest_ids)
)
stats.extend(get_latest_short_term_statistics_by_ids(session, found_latest_ids))
if not stats:
return {}
@ -2316,14 +2310,8 @@ def _import_statistics_with_session(
# We just inserted new short term statistics, so we need to update the
# ShortTermStatisticsRunCache with the latest id for the metadata_id
run_cache = get_short_term_statistics_run_cache(instance.hass)
#
# Because we are in the same session and we want to read rows
# that have not been flushed yet, we need to pass orm_rows=True
# to cache_latest_short_term_statistic_id_for_metadata_id
# to ensure that it gets the rows that were just inserted
#
cache_latest_short_term_statistic_id_for_metadata_id(
run_cache, session, metadata_id, orm_rows=True
run_cache, session, metadata_id
)
return True
@ -2341,7 +2329,6 @@ def cache_latest_short_term_statistic_id_for_metadata_id(
run_cache: ShortTermStatisticsRunCache,
session: Session,
metadata_id: int,
orm_rows: bool,
) -> int | None:
"""Cache the latest short term statistic for a given metadata_id.
@ -2352,13 +2339,7 @@ def cache_latest_short_term_statistic_id_for_metadata_id(
if latest := cast(
Sequence[Row],
execute_stmt_lambda_element(
session,
_find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id),
orm_rows=orm_rows
# _import_statistics_with_session needs to be able
# to read back the rows it just inserted without
# a flush so we have to pass orm_rows so we get
# back the latest data.
session, _find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id)
),
):
id_: int = latest[0].id

View File

@ -16,7 +16,7 @@
},
"complete_task": {
"name": "Complete task",
"description": "Completes a tasks that was privously created.",
"description": "Completes a task that was previously created.",
"fields": {
"id": {
"name": "ID",

View File

@ -92,7 +92,6 @@ class RoonDevice(MediaPlayerEntity):
MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.GROUPING
| MediaPlayerEntityFeature.PAUSE
| MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.NEXT_TRACK
@ -104,7 +103,6 @@ class RoonDevice(MediaPlayerEntity):
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.VOLUME_STEP
)
def __init__(self, server, player_data):
@ -124,6 +122,8 @@ class RoonDevice(MediaPlayerEntity):
self._attr_shuffle = False
self._attr_media_image_url = None
self._attr_volume_level = 0
self._volume_fixed = True
self._volume_incremental = False
self.update_data(player_data)
async def async_added_to_hass(self) -> None:
@ -190,12 +190,21 @@ class RoonDevice(MediaPlayerEntity):
"level": 0,
"step": 0,
"muted": False,
"fixed": True,
"incremental": False,
}
try:
volume_data = player_data["volume"]
volume_muted = volume_data["is_muted"]
volume_step = convert(volume_data["step"], int, 0)
except KeyError:
return volume
volume["fixed"] = False
volume["incremental"] = volume_data["type"] == "incremental"
volume["muted"] = volume_data.get("is_muted", False)
volume["step"] = convert(volume_data.get("step"), int, 0)
try:
volume_max = volume_data["max"]
volume_min = volume_data["min"]
raw_level = convert(volume_data["value"], float, 0)
@ -204,15 +213,9 @@ class RoonDevice(MediaPlayerEntity):
volume_percentage_factor = volume_range / 100
level = (raw_level - volume_min) / volume_percentage_factor
volume_level = convert(level, int, 0) / 100
volume["level"] = convert(level, int, 0) / 100
except KeyError:
# catch KeyError
pass
else:
volume["muted"] = volume_muted
volume["step"] = volume_step
volume["level"] = volume_level
return volume
@ -288,6 +291,16 @@ class RoonDevice(MediaPlayerEntity):
self._attr_is_volume_muted = volume["muted"]
self._attr_volume_step = volume["step"]
self._attr_volume_level = volume["level"]
self._volume_fixed = volume["fixed"]
self._volume_incremental = volume["incremental"]
if not self._volume_fixed:
self._attr_supported_features = (
self._attr_supported_features | MediaPlayerEntityFeature.VOLUME_STEP
)
if not self._volume_incremental:
self._attr_supported_features = (
self._attr_supported_features | MediaPlayerEntityFeature.VOLUME_SET
)
now_playing = self._parse_now_playing(self.player_data)
self._attr_media_title = now_playing["title"]
@ -359,10 +372,16 @@ class RoonDevice(MediaPlayerEntity):
def volume_up(self) -> None:
"""Send new volume_level to device."""
if self._volume_incremental:
self._server.roonapi.change_volume_raw(self.output_id, 1, "relative_step")
else:
self._server.roonapi.change_volume_percent(self.output_id, 3)
def volume_down(self) -> None:
"""Send new volume_level to device."""
if self._volume_incremental:
self._server.roonapi.change_volume_raw(self.output_id, -1, "relative_step")
else:
self._server.roonapi.change_volume_percent(self.output_id, -3)
def turn_on(self) -> None:

View File

@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/screenlogic",
"iot_class": "local_push",
"loggers": ["screenlogicpy"],
"requirements": ["screenlogicpy==0.9.1"]
"requirements": ["screenlogicpy==0.9.2"]
}

View File

@ -1,5 +1,6 @@
"""Support for a ScreenLogic number entity."""
from collections.abc import Callable
import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import logging
@ -105,13 +106,13 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity):
) -> None:
"""Initialize a ScreenLogic number entity."""
super().__init__(coordinator, entity_description)
if not callable(
if not asyncio.iscoroutinefunction(
func := getattr(self.gateway, entity_description.set_value_name)
):
raise TypeError(
f"set_value_name '{entity_description.set_value_name}' is not a callable"
f"set_value_name '{entity_description.set_value_name}' is not a coroutine"
)
self._set_value_func: Callable[..., bool] = func
self._set_value_func: Callable[..., Awaitable[bool]] = func
self._set_value_args = entity_description.set_value_args
self._attr_native_unit_of_measurement = get_ha_unit(
self.entity_data.get(ATTR.UNIT)
@ -145,9 +146,12 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity):
data_key = data_path[-1]
args[data_key] = self.coordinator.gateway.get_value(*data_path, strict=True)
# Current API requires int values for the currently supported numbers.
value = int(value)
args[self._data_key] = value
if self._set_value_func(*args.values()):
if await self._set_value_func(*args.values()):
_LOGGER.debug("Set '%s' to %s", self._data_key, value)
await self._async_refresh()
else:

View File

@ -16,7 +16,6 @@ from homeassistant.components.recorder import (
get_instance,
history,
statistics,
util as recorder_util,
)
from homeassistant.components.recorder.models import (
StatisticData,
@ -383,27 +382,7 @@ def _timestamp_to_isoformat_or_none(timestamp: float | None) -> str | None:
return dt_util.utc_from_timestamp(timestamp).isoformat()
def compile_statistics(
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
) -> statistics.PlatformCompiledStatistics:
"""Compile statistics for all entities during start-end.
Note: This will query the database and must not be run in the event loop
"""
# There is already an active session when this code is called since
# it is called from the recorder statistics. We need to make sure
# this session never gets committed since it would be out of sync
# with the recorder statistics session so we mark it as read only.
#
# If we ever need to write to the database from this function we
# will need to refactor the recorder statistics to use a single
# session.
with recorder_util.session_scope(hass=hass, read_only=True) as session:
compiled = _compile_statistics(hass, session, start, end)
return compiled
def _compile_statistics( # noqa: C901
def compile_statistics( # noqa: C901
hass: HomeAssistant,
session: Session,
start: datetime.datetime,
@ -480,8 +459,8 @@ def _compile_statistics( # noqa: C901
if "sum" in wanted_statistics[entity_id]:
to_query.add(entity_id)
last_stats = statistics.get_latest_short_term_statistics(
hass, to_query, {"last_reset", "state", "sum"}, metadata=old_metadatas
last_stats = statistics.get_latest_short_term_statistics_with_session(
hass, session, to_query, {"last_reset", "state", "sum"}, metadata=old_metadatas
)
for ( # pylint: disable=too-many-nested-blocks
entity_id,

View File

@ -166,7 +166,7 @@ class SlackNotificationService(BaseNotificationService):
filename=filename,
initial_comment=message,
title=title or filename,
thread_ts=thread_ts,
thread_ts=thread_ts or "",
)
except (SlackApiError, ClientError) as err:
_LOGGER.error("Error while uploading file-based message: %r", err)

View File

@ -274,8 +274,6 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = {
"grid_power_factor_excitation": SensorEntityDescription(
key="grid_power_factor_excitation",
name="Grid Power Factor Excitation",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER_FACTOR,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),

View File

@ -10,6 +10,6 @@
"iot_class": "local_push",
"loggers": ["systembridgeconnector"],
"quality_scale": "silver",
"requirements": ["systembridgeconnector==3.8.2"],
"requirements": ["systembridgeconnector==3.8.4"],
"zeroconf": ["_system-bridge._tcp.local."]
}

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from abc import abstractmethod
import asyncio
from collections.abc import AsyncIterable
import logging
from typing import final
@ -34,6 +35,8 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
TIMEOUT_FETCH_WAKE_WORDS = 10
@callback
def async_default_entity(hass: HomeAssistant) -> str | None:
@ -86,9 +89,8 @@ class WakeWordDetectionEntity(RestoreEntity):
"""Return the state of the entity."""
return self.__last_detected
@property
@abstractmethod
def supported_wake_words(self) -> list[WakeWord]:
async def get_supported_wake_words(self) -> list[WakeWord]:
"""Return a list of supported wake words."""
@abstractmethod
@ -133,8 +135,9 @@ class WakeWordDetectionEntity(RestoreEntity):
vol.Required("entity_id"): cv.entity_domain(DOMAIN),
}
)
@websocket_api.async_response
@callback
def websocket_entity_info(
async def websocket_entity_info(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Get info about wake word entity."""
@ -147,7 +150,16 @@ def websocket_entity_info(
)
return
try:
async with asyncio.timeout(TIMEOUT_FETCH_WAKE_WORDS):
wake_words = await entity.get_supported_wake_words()
except asyncio.TimeoutError:
connection.send_error(
msg["id"], websocket_api.const.ERR_TIMEOUT, "Timeout fetching wake words"
)
return
connection.send_result(
msg["id"],
{"wake_words": entity.supported_wake_words},
{"wake_words": wake_words},
)

View File

@ -3,7 +3,7 @@ from enum import StrEnum
DOMAIN = "wallbox"
BIDIRECTIONAL_MODEL_PREFIXES = ["QSX"]
BIDIRECTIONAL_MODEL_PREFIXES = ["QS"]
CODE_KEY = "code"
CONF_STATION = "station"

View File

@ -79,7 +79,7 @@ class WallboxNumber(WallboxEntity, NumberEntity):
self._coordinator = coordinator
self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}"
self._is_bidirectional = (
coordinator.data[CHARGER_DATA_KEY][CHARGER_PART_NUMBER_KEY][0:3]
coordinator.data[CHARGER_DATA_KEY][CHARGER_PART_NUMBER_KEY][0:2]
in BIDIRECTIONAL_MODEL_PREFIXES
)

View File

@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyweatherflowudp"],
"requirements": ["pyweatherflowudp==1.4.3"]
"requirements": ["pyweatherflowudp==1.4.5"]
}

View File

@ -66,6 +66,7 @@ class WeatherKitFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
try:
user_input[CONF_KEY_PEM] = self._fix_key_input(user_input[CONF_KEY_PEM])
await self._test_config(user_input)
except WeatherKitUnsupportedLocationError as exception:
LOGGER.error(exception)
@ -104,6 +105,25 @@ class WeatherKitFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
def _fix_key_input(self, key_input: str) -> str:
"""Fix common user errors with the key input."""
# OSes may sometimes turn two hyphens (--) into an em dash (—)
key_input = key_input.replace("", "--")
# Trim whitespace and line breaks
key_input = key_input.strip()
# Make sure header and footer are present
header = "-----BEGIN PRIVATE KEY-----"
if not key_input.startswith(header):
key_input = f"{header}\n{key_input}"
footer = "-----END PRIVATE KEY-----"
if not key_input.endswith(footer):
key_input += f"\n{footer}"
return key_input
async def _test_config(self, user_input: dict[str, Any]) -> None:
"""Validate credentials."""
client = WeatherKitApiClient(

View File

@ -4,8 +4,10 @@ For more details about this platform, please refer to the documentation at
"""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
import contextlib
from datetime import timedelta
from typing import Any
from aiohttp.hdrs import METH_HEAD, METH_POST
@ -41,14 +43,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
from homeassistant.helpers.typing import ConfigType
from .api import ConfigEntryWithingsApi
from .const import (
CONF_CLOUDHOOK_URL,
CONF_PROFILES,
CONF_USE_WEBHOOK,
DEFAULT_TITLE,
DOMAIN,
LOGGER,
)
from .const import CONF_PROFILES, CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN, LOGGER
from .coordinator import WithingsDataUpdateCoordinator
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
@ -78,6 +73,9 @@ CONFIG_SCHEMA = vol.Schema(
},
extra=vol.ALLOW_EXTRA,
)
SUBSCRIBE_DELAY = timedelta(seconds=5)
UNSUBSCRIBE_DELAY = timedelta(seconds=1)
CONF_CLOUDHOOK_URL = "cloudhook_url"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@ -141,13 +139,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) -> None:
LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID])
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
await hass.data[DOMAIN][entry.entry_id].async_unsubscribe_webhooks()
await async_unsubscribe_webhooks(client)
coordinator.webhook_subscription_listener(False)
async def register_webhook(
_: Any,
) -> None:
if cloud.async_active_subscription(hass):
webhook_url = await async_cloudhook_generate_url(hass, entry)
webhook_url = await _async_cloudhook_generate_url(hass, entry)
else:
webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID])
@ -170,7 +169,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
get_webhook_handler(coordinator),
)
await hass.data[DOMAIN][entry.entry_id].async_subscribe_webhooks(webhook_url)
await async_subscribe_webhooks(client, webhook_url)
coordinator.webhook_subscription_listener(True)
LOGGER.debug("Register Withings webhook: %s", webhook_url)
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
@ -194,7 +194,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload(async_call_later(hass, 1, register_webhook))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
@ -208,12 +207,54 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_subscribe_webhooks(
client: ConfigEntryWithingsApi, webhook_url: str
) -> None:
"""Subscribe to Withings webhooks."""
await async_unsubscribe_webhooks(client)
notification_to_subscribe = {
NotifyAppli.WEIGHT,
NotifyAppli.CIRCULATORY,
NotifyAppli.ACTIVITY,
NotifyAppli.SLEEP,
NotifyAppli.BED_IN,
NotifyAppli.BED_OUT,
}
for notification in notification_to_subscribe:
LOGGER.debug(
"Subscribing %s for %s in %s seconds",
webhook_url,
notification,
SUBSCRIBE_DELAY.total_seconds(),
)
# Withings will HTTP HEAD the callback_url and needs some downtime
# between each call or there is a higher chance of failure.
await asyncio.sleep(SUBSCRIBE_DELAY.total_seconds())
await client.async_notify_subscribe(webhook_url, notification)
async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str:
async def async_unsubscribe_webhooks(client: ConfigEntryWithingsApi) -> None:
"""Unsubscribe to all Withings webhooks."""
current_webhooks = await client.async_notify_list()
for webhook_configuration in current_webhooks.profiles:
LOGGER.debug(
"Unsubscribing %s for %s in %s seconds",
webhook_configuration.callbackurl,
webhook_configuration.appli,
UNSUBSCRIBE_DELAY.total_seconds(),
)
# Quick calls to Withings can result in the service returning errors.
# Give them some time to cool down.
await asyncio.sleep(UNSUBSCRIBE_DELAY.total_seconds())
await client.async_notify_revoke(
webhook_configuration.callbackurl, webhook_configuration.appli
)
async def _async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str:
"""Generate the full URL for a webhook_id."""
if CONF_CLOUDHOOK_URL not in entry.data:
webhook_id = entry.data[CONF_WEBHOOK_ID]

View File

@ -76,6 +76,7 @@ class WithingsFlowHandler(
self.hass.config_entries.async_update_entry(
self.reauth_entry, data={**self.reauth_entry.data, **data}
)
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_abort(reason="wrong_account")

View File

@ -5,7 +5,6 @@ import logging
DEFAULT_TITLE = "Withings"
CONF_PROFILES = "profiles"
CONF_USE_WEBHOOK = "use_webhook"
CONF_CLOUDHOOK_URL = "cloudhook_url"
DATA_MANAGER = "data_manager"

View File

@ -1,5 +1,4 @@
"""Withings coordinator."""
import asyncio
from collections.abc import Callable
from datetime import timedelta
from typing import Any
@ -24,9 +23,6 @@ from homeassistant.util import dt as dt_util
from .api import ConfigEntryWithingsApi
from .const import LOGGER, Measurement
SUBSCRIBE_DELAY = timedelta(seconds=5)
UNSUBSCRIBE_DELAY = timedelta(seconds=1)
WITHINGS_MEASURE_TYPE_MAP: dict[
NotifyAppli | GetSleepSummaryField | MeasureType, Measurement
] = {
@ -84,54 +80,11 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
super().__init__(hass, LOGGER, name="Withings", update_interval=UPDATE_INTERVAL)
self._client = client
async def async_subscribe_webhooks(self, webhook_url: str) -> None:
"""Subscribe to webhooks."""
await self.async_unsubscribe_webhooks()
current_webhooks = await self._client.async_notify_list()
subscribed_notifications = frozenset(
profile.appli
for profile in current_webhooks.profiles
if profile.callbackurl == webhook_url
)
notification_to_subscribe = (
set(NotifyAppli)
- subscribed_notifications
- {NotifyAppli.USER, NotifyAppli.UNKNOWN}
)
for notification in notification_to_subscribe:
LOGGER.debug(
"Subscribing %s for %s in %s seconds",
webhook_url,
notification,
SUBSCRIBE_DELAY.total_seconds(),
)
# Withings will HTTP HEAD the callback_url and needs some downtime
# between each call or there is a higher chance of failure.
await asyncio.sleep(SUBSCRIBE_DELAY.total_seconds())
await self._client.async_notify_subscribe(webhook_url, notification)
def webhook_subscription_listener(self, connected: bool) -> None:
"""Call when webhook status changed."""
if connected:
self.update_interval = None
async def async_unsubscribe_webhooks(self) -> None:
"""Unsubscribe to webhooks."""
current_webhooks = await self._client.async_notify_list()
for webhook_configuration in current_webhooks.profiles:
LOGGER.debug(
"Unsubscribing %s for %s in %s seconds",
webhook_configuration.callbackurl,
webhook_configuration.appli,
UNSUBSCRIBE_DELAY.total_seconds(),
)
# Quick calls to Withings can result in the service returning errors.
# Give them some time to cool down.
await asyncio.sleep(UNSUBSCRIBE_DELAY.total_seconds())
await self._client.async_notify_revoke(
webhook_configuration.callbackurl, webhook_configuration.appli
)
else:
self.update_interval = UPDATE_INTERVAL
async def _async_update_data(self) -> dict[Measurement, Any]:

View File

@ -16,7 +16,8 @@
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"already_configured": "Configuration updated for profile.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]"
},
"create_entry": {
"default": "Successfully authenticated with Withings."

View File

@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .data import WyomingService
from .data import WyomingService, load_wyoming_info
from .error import WyomingError
_LOGGER = logging.getLogger(__name__)
@ -28,7 +28,7 @@ async def async_setup_entry(
service: WyomingService = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
[
WyomingWakeWordProvider(config_entry, service),
WyomingWakeWordProvider(hass, config_entry, service),
]
)
@ -38,10 +38,12 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity):
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
service: WyomingService,
) -> None:
"""Set up provider."""
self.hass = hass
self.service = service
wake_service = service.info.wake[0]
@ -52,9 +54,19 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity):
self._attr_name = wake_service.name
self._attr_unique_id = f"{config_entry.entry_id}-wake_word"
@property
def supported_wake_words(self) -> list[wake_word.WakeWord]:
async def get_supported_wake_words(self) -> list[wake_word.WakeWord]:
"""Return a list of supported wake words."""
info = await load_wyoming_info(
self.service.host, self.service.port, retries=0, timeout=1
)
if info is not None:
wake_service = info.wake[0]
self._supported_wake_words = [
wake_word.WakeWord(id=ww.name, name=ww.description or ww.name)
for ww in wake_service.models
]
return self._supported_wake_words
async def _async_process_audio_stream(

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 10
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

View File

@ -270,7 +270,6 @@ FLOWS = {
"lyric",
"mailgun",
"matter",
"mazda",
"meater",
"medcom_ble",
"melcloud",

View File

@ -3258,12 +3258,6 @@
"config_flow": true,
"iot_class": "local_push"
},
"mazda": {
"name": "Mazda Connected Services",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"meater": {
"name": "Meater",
"integration_type": "hub",

View File

@ -318,6 +318,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
_LOGGER.error("Timeout resolving OAuth token: %s", err)
return self.async_abort(reason="oauth2_timeout")
if "expires_in" not in token:
_LOGGER.warning("Invalid token: %s", token)
return self.async_abort(reason="oauth_error")
# Force int for non-compliant oauth2 providers
try:
token["expires_in"] = int(token["expires_in"])

View File

@ -1,5 +1,5 @@
aiodiscover==1.5.1
aiohttp==3.8.5
aiohttp==3.8.6
aiohttp_cors==0.7.0
astral==2.2
async-upnp-client==0.36.1

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.10.1"
version = "2023.10.2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@ -23,7 +23,7 @@ classifiers = [
]
requires-python = ">=3.11.0"
dependencies = [
"aiohttp==3.8.5",
"aiohttp==3.8.6",
"astral==2.2",
"attrs==23.1.0",
"atomicwrites-homeassistant==1.4.1",
@ -437,7 +437,7 @@ filterwarnings = [
# -- design choice 3rd party
# https://github.com/gwww/elkm1/blob/2.2.5/elkm1_lib/util.py#L8-L19
"ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util",
# https://github.com/michaeldavie/env_canada/blob/v0.5.37/env_canada/ec_cache.py
# https://github.com/michaeldavie/env_canada/blob/v0.6.0/env_canada/ec_cache.py
"ignore:Inheritance class CacheClientSession from ClientSession is discouraged:DeprecationWarning:env_canada.ec_cache",
# https://github.com/bachya/regenmaschine/blob/2023.08.0/regenmaschine/client.py#L51
"ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client",

View File

@ -1,7 +1,7 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
aiohttp==3.8.5
aiohttp==3.8.6
astral==2.2
attrs==23.1.0
atomicwrites-homeassistant==1.4.1

View File

@ -64,7 +64,7 @@ PyFlick==0.0.2
PyFlume==0.6.5
# homeassistant.components.fronius
PyFronius==0.7.1
PyFronius==0.7.2
# homeassistant.components.mvglive
PyMVGLive==1.1.4
@ -324,7 +324,7 @@ aiopvpc==4.2.2
aiopyarr==23.4.0
# homeassistant.components.qnap_qsw
aioqsw==0.3.4
aioqsw==0.3.5
# homeassistant.components.recollect_waste
aiorecollect==2023.09.0
@ -515,7 +515,7 @@ beautifulsoup4==4.12.2
bellows==0.36.5
# homeassistant.components.bmw_connected_drive
bimmer-connected==0.14.0
bimmer-connected==0.14.1
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@ -749,7 +749,7 @@ enocean==0.50
enturclient==0.2.4
# homeassistant.components.environment_canada
env-canada==0.5.37
env-canada==0.6.0
# homeassistant.components.season
ephem==4.1.2
@ -767,7 +767,7 @@ esphome-dashboard-api==1.2.3
eternalegypt==0.0.16
# homeassistant.components.eufylife_ble
eufylife-ble-client==0.1.7
eufylife-ble-client==0.1.8
# homeassistant.components.keyboard_remote
# evdev==1.6.1
@ -955,7 +955,7 @@ ha-ffmpeg==3.1.0
ha-iotawattpy==0.1.1
# homeassistant.components.philips_js
ha-philipsjs==3.1.0
ha-philipsjs==3.1.1
# homeassistant.components.habitica
habitipy==0.2.0
@ -1042,7 +1042,7 @@ ical==5.0.1
icmplib==3.0
# homeassistant.components.idasen_desk
idasen-ha==1.4
idasen-ha==1.4.1
# homeassistant.components.network
ifaddr==0.2.0
@ -1538,7 +1538,7 @@ pyCEC==0.5.2
pyControl4==1.1.0
# homeassistant.components.duotecno
pyDuotecno==2023.9.0
pyDuotecno==2023.10.0
# homeassistant.components.eight_sleep
pyEight==0.3.2
@ -1833,7 +1833,7 @@ pylitejet==0.5.0
pylitterbot==2023.4.9
# homeassistant.components.lutron_caseta
pylutron-caseta==0.18.2
pylutron-caseta==0.18.3
# homeassistant.components.lutron
pylutron==0.2.8
@ -1844,9 +1844,6 @@ pymailgunner==1.4
# homeassistant.components.firmata
pymata-express==1.19
# homeassistant.components.mazda
pymazda==0.3.11
# homeassistant.components.mediaroom
pymediaroom==0.6.5.4
@ -2149,7 +2146,7 @@ python-miio==0.5.12
python-mpd2==3.0.5
# homeassistant.components.myq
python-myq==3.1.11
python-myq==3.1.13
# homeassistant.components.mystrom
python-mystrom==2.2.0
@ -2249,7 +2246,7 @@ pyvolumio==0.1.5
pywaze==0.5.1
# homeassistant.components.weatherflow
pyweatherflowudp==1.4.3
pyweatherflowudp==1.4.5
# homeassistant.components.html5
pywebpush==1.9.2
@ -2375,7 +2372,7 @@ satel-integra==0.3.7
scapy==2.5.0
# homeassistant.components.screenlogic
screenlogicpy==0.9.1
screenlogicpy==0.9.2
# homeassistant.components.scsgate
scsgate==0.1.0
@ -2523,7 +2520,7 @@ switchbot-api==1.1.0
synology-srm==0.2.0
# homeassistant.components.system_bridge
systembridgeconnector==3.8.2
systembridgeconnector==3.8.4
# homeassistant.components.tailscale
tailscale==0.2.0

View File

@ -57,7 +57,7 @@ PyFlick==0.0.2
PyFlume==0.6.5
# homeassistant.components.fronius
PyFronius==0.7.1
PyFronius==0.7.2
# homeassistant.components.met_eireann
PyMetEireann==2021.8.0
@ -299,7 +299,7 @@ aiopvpc==4.2.2
aiopyarr==23.4.0
# homeassistant.components.qnap_qsw
aioqsw==0.3.4
aioqsw==0.3.5
# homeassistant.components.recollect_waste
aiorecollect==2023.09.0
@ -439,7 +439,7 @@ beautifulsoup4==4.12.2
bellows==0.36.5
# homeassistant.components.bmw_connected_drive
bimmer-connected==0.14.0
bimmer-connected==0.14.1
# homeassistant.components.bluetooth
bleak-retry-connector==3.2.1
@ -605,7 +605,7 @@ energyzero==0.5.0
enocean==0.50
# homeassistant.components.environment_canada
env-canada==0.5.37
env-canada==0.6.0
# homeassistant.components.season
ephem==4.1.2
@ -617,7 +617,7 @@ epson-projector==0.5.1
esphome-dashboard-api==1.2.3
# homeassistant.components.eufylife_ble
eufylife-ble-client==0.1.7
eufylife-ble-client==0.1.8
# homeassistant.components.faa_delays
faadelays==2023.9.1
@ -756,7 +756,7 @@ ha-ffmpeg==3.1.0
ha-iotawattpy==0.1.1
# homeassistant.components.philips_js
ha-philipsjs==3.1.0
ha-philipsjs==3.1.1
# homeassistant.components.habitica
habitipy==0.2.0
@ -822,7 +822,7 @@ ical==5.0.1
icmplib==3.0
# homeassistant.components.idasen_desk
idasen-ha==1.4
idasen-ha==1.4.1
# homeassistant.components.network
ifaddr==0.2.0
@ -1171,7 +1171,7 @@ pyCEC==0.5.2
pyControl4==1.1.0
# homeassistant.components.duotecno
pyDuotecno==2023.9.0
pyDuotecno==2023.10.0
# homeassistant.components.eight_sleep
pyEight==0.3.2
@ -1379,7 +1379,7 @@ pylitejet==0.5.0
pylitterbot==2023.4.9
# homeassistant.components.lutron_caseta
pylutron-caseta==0.18.2
pylutron-caseta==0.18.3
# homeassistant.components.mailgun
pymailgunner==1.4
@ -1387,9 +1387,6 @@ pymailgunner==1.4
# homeassistant.components.firmata
pymata-express==1.19
# homeassistant.components.mazda
pymazda==0.3.11
# homeassistant.components.melcloud
pymelcloud==2.5.8
@ -1599,7 +1596,7 @@ python-matter-server==3.7.0
python-miio==0.5.12
# homeassistant.components.myq
python-myq==3.1.11
python-myq==3.1.13
# homeassistant.components.mystrom
python-mystrom==2.2.0
@ -1675,7 +1672,7 @@ pyvolumio==0.1.5
pywaze==0.5.1
# homeassistant.components.weatherflow
pyweatherflowudp==1.4.3
pyweatherflowudp==1.4.5
# homeassistant.components.html5
pywebpush==1.9.2
@ -1762,7 +1759,7 @@ samsungtvws[async,encrypted]==2.6.0
scapy==2.5.0
# homeassistant.components.screenlogic
screenlogicpy==0.9.1
screenlogicpy==0.9.2
# homeassistant.components.backup
securetar==2023.3.0
@ -1877,7 +1874,7 @@ surepy==0.8.0
switchbot-api==1.1.0
# homeassistant.components.system_bridge
systembridgeconnector==3.8.2
systembridgeconnector==3.8.4
# homeassistant.components.tailscale
tailscale==0.2.0

View File

@ -615,5 +615,5 @@ async def test_airzone_climate_set_temp_range(hass: HomeAssistant) -> None:
)
state = hass.states.get("climate.dkn_plus")
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 20.0
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 20.0
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 25.0

View File

@ -181,8 +181,7 @@ class MockWakeWordEntity(wake_word.WakeWordDetectionEntity):
url_path = "wake_word.test"
_attr_name = "test"
@property
def supported_wake_words(self) -> list[wake_word.WakeWord]:
async def get_supported_wake_words(self) -> list[wake_word.WakeWord]:
"""Return a list of supported wake words."""
return [wake_word.WakeWord(id="test_ww", name="Test Wake Word")]
@ -191,7 +190,7 @@ class MockWakeWordEntity(wake_word.WakeWordDetectionEntity):
) -> wake_word.DetectionResult | None:
"""Try to detect wake word(s) in an audio stream with timestamps."""
if wake_word_id is None:
wake_word_id = self.supported_wake_words[0].id
wake_word_id = (await self.get_supported_wake_words())[0].id
async for chunk, timestamp in stream:
if chunk.startswith(b"wake word"):
return wake_word.DetectionResult(

View File

@ -824,6 +824,11 @@
}),
'has_combustion_drivetrain': False,
'has_electric_drivetrain': True,
'headunit': dict({
'headunit_type': 'MGU',
'idrive_version': 'ID8',
'software_version': '07/2021.00',
}),
'is_charging_plan_supported': True,
'is_lsc_enabled': True,
'is_remote_charge_start_enabled': True,
@ -1685,6 +1690,11 @@
}),
'has_combustion_drivetrain': False,
'has_electric_drivetrain': True,
'headunit': dict({
'headunit_type': 'MGU',
'idrive_version': 'ID8',
'software_version': '11/2021.70',
}),
'is_charging_plan_supported': True,
'is_lsc_enabled': True,
'is_remote_charge_start_enabled': False,
@ -2318,6 +2328,11 @@
}),
'has_combustion_drivetrain': True,
'has_electric_drivetrain': False,
'headunit': dict({
'headunit_type': 'MGU',
'idrive_version': 'ID7',
'software_version': '07/2021.70',
}),
'is_charging_plan_supported': False,
'is_lsc_enabled': True,
'is_remote_charge_start_enabled': False,
@ -3015,6 +3030,11 @@
}),
'has_combustion_drivetrain': True,
'has_electric_drivetrain': True,
'headunit': dict({
'headunit_type': 'NBT',
'idrive_version': 'ID4',
'software_version': '11/2021.10',
}),
'is_charging_plan_supported': True,
'is_lsc_enabled': True,
'is_remote_charge_start_enabled': False,
@ -5346,6 +5366,11 @@
}),
'has_combustion_drivetrain': True,
'has_electric_drivetrain': True,
'headunit': dict({
'headunit_type': 'NBT',
'idrive_version': 'ID4',
'software_version': '11/2021.10',
}),
'is_charging_plan_supported': True,
'is_lsc_enabled': True,
'is_remote_charge_start_enabled': False,

View File

@ -6,6 +6,7 @@ from typing import Any
import pytest
from homeassistant.components.energy import data
from homeassistant.components.recorder.util import session_scope
from homeassistant.components.sensor import (
ATTR_LAST_RESET,
ATTR_STATE_CLASS,
@ -155,7 +156,10 @@ async def test_cost_sensor_price_entity_total_increasing(
"""Test energy cost price from total_increasing type sensor entity."""
def _compile_statistics(_):
return compile_statistics(hass, now, now + timedelta(seconds=1)).platform_stats
with session_scope(hass=hass) as session:
return compile_statistics(
hass, session, now, now + timedelta(seconds=1)
).platform_stats
energy_attributes = {
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
@ -365,8 +369,9 @@ async def test_cost_sensor_price_entity_total(
"""Test energy cost price from total type sensor entity."""
def _compile_statistics(_):
with session_scope(hass=hass) as session:
return compile_statistics(
hass, now, now + timedelta(seconds=0.17)
hass, session, now, now + timedelta(seconds=0.17)
).platform_stats
energy_attributes = {
@ -579,7 +584,10 @@ async def test_cost_sensor_price_entity_total_no_reset(
"""Test energy cost price from total type sensor entity with no last_reset."""
def _compile_statistics(_):
return compile_statistics(hass, now, now + timedelta(seconds=1)).platform_stats
with session_scope(hass=hass) as session:
return compile_statistics(
hass, session, now, now + timedelta(seconds=1)
).platform_stats
energy_attributes = {
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,

View File

@ -576,6 +576,59 @@ async def test_add_event_date_time(
}
@pytest.mark.parametrize(
"calendars_config",
[
[
{
"cal_id": CALENDAR_ID,
"entities": [
{
"device_id": "backyard_light",
"name": "Backyard Light",
"search": "#Backyard",
},
],
}
],
],
)
async def test_unsupported_create_event(
hass: HomeAssistant,
mock_calendars_yaml: Mock,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
mock_insert_event: Callable[[str, dict[str, Any]], None],
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test create event service call is unsupported for virtual calendars."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina"))
delta = datetime.timedelta(days=3, hours=3)
end_datetime = start_datetime + delta
with pytest.raises(HomeAssistantError, match="does not support this service"):
await hass.services.async_call(
DOMAIN,
"create_event",
{
# **data,
"start_date_time": start_datetime.isoformat(),
"end_date_time": end_datetime.isoformat(),
"summary": TEST_EVENT_SUMMARY,
"description": TEST_EVENT_DESCRIPTION,
},
target={"entity_id": "calendar.backyard_light"},
blocking=True,
)
async def test_add_event_failure(
hass: HomeAssistant,
component_setup: ComponentSetup,

View File

@ -2,6 +2,7 @@
from unittest.mock import patch
from bleak import BleakError
from idasen_ha import AuthFailedError
import pytest
from homeassistant import config_entries
@ -89,7 +90,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None:
async def test_user_step_cannot_connect(
hass: HomeAssistant, exception: Exception
) -> None:
"""Test user step and we cannot connect."""
"""Test user step with a cannot connect error."""
with patch(
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
return_value=[IDASEN_DISCOVERY_INFO],
@ -140,6 +141,58 @@ async def test_user_step_cannot_connect(
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_step_auth_failed(hass: HomeAssistant) -> None:
"""Test user step with an auth failed error."""
with patch(
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
return_value=[IDASEN_DISCOVERY_INFO],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
with patch(
"homeassistant.components.idasen_desk.config_flow.Desk.connect",
side_effect=AuthFailedError,
), patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "auth_failed"}
with patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), patch(
"homeassistant.components.idasen_desk.config_flow.Desk.disconnect"
), patch(
"homeassistant.components.idasen_desk.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
},
)
await hass.async_block_till_done()
assert result3["type"] == FlowResultType.CREATE_ENTRY
assert result3["title"] == IDASEN_DISCOVERY_INFO.name
assert result3["data"] == {
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
}
assert result3["result"].unique_id == IDASEN_DISCOVERY_INFO.address
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_step_unknown_exception(hass: HomeAssistant) -> None:
"""Test user step with an unknown exception."""
with patch(

View File

@ -1,80 +1 @@
"""Tests for the Mazda Connected Services integration."""
import json
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from pymazda import Client as MazdaAPI
from homeassistant.components.mazda.const import DOMAIN
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from tests.common import MockConfigEntry, load_fixture
FIXTURE_USER_INPUT = {
CONF_EMAIL: "example@example.com",
CONF_PASSWORD: "password",
CONF_REGION: "MNAO",
}
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")
)
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)
client_mock = MagicMock(
MazdaAPI(
FIXTURE_USER_INPUT[CONF_EMAIL],
FIXTURE_USER_INPUT[CONF_PASSWORD],
FIXTURE_USER_INPUT[CONF_REGION],
aiohttp_client.async_get_clientsession(hass),
)
)
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()
client_mock.start_charging = AsyncMock()
client_mock.start_engine = AsyncMock()
client_mock.stop_charging = AsyncMock()
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",
return_value=client_mock,
), patch("homeassistant.components.mazda.MazdaAPI", return_value=client_mock):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return client_mock

View File

@ -1,62 +0,0 @@
{
"info": {
"email": "**REDACTED**",
"password": "**REDACTED**",
"region": "MNAO"
},
"data": [
{
"vin": "**REDACTED**",
"id": "**REDACTED**",
"nickname": "My Mazda3",
"carlineCode": "M3S",
"carlineName": "MAZDA3 2.5 S SE AWD",
"modelYear": "2021",
"modelCode": "M3S SE XA",
"modelName": "W/ SELECT PKG AWD SDN",
"automaticTransmission": true,
"interiorColorCode": "BY3",
"interiorColorName": "BLACK",
"exteriorColorCode": "42M",
"exteriorColorName": "DEEP CRYSTAL BLUE MICA",
"isElectric": false,
"status": {
"lastUpdatedTimestamp": "20210123143809",
"latitude": "**REDACTED**",
"longitude": "**REDACTED**",
"positionTimestamp": "20210123143808",
"fuelRemainingPercent": 87.0,
"fuelDistanceRemainingKm": 380.8,
"odometerKm": 2795.8,
"doors": {
"driverDoorOpen": false,
"passengerDoorOpen": true,
"rearLeftDoorOpen": false,
"rearRightDoorOpen": false,
"trunkOpen": false,
"hoodOpen": true,
"fuelLidOpen": false
},
"doorLocks": {
"driverDoorUnlocked": false,
"passengerDoorUnlocked": false,
"rearLeftDoorUnlocked": false,
"rearRightDoorUnlocked": false
},
"windows": {
"driverWindowOpen": false,
"passengerWindowOpen": false,
"rearLeftWindowOpen": false,
"rearRightWindowOpen": false
},
"hazardLightsOn": false,
"tirePressure": {
"frontLeftTirePressurePsi": 35.0,
"frontRightTirePressurePsi": 35.0,
"rearLeftTirePressurePsi": 33.0,
"rearRightTirePressurePsi": 33.0
}
}
}
]
}

View File

@ -1,60 +0,0 @@
{
"info": {
"email": "**REDACTED**",
"password": "**REDACTED**",
"region": "MNAO"
},
"data": {
"vin": "**REDACTED**",
"id": "**REDACTED**",
"nickname": "My Mazda3",
"carlineCode": "M3S",
"carlineName": "MAZDA3 2.5 S SE AWD",
"modelYear": "2021",
"modelCode": "M3S SE XA",
"modelName": "W/ SELECT PKG AWD SDN",
"automaticTransmission": true,
"interiorColorCode": "BY3",
"interiorColorName": "BLACK",
"exteriorColorCode": "42M",
"exteriorColorName": "DEEP CRYSTAL BLUE MICA",
"isElectric": false,
"status": {
"lastUpdatedTimestamp": "20210123143809",
"latitude": "**REDACTED**",
"longitude": "**REDACTED**",
"positionTimestamp": "20210123143808",
"fuelRemainingPercent": 87.0,
"fuelDistanceRemainingKm": 380.8,
"odometerKm": 2795.8,
"doors": {
"driverDoorOpen": false,
"passengerDoorOpen": true,
"rearLeftDoorOpen": false,
"rearRightDoorOpen": false,
"trunkOpen": false,
"hoodOpen": true,
"fuelLidOpen": false
},
"doorLocks": {
"driverDoorUnlocked": false,
"passengerDoorUnlocked": false,
"rearLeftDoorUnlocked": false,
"rearRightDoorUnlocked": false
},
"windows": {
"driverWindowOpen": false,
"passengerWindowOpen": false,
"rearLeftWindowOpen": false,
"rearRightWindowOpen": false
},
"hazardLightsOn": false,
"tirePressure": {
"frontLeftTirePressurePsi": 35.0,
"frontRightTirePressurePsi": 35.0,
"rearLeftTirePressurePsi": 33.0,
"rearRightTirePressurePsi": 33.0
}
}
}
}

View File

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

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

View File

@ -1,37 +0,0 @@
{
"lastUpdatedTimestamp": "20210123143809",
"latitude": 1.234567,
"longitude": -2.345678,
"positionTimestamp": "20210123143808",
"fuelRemainingPercent": 87.0,
"fuelDistanceRemainingKm": 380.8,
"odometerKm": 2795.8,
"doors": {
"driverDoorOpen": false,
"passengerDoorOpen": true,
"rearLeftDoorOpen": false,
"rearRightDoorOpen": false,
"trunkOpen": false,
"hoodOpen": true,
"fuelLidOpen": false
},
"doorLocks": {
"driverDoorUnlocked": false,
"passengerDoorUnlocked": false,
"rearLeftDoorUnlocked": false,
"rearRightDoorUnlocked": false
},
"windows": {
"driverWindowOpen": false,
"passengerWindowOpen": false,
"rearLeftWindowOpen": false,
"rearRightWindowOpen": false
},
"hazardLightsOn": false,
"tirePressure": {
"frontLeftTirePressurePsi": 35.0,
"frontRightTirePressurePsi": 35.0,
"rearLeftTirePressurePsi": 33.0,
"rearRightTirePressurePsi": 33.0
}
}

View File

@ -1,18 +0,0 @@
[
{
"vin": "JM000000000000000",
"id": 12345,
"nickname": "My Mazda3",
"carlineCode": "M3S",
"carlineName": "MAZDA3 2.5 S SE AWD",
"modelYear": "2021",
"modelCode": "M3S SE XA",
"modelName": "W/ SELECT PKG AWD SDN",
"automaticTransmission": true,
"interiorColorCode": "BY3",
"interiorColorName": "BLACK",
"exteriorColorCode": "42M",
"exteriorColorName": "DEEP CRYSTAL BLUE MICA",
"isElectric": false
}
]

View File

@ -1,98 +0,0 @@
"""The binary sensor tests for the Mazda Connected Services integration."""
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_binary_sensors(hass: HomeAssistant) -> None:
"""Test creation of the binary sensors."""
await init_integration(hass)
entity_registry = er.async_get(hass)
# Driver Door
state = hass.states.get("binary_sensor.my_mazda3_driver_door")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver door"
assert state.attributes.get(ATTR_ICON) == "mdi:car-door"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "off"
entry = entity_registry.async_get("binary_sensor.my_mazda3_driver_door")
assert entry
assert entry.unique_id == "JM000000000000000_driver_door"
# Passenger Door
state = hass.states.get("binary_sensor.my_mazda3_passenger_door")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger door"
assert state.attributes.get(ATTR_ICON) == "mdi:car-door"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "on"
entry = entity_registry.async_get("binary_sensor.my_mazda3_passenger_door")
assert entry
assert entry.unique_id == "JM000000000000000_passenger_door"
# Rear Left Door
state = hass.states.get("binary_sensor.my_mazda3_rear_left_door")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left door"
assert state.attributes.get(ATTR_ICON) == "mdi:car-door"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "off"
entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_left_door")
assert entry
assert entry.unique_id == "JM000000000000000_rear_left_door"
# Rear Right Door
state = hass.states.get("binary_sensor.my_mazda3_rear_right_door")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right door"
assert state.attributes.get(ATTR_ICON) == "mdi:car-door"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "off"
entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_right_door")
assert entry
assert entry.unique_id == "JM000000000000000_rear_right_door"
# Trunk
state = hass.states.get("binary_sensor.my_mazda3_trunk")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Trunk"
assert state.attributes.get(ATTR_ICON) == "mdi:car-back"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "off"
entry = entity_registry.async_get("binary_sensor.my_mazda3_trunk")
assert entry
assert entry.unique_id == "JM000000000000000_trunk"
# Hood
state = hass.states.get("binary_sensor.my_mazda3_hood")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Hood"
assert state.attributes.get(ATTR_ICON) == "mdi:car"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR
assert state.state == "on"
entry = entity_registry.async_get("binary_sensor.my_mazda3_hood")
assert entry
assert entry.unique_id == "JM000000000000000_hood"
async def test_electric_vehicle_binary_sensors(hass: HomeAssistant) -> None:
"""Test sensors which are specific to electric vehicles."""
await init_integration(hass, electric_vehicle=True)
entity_registry = er.async_get(hass)
# Plugged In
state = hass.states.get("binary_sensor.my_mazda3_plugged_in")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged in"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG
assert state.state == "on"
entry = entity_registry.async_get("binary_sensor.my_mazda3_plugged_in")
assert entry
assert entry.unique_id == "JM000000000000000_ev_plugged_in"

View File

@ -1,145 +0,0 @@
"""The button tests for the Mazda Connected Services integration."""
from pymazda import MazdaException
import pytest
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_button_setup_non_electric_vehicle(hass: HomeAssistant) -> None:
"""Test creation of button entities."""
await init_integration(hass)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("button.my_mazda3_start_engine")
assert entry
assert entry.unique_id == "JM000000000000000_start_engine"
state = hass.states.get("button.my_mazda3_start_engine")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine"
assert state.attributes.get(ATTR_ICON) == "mdi:engine"
entry = entity_registry.async_get("button.my_mazda3_stop_engine")
assert entry
assert entry.unique_id == "JM000000000000000_stop_engine"
state = hass.states.get("button.my_mazda3_stop_engine")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine"
assert state.attributes.get(ATTR_ICON) == "mdi:engine-off"
entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights")
assert entry
assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights"
state = hass.states.get("button.my_mazda3_turn_on_hazard_lights")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights"
assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights"
entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights")
assert entry
assert entry.unique_id == "JM000000000000000_turn_off_hazard_lights"
state = hass.states.get("button.my_mazda3_turn_off_hazard_lights")
assert state
assert (
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights"
)
assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights"
# Since this is a non-electric vehicle, electric vehicle buttons should not be created
entry = entity_registry.async_get("button.my_mazda3_refresh_vehicle_status")
assert entry is None
state = hass.states.get("button.my_mazda3_refresh_vehicle_status")
assert state is None
async def test_button_setup_electric_vehicle(hass: HomeAssistant) -> None:
"""Test creation of button entities for an electric vehicle."""
await init_integration(hass, electric_vehicle=True)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("button.my_mazda3_refresh_status")
assert entry
assert entry.unique_id == "JM000000000000000_refresh_vehicle_status"
state = hass.states.get("button.my_mazda3_refresh_status")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh status"
assert state.attributes.get(ATTR_ICON) == "mdi:refresh"
@pytest.mark.parametrize(
("electric_vehicle", "entity_id_suffix"),
[
(True, "start_engine"),
(True, "stop_engine"),
(True, "turn_on_hazard_lights"),
(True, "turn_off_hazard_lights"),
(False, "refresh_status"),
],
)
async def test_button_not_created(
hass: HomeAssistant, electric_vehicle, entity_id_suffix
) -> None:
"""Test that button entities are not created when they should not be."""
await init_integration(hass, electric_vehicle=electric_vehicle)
entity_registry = er.async_get(hass)
entity_id = f"button.my_mazda3_{entity_id_suffix}"
entry = entity_registry.async_get(entity_id)
assert entry is None
state = hass.states.get(entity_id)
assert state is None
@pytest.mark.parametrize(
("electric_vehicle", "entity_id_suffix", "api_method_name"),
[
(False, "start_engine", "start_engine"),
(False, "stop_engine", "stop_engine"),
(False, "turn_on_hazard_lights", "turn_on_hazard_lights"),
(False, "turn_off_hazard_lights", "turn_off_hazard_lights"),
(True, "refresh_status", "refresh_vehicle_status"),
],
)
async def test_button_press(
hass: HomeAssistant, electric_vehicle, entity_id_suffix, api_method_name
) -> None:
"""Test pressing the button entities."""
client_mock = await init_integration(hass, electric_vehicle=electric_vehicle)
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: f"button.my_mazda3_{entity_id_suffix}"},
blocking=True,
)
await hass.async_block_till_done()
api_method = getattr(client_mock, api_method_name)
api_method.assert_called_once_with(12345)
async def test_button_press_error(hass: HomeAssistant) -> None:
"""Test the Mazda API raising an error when a button entity is pressed."""
client_mock = await init_integration(hass)
client_mock.start_engine.side_effect = MazdaException("Test error")
with pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.my_mazda3_start_engine"},
blocking=True,
)
await hass.async_block_till_done()
assert str(err.value) == "Test error"

View File

@ -1,341 +0,0 @@
"""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.core import HomeAssistant
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: HomeAssistant) -> None:
"""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: HomeAssistant,
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,
) -> None:
"""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: HomeAssistant, hvac_mode, api_method) -> None:
"""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: HomeAssistant) -> None:
"""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: HomeAssistant, preset_mode, front_defroster, rear_defroster
) -> None:
"""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
)

View File

@ -1,423 +0,0 @@
"""Test the Mazda Connected Services config flow."""
from unittest.mock import patch
import aiohttp
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.mazda.config_flow import (
MazdaAccountLockedException,
MazdaAuthenticationException,
)
from homeassistant.components.mazda.const import DOMAIN
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
FIXTURE_USER_INPUT = {
CONF_EMAIL: "example@example.com",
CONF_PASSWORD: "password",
CONF_REGION: "MNAO",
}
FIXTURE_USER_INPUT_REAUTH = {
CONF_EMAIL: "example@example.com",
CONF_PASSWORD: "password_fixed",
CONF_REGION: "MNAO",
}
FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL = {
CONF_EMAIL: "example2@example.com",
CONF_PASSWORD: "password_fixed",
CONF_REGION: "MNAO",
}
async def test_form(hass: HomeAssistant) -> None:
"""Test the entire flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
return_value=True,
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT,
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == FIXTURE_USER_INPUT[CONF_EMAIL]
assert result2["data"] == FIXTURE_USER_INPUT
assert len(mock_setup_entry.mock_calls) == 1
async def test_account_already_exists(hass: HomeAssistant) -> None:
"""Test account already exists."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
return_value=True,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT,
)
await hass.async_block_till_done()
assert result2["type"] == "abort"
assert result2["reason"] == "already_configured"
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=MazdaAuthenticationException("Failed to authenticate"),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT,
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_account_locked(hass: HomeAssistant) -> None:
"""Test we handle account locked error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=MazdaAccountLockedException("Account locked"),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT,
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "account_locked"}
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=aiohttp.ClientError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT,
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_error(hass: HomeAssistant) -> None:
"""Test we handle unknown error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=Exception,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT,
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}
async def test_reauth_flow(hass: HomeAssistant) -> None:
"""Test reauth works."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=MazdaAuthenticationException("Failed to authenticate"),
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
return_value=True,
), patch("homeassistant.components.mazda.async_setup_entry", return_value=True):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT_REAUTH,
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"
async def test_reauth_authorization_error(hass: HomeAssistant) -> None:
"""Test we show user form on authorization error."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=MazdaAuthenticationException("Failed to authenticate"),
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT_REAUTH,
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "invalid_auth"}
async def test_reauth_account_locked(hass: HomeAssistant) -> None:
"""Test we show user form on account_locked error."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=MazdaAccountLockedException("Account locked"),
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT_REAUTH,
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "account_locked"}
async def test_reauth_connection_error(hass: HomeAssistant) -> None:
"""Test we show user form on connection error."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=aiohttp.ClientError,
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT_REAUTH,
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_reauth_unknown_error(hass: HomeAssistant) -> None:
"""Test we show user form on unknown error."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
side_effect=Exception,
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT_REAUTH,
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "unknown"}
async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None:
"""Test reauth with a new email address but same account."""
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
data=FIXTURE_USER_INPUT,
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
return_value=True,
), patch(
"homeassistant.components.mazda.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config.entry_id,
},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
# Change the email and ensure the entry and its unique id gets
# updated in the event the user has changed their email with mazda
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL,
)
await hass.async_block_till_done()
assert (
mock_config.unique_id == FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL[CONF_EMAIL]
)
assert result2["type"] == data_entry_flow.FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"

View File

@ -1,30 +0,0 @@
"""The device tracker tests for the Mazda Connected Services integration."""
from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SourceType
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_LATITUDE,
ATTR_LONGITUDE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_device_tracker(hass: HomeAssistant) -> None:
"""Test creation of the device tracker."""
await init_integration(hass)
entity_registry = er.async_get(hass)
state = hass.states.get("device_tracker.my_mazda3_device_tracker")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device tracker"
assert state.attributes.get(ATTR_ICON) == "mdi:car"
assert state.attributes.get(ATTR_LATITUDE) == 1.234567
assert state.attributes.get(ATTR_LONGITUDE) == -2.345678
assert state.attributes.get(ATTR_SOURCE_TYPE) == SourceType.GPS
entry = entity_registry.async_get("device_tracker.my_mazda3_device_tracker")
assert entry
assert entry.unique_id == "JM000000000000000"

View File

@ -1,81 +0,0 @@
"""Test Mazda diagnostics."""
import json
import pytest
from homeassistant.components.mazda.const import DATA_COORDINATOR, DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import init_integration
from tests.common import load_fixture
from tests.components.diagnostics import (
get_diagnostics_for_config_entry,
get_diagnostics_for_device,
)
from tests.typing import ClientSessionGenerator
async def test_config_entry_diagnostics(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test config entry diagnostics."""
await init_integration(hass)
assert hass.data[DOMAIN]
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
diagnostics_fixture = json.loads(
load_fixture("mazda/diagnostics_config_entry.json")
)
assert (
await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
== diagnostics_fixture
)
async def test_device_diagnostics(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test device diagnostics."""
await init_integration(hass)
assert hass.data[DOMAIN]
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
assert reg_device is not None
diagnostics_fixture = json.loads(load_fixture("mazda/diagnostics_device.json"))
assert (
await get_diagnostics_for_device(hass, hass_client, config_entry, reg_device)
== diagnostics_fixture
)
async def test_device_diagnostics_vehicle_not_found(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test device diagnostics when the vehicle cannot be found."""
await init_integration(hass)
assert hass.data[DOMAIN]
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
assert reg_device is not None
# Remove vehicle info from hass.data so that vehicle will not be found
hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR].data = []
with pytest.raises(AssertionError):
await get_diagnostics_for_device(hass, hass_client, config_entry, reg_device)

View File

@ -1,365 +1,50 @@
"""Tests for the Mazda Connected Services integration."""
from datetime import timedelta
import json
from unittest.mock import patch
from pymazda import MazdaAuthenticationException, MazdaException
import pytest
import voluptuous as vol
from homeassistant.components.mazda.const import DOMAIN
from homeassistant.components.mazda import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_EMAIL,
CONF_PASSWORD,
CONF_REGION,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.util import dt as dt_util
from homeassistant.helpers import issue_registry as ir
from . import init_integration
from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
FIXTURE_USER_INPUT = {
CONF_EMAIL: "example@example.com",
CONF_PASSWORD: "password",
CONF_REGION: "MNAO",
}
from tests.common import MockConfigEntry
async def test_config_entry_not_ready(hass: HomeAssistant) -> None:
"""Test the Mazda configuration entry not ready."""
config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
side_effect=MazdaException("Unknown error"),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_init_auth_failure(hass: HomeAssistant) -> None:
"""Test auth failure during setup."""
with patch(
"homeassistant.components.mazda.MazdaAPI.validate_credentials",
side_effect=MazdaAuthenticationException("Login failed"),
):
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 is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "user"
async def test_update_auth_failure(hass: HomeAssistant) -> None:
"""Test auth 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 is ConfigEntryState.LOADED
with patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
side_effect=MazdaAuthenticationException("Login failed"),
):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181))
await hass.async_block_till_done()
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "user"
async def test_update_general_failure(hass: HomeAssistant) -> None:
"""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 is ConfigEntryState.LOADED
with patch(
"homeassistant.components.mazda.MazdaAPI.get_vehicles",
side_effect=Exception("Unknown exception"),
):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181))
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."""
await init_integration(hass)
assert hass.data[DOMAIN]
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(entries[0].entry_id)
await hass.async_block_till_done()
assert entries[0].state is ConfigEntryState.NOT_LOADED
async def test_init_electric_vehicle(hass: HomeAssistant) -> None:
"""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: HomeAssistant) -> None:
"""Test creation of the device when vehicle has a nickname."""
await init_integration(hass, use_nickname=True)
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD"
assert reg_device.manufacturer == "Mazda"
assert reg_device.name == "My Mazda3"
async def test_device_no_nickname(hass: HomeAssistant) -> None:
"""Test creation of the device when vehicle has no nickname."""
await init_integration(hass, use_nickname=False)
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD"
assert reg_device.manufacturer == "Mazda"
assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD"
@pytest.mark.parametrize(
("service", "service_data", "expected_args"),
[
(
"send_poi",
{"latitude": 1.2345, "longitude": 2.3456, "poi_name": "Work"},
[12345, 1.2345, 2.3456, "Work"],
),
],
)
async def test_services(
hass: HomeAssistant, service, service_data, expected_args
async def test_mazda_repair_issue(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test service calls."""
client_mock = await init_integration(hass)
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
"""Test the Mazda configuration entry loading/unloading handles the repair."""
config_entry_1 = MockConfigEntry(
title="Example 1",
domain=DOMAIN,
)
device_id = reg_device.id
config_entry_1.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry_1.entry_id)
await hass.async_block_till_done()
assert config_entry_1.state is ConfigEntryState.LOADED
service_data["device_id"] = device_id
await hass.services.async_call(DOMAIN, service, service_data, blocking=True)
# Add a second one
config_entry_2 = MockConfigEntry(
title="Example 2",
domain=DOMAIN,
)
config_entry_2.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry_2.entry_id)
await hass.async_block_till_done()
api_method = getattr(client_mock, service)
api_method.assert_called_once_with(*expected_args)
assert config_entry_2.state is ConfigEntryState.LOADED
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
async def test_service_invalid_device_id(hass: HomeAssistant) -> None:
"""Test service call when the specified device ID is invalid."""
await init_integration(hass)
with pytest.raises(vol.error.MultipleInvalid) as err:
await hass.services.async_call(
DOMAIN,
"send_poi",
{
"device_id": "invalid",
"latitude": 1.2345,
"longitude": 6.7890,
"poi_name": "poi_name",
},
blocking=True,
)
# Remove the first one
await hass.config_entries.async_remove(config_entry_1.entry_id)
await hass.async_block_till_done()
assert "Invalid device ID" in str(err.value)
assert config_entry_1.state is ConfigEntryState.NOT_LOADED
assert config_entry_2.state is ConfigEntryState.LOADED
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
async def test_service_device_id_not_mazda_vehicle(hass: HomeAssistant) -> None:
"""Test service call when the specified device ID is not the device ID of a Mazda vehicle."""
await init_integration(hass)
device_registry = dr.async_get(hass)
# Create another device and pass its device ID.
# Service should fail because device is from wrong domain.
other_config_entry = MockConfigEntry()
other_config_entry.add_to_hass(hass)
other_device = device_registry.async_get_or_create(
config_entry_id=other_config_entry.entry_id,
identifiers={("OTHER_INTEGRATION", "ID_FROM_OTHER_INTEGRATION")},
)
with pytest.raises(vol.error.MultipleInvalid) as err:
await hass.services.async_call(
DOMAIN,
"send_poi",
{
"device_id": other_device.id,
"latitude": 1.2345,
"longitude": 6.7890,
"poi_name": "poi_name",
},
blocking=True,
)
# Remove the second one
await hass.config_entries.async_remove(config_entry_2.entry_id)
await hass.async_block_till_done()
assert "Device ID is not a Mazda vehicle" in str(err.value)
async def test_service_vehicle_id_not_found(hass: HomeAssistant) -> None:
"""Test service call when the vehicle ID is not found."""
await init_integration(hass)
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
device_id = reg_device.id
entries = hass.config_entries.async_entries(DOMAIN)
entry_id = entries[0].entry_id
# Remove vehicle info from hass.data so that vehicle ID will not be found
hass.data[DOMAIN][entry_id]["vehicles"] = []
with pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
DOMAIN,
"send_poi",
{
"device_id": device_id,
"latitude": 1.2345,
"longitude": 6.7890,
"poi_name": "poi_name",
},
blocking=True,
)
await hass.async_block_till_done()
assert str(err.value) == "Vehicle ID not found"
async def test_service_mazda_api_error(hass: HomeAssistant) -> None:
"""Test the Mazda API raising an error when a service is called."""
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()
device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={(DOMAIN, "JM000000000000000")},
)
device_id = reg_device.id
with patch(
"homeassistant.components.mazda.MazdaAPI.send_poi",
side_effect=MazdaException("Test error"),
), pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
DOMAIN,
"send_poi",
{
"device_id": device_id,
"latitude": 1.2345,
"longitude": 6.7890,
"poi_name": "poi_name",
},
blocking=True,
)
await hass.async_block_till_done()
assert str(err.value) == "Test error"
assert config_entry_1.state is ConfigEntryState.NOT_LOADED
assert config_entry_2.state is ConfigEntryState.NOT_LOADED
assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None

View File

@ -1,58 +0,0 @@
"""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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_lock_setup(hass: HomeAssistant) -> None:
"""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: HomeAssistant) -> None:
"""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: HomeAssistant) -> None:
"""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()

View File

@ -1,195 +0,0 @@
"""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,
PERCENTAGE,
UnitOfLength,
UnitOfPressure,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from . import init_integration
async def test_sensors(hass: HomeAssistant) -> None:
"""Test creation of the sensors."""
await init_integration(hass)
entity_registry = er.async_get(hass)
# Fuel Remaining Percentage
state = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage")
assert state
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "My Mazda3 Fuel remaining percentage"
)
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
assert entry.unique_id == "JM000000000000000_fuel_remaining_percentage"
# Fuel Distance Remaining
state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining")
assert state
assert (
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel distance remaining"
)
assert state.attributes.get(ATTR_ICON) == "mdi:gas-station"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.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
assert entry.unique_id == "JM000000000000000_fuel_distance_remaining"
# Odometer
state = hass.states.get("sensor.my_mazda3_odometer")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Odometer"
assert state.attributes.get(ATTR_ICON) == "mdi:speedometer"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING
assert state.state == "2795"
entry = entity_registry.async_get("sensor.my_mazda3_odometer")
assert entry
assert entry.unique_id == "JM000000000000000_odometer"
# Front Left Tire Pressure
state = hass.states.get("sensor.my_mazda3_front_left_tire_pressure")
assert state
assert (
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front left tire pressure"
)
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "241"
entry = entity_registry.async_get("sensor.my_mazda3_front_left_tire_pressure")
assert entry
assert entry.unique_id == "JM000000000000000_front_left_tire_pressure"
# Front Right Tire Pressure
state = hass.states.get("sensor.my_mazda3_front_right_tire_pressure")
assert state
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "My Mazda3 Front right tire pressure"
)
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "241"
entry = entity_registry.async_get("sensor.my_mazda3_front_right_tire_pressure")
assert entry
assert entry.unique_id == "JM000000000000000_front_right_tire_pressure"
# Rear Left Tire Pressure
state = hass.states.get("sensor.my_mazda3_rear_left_tire_pressure")
assert state
assert (
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left tire pressure"
)
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "228"
entry = entity_registry.async_get("sensor.my_mazda3_rear_left_tire_pressure")
assert entry
assert entry.unique_id == "JM000000000000000_rear_left_tire_pressure"
# Rear Right Tire Pressure
state = hass.states.get("sensor.my_mazda3_rear_right_tire_pressure")
assert state
assert (
state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right tire pressure"
)
assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.state == "228"
entry = entity_registry.async_get("sensor.my_mazda3_rear_right_tire_pressure")
assert entry
assert entry.unique_id == "JM000000000000000_rear_right_tire_pressure"
async def test_sensors_us_customary_units(hass: HomeAssistant) -> None:
"""Test that the sensors work properly with US customary units."""
hass.config.units = US_CUSTOMARY_SYSTEM
await init_integration(hass)
# In the US, miles are used for vehicle odometers.
# These tests verify that the unit conversion logic for the distance
# sensor device class automatically converts the unit to miles.
# Fuel Distance Remaining
state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining")
assert state
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES
assert state.state == "237"
# Odometer
state = hass.states.get("sensor.my_mazda3_odometer")
assert state
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES
assert state.state == "1737"
async def test_electric_vehicle_sensors(hass: HomeAssistant) -> None:
"""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_DEVICE_CLASS) == SensorDeviceClass.DISTANCE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.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"

View File

@ -1,69 +0,0 @@
"""The switch tests for the Mazda Connected Services integration."""
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def test_switch_setup(hass: HomeAssistant) -> None:
"""Test setup of the switch entity."""
await init_integration(hass, electric_vehicle=True)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("switch.my_mazda3_charging")
assert entry
assert entry.unique_id == "JM000000000000000"
state = hass.states.get("switch.my_mazda3_charging")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charging"
assert state.attributes.get(ATTR_ICON) == "mdi:ev-station"
assert state.state == STATE_ON
async def test_start_charging(hass: HomeAssistant) -> None:
"""Test turning on the charging switch."""
client_mock = await init_integration(hass, electric_vehicle=True)
client_mock.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.my_mazda3_charging"},
blocking=True,
)
await hass.async_block_till_done()
client_mock.start_charging.assert_called_once()
client_mock.refresh_vehicle_status.assert_called_once()
client_mock.get_vehicle_status.assert_called_once()
client_mock.get_ev_vehicle_status.assert_called_once()
async def test_stop_charging(hass: HomeAssistant) -> None:
"""Test turning off the charging switch."""
client_mock = await init_integration(hass, electric_vehicle=True)
client_mock.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.my_mazda3_charging"},
blocking=True,
)
await hass.async_block_till_done()
client_mock.stop_charging.assert_called_once()
client_mock.refresh_vehicle_status.assert_called_once()
client_mock.get_vehicle_status.assert_called_once()
client_mock.get_ev_vehicle_status.assert_called_once()

View File

@ -123,7 +123,6 @@ async def test_setting_sensor_value_expires_availability_topic(
"name": "test",
"state_topic": "test-topic",
"expire_after": 4,
"force_update": True,
}
}
}
@ -200,6 +199,18 @@ async def expires_helper(hass: HomeAssistant) -> None:
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_UNAVAILABLE
# Send the last message again
# Time jump 0.5s
now += timedelta(seconds=0.5)
freezer.move_to(now)
async_fire_time_changed(hass, now)
async_fire_mqtt_message(hass, "test-topic", "OFF")
await hass.async_block_till_done()
# Value was updated correctly.
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor(
hass: HomeAssistant,

View File

@ -339,7 +339,6 @@ async def test_setting_sensor_value_expires_availability_topic(
"state_topic": "test-topic",
"unit_of_measurement": "fav unit",
"expire_after": "4",
"force_update": True,
}
}
}
@ -413,6 +412,18 @@ async def expires_helper(hass: HomeAssistant) -> None:
state = hass.states.get("sensor.test")
assert state.state == STATE_UNAVAILABLE
# Send the last message again
# Time jump 0.5s
now += timedelta(seconds=0.5)
freezer.move_to(now)
async_fire_time_changed(hass, now)
async_fire_mqtt_message(hass, "test-topic", "101")
await hass.async_block_till_done()
# Value was updated correctly.
state = hass.states.get("sensor.test")
assert state.state == "101"
@pytest.mark.parametrize(
"hass_config",

View File

@ -6,7 +6,7 @@ from collections.abc import Callable
from mysensors.sensor import Sensor
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import ATTR_DEVICE_CLASS
from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS
from homeassistant.core import HomeAssistant
@ -23,6 +23,7 @@ async def test_door_sensor(
assert state
assert state.state == "off"
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
receive_message("1;1;1;0;16;1\n")
await hass.async_block_till_done()

View File

@ -19,7 +19,7 @@ from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE,
HVACMode,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
@ -36,6 +36,7 @@ async def test_hvac_node_auto(
assert state
assert state.state == HVACMode.OFF
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
# Test set hvac mode auto
await hass.services.async_call(
@ -150,6 +151,7 @@ async def test_hvac_node_heat(
assert state
assert state.state == HVACMode.OFF
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
# Test set hvac mode heat
await hass.services.async_call(
@ -259,6 +261,7 @@ async def test_hvac_node_cool(
assert state
assert state.state == HVACMode.OFF
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
# Test set hvac mode heat
await hass.services.async_call(

View File

@ -19,7 +19,7 @@ from homeassistant.components.cover import (
STATE_OPEN,
STATE_OPENING,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
@ -37,6 +37,7 @@ async def test_cover_node_percentage(
assert state
assert state.state == STATE_CLOSED
assert state.attributes[ATTR_CURRENT_POSITION] == 0
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
await hass.services.async_call(
COVER_DOMAIN,

View File

@ -6,7 +6,12 @@ from collections.abc import Callable
from mysensors.sensor import Sensor
from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SourceType
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_NOT_HOME
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_LATITUDE,
ATTR_LONGITUDE,
STATE_NOT_HOME,
)
from homeassistant.core import HomeAssistant
@ -32,6 +37,7 @@ async def test_gps_sensor(
assert state.attributes[ATTR_SOURCE_TYPE] == SourceType.GPS
assert state.attributes[ATTR_LATITUDE] == float(latitude)
assert state.attributes[ATTR_LONGITUDE] == float(longitude)
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
latitude = "40.782"
longitude = "-73.965"

View File

@ -12,6 +12,7 @@ from homeassistant.components.light import (
ATTR_RGBW_COLOR,
DOMAIN as LIGHT_DOMAIN,
)
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import HomeAssistant
@ -28,6 +29,7 @@ async def test_dimmer_node(
assert state
assert state.state == "off"
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
# Test turn on
await hass.services.async_call(
@ -108,6 +110,7 @@ async def test_rgb_node(
assert state
assert state.state == "off"
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
# Test turn on
await hass.services.async_call(
@ -218,6 +221,7 @@ async def test_rgbw_node(
assert state
assert state.state == "off"
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
# Test turn on
await hass.services.async_call(

View File

@ -14,7 +14,12 @@ from homeassistant.components.remote import (
SERVICE_LEARN_COMMAND,
SERVICE_SEND_COMMAND,
)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.core import HomeAssistant
@ -31,6 +36,7 @@ async def test_ir_transceiver(
assert state
assert state.state == "off"
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
# Test turn on
await hass.services.async_call(

View File

@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_DEVICE_CLASS,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
@ -41,6 +42,7 @@ async def test_gps_sensor(
assert state
assert state.state == "40.741894,-73.989311,12"
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
altitude = 0
new_coords = "40.782,-73.965"
@ -67,6 +69,7 @@ async def test_ir_transceiver(
assert state
assert state.state == "test_code"
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
receive_message("1;1;1;0;50;new_code\n")
await hass.async_block_till_done()
@ -87,6 +90,7 @@ async def test_battery_entity(
state = hass.states.get(battery_entity_id)
assert state
assert state.state == "42"
assert ATTR_BATTERY_LEVEL not in state.attributes
receive_message("1;255;3;0;0;84\n")
await hass.async_block_till_done()
@ -94,6 +98,7 @@ async def test_battery_entity(
state = hass.states.get(battery_entity_id)
assert state
assert state.state == "84"
assert ATTR_BATTERY_LEVEL not in state.attributes
async def test_power_sensor(
@ -111,6 +116,7 @@ async def test_power_sensor(
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT
assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
async def test_energy_sensor(
@ -128,6 +134,7 @@ async def test_energy_sensor(
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.KILO_WATT_HOUR
assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
async def test_sound_sensor(
@ -144,6 +151,7 @@ async def test_sound_sensor(
assert state.state == "10"
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.SOUND_PRESSURE
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "dB"
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
async def test_distance_sensor(
@ -161,6 +169,7 @@ async def test_distance_sensor(
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DISTANCE
assert ATTR_ICON not in state.attributes
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "cm"
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
@pytest.mark.parametrize(
@ -193,3 +202,4 @@ async def test_temperature_sensor(
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit
assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT
assert state.attributes[ATTR_BATTERY_LEVEL] == 0

View File

@ -7,6 +7,7 @@ from unittest.mock import MagicMock, call
from mysensors.sensor import Sensor
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import HomeAssistant
@ -23,6 +24,7 @@ async def test_relay_node(
assert state
assert state.state == "off"
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
await hass.services.async_call(
SWITCH_DOMAIN,

Some files were not shown because too many files have changed in this diff Show More