mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
2023.10.2 (#101871)
This commit is contained in:
commit
f5b5215247
@ -738,8 +738,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/matrix/ @PaarthShah
|
/tests/components/matrix/ @PaarthShah
|
||||||
/homeassistant/components/matter/ @home-assistant/matter
|
/homeassistant/components/matter/ @home-assistant/matter
|
||||||
/tests/components/matter/ @home-assistant/matter
|
/tests/components/matter/ @home-assistant/matter
|
||||||
/homeassistant/components/mazda/ @bdr99
|
|
||||||
/tests/components/mazda/ @bdr99
|
|
||||||
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
||||||
/tests/components/meater/ @Sotolotl @emontnemery
|
/tests/components/meater/ @Sotolotl @emontnemery
|
||||||
/homeassistant/components/medcom_ble/ @elafargue
|
/homeassistant/components/medcom_ble/ @elafargue
|
||||||
|
@ -217,8 +217,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
|||||||
if ATTR_TEMPERATURE in kwargs:
|
if ATTR_TEMPERATURE in kwargs:
|
||||||
params[API_SET_POINT] = kwargs[ATTR_TEMPERATURE]
|
params[API_SET_POINT] = kwargs[ATTR_TEMPERATURE]
|
||||||
if ATTR_TARGET_TEMP_LOW in kwargs and ATTR_TARGET_TEMP_HIGH in kwargs:
|
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_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
|
||||||
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
|
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
|
||||||
await self._async_update_hvac_params(params)
|
await self._async_update_hvac_params(params)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -248,8 +248,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
|||||||
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
|
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
|
||||||
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||||
self._attr_target_temperature_high = self.get_airzone_value(
|
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
|
AZD_COOL_TEMP_SET
|
||||||
)
|
)
|
||||||
|
self._attr_target_temperature_low = self.get_airzone_value(
|
||||||
|
AZD_HEAT_TEMP_SET
|
||||||
|
)
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["bimmer_connected"],
|
"loggers": ["bimmer_connected"],
|
||||||
"requirements": ["bimmer-connected==0.14.0"]
|
"requirements": ["bimmer-connected==0.14.1"]
|
||||||
}
|
}
|
||||||
|
@ -528,7 +528,9 @@ class CalendarEntity(Entity):
|
|||||||
the current or upcoming event.
|
the current or upcoming event.
|
||||||
"""
|
"""
|
||||||
super().async_write_ha_state()
|
super().async_write_ha_state()
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Clearing %s alarms (%s)", self.entity_id, len(self._alarm_unsubs)
|
||||||
|
)
|
||||||
for unsub in self._alarm_unsubs:
|
for unsub in self._alarm_unsubs:
|
||||||
unsub()
|
unsub()
|
||||||
self._alarm_unsubs.clear()
|
self._alarm_unsubs.clear()
|
||||||
@ -536,6 +538,7 @@ class CalendarEntity(Entity):
|
|||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
event = self.event
|
event = self.event
|
||||||
if event is None or now >= event.end_datetime_local:
|
if event is None or now >= event.end_datetime_local:
|
||||||
|
_LOGGER.debug("No alarms needed for %s (event=%s)", self.entity_id, event)
|
||||||
return
|
return
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/duotecno",
|
"documentation": "https://www.home-assistant.io/integrations/duotecno",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["pyDuotecno==2023.9.0"]
|
"requirements": ["pyDuotecno==2023.10.0"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["env_canada"],
|
"loggers": ["env_canada"],
|
||||||
"requirements": ["env-canada==0.5.37"]
|
"requirements": ["env-canada==0.6.0"]
|
||||||
}
|
}
|
||||||
|
@ -327,7 +327,10 @@ class ESPHomeManager:
|
|||||||
) -> int | None:
|
) -> int | None:
|
||||||
"""Start a voice assistant pipeline."""
|
"""Start a voice assistant pipeline."""
|
||||||
if self.voice_assistant_udp_server is not None:
|
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
|
hass = self.hass
|
||||||
self.voice_assistant_udp_server = VoiceAssistantUDPServer(
|
self.voice_assistant_udp_server = VoiceAssistantUDPServer(
|
||||||
|
@ -24,5 +24,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/eufylife_ble",
|
"documentation": "https://www.home-assistant.io/integrations/eufylife_ble",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["eufylife-ble-client==0.1.7"]
|
"requirements": ["eufylife-ble-client==0.1.8"]
|
||||||
}
|
}
|
||||||
|
@ -12,5 +12,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyfronius"],
|
"loggers": ["pyfronius"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["PyFronius==0.7.1"]
|
"requirements": ["PyFronius==0.7.2"]
|
||||||
}
|
}
|
||||||
|
@ -240,6 +240,7 @@ async def async_setup_entry(
|
|||||||
SERVICE_CREATE_EVENT,
|
SERVICE_CREATE_EVENT,
|
||||||
CREATE_EVENT_SCHEMA,
|
CREATE_EVENT_SCHEMA,
|
||||||
async_create_event,
|
async_create_event,
|
||||||
|
required_features=CalendarEntityFeature.CREATE_EVENT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from bleak import BleakError
|
from bleak.exc import BleakError
|
||||||
from bluetooth_data_tools import human_readable_name
|
from bluetooth_data_tools import human_readable_name
|
||||||
from idasen_ha import Desk
|
from idasen_ha import AuthFailedError, Desk
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
@ -64,6 +64,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
desk = Desk(None)
|
desk = Desk(None)
|
||||||
try:
|
try:
|
||||||
await desk.connect(discovery_info.device, monitor_height=False)
|
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:
|
except TimeoutError as err:
|
||||||
_LOGGER.exception("TimeoutError", exc_info=err)
|
_LOGGER.exception("TimeoutError", exc_info=err)
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
|
@ -11,5 +11,5 @@
|
|||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
|
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["idasen-ha==1.4"]
|
"requirements": ["idasen-ha==1.4.1"]
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"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%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pylutron_caseta"],
|
"loggers": ["pylutron_caseta"],
|
||||||
"requirements": ["pylutron-caseta==0.18.2"],
|
"requirements": ["pylutron-caseta==0.18.3"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_lutron._tcp.local.",
|
"type": "_lutron._tcp.local.",
|
||||||
|
@ -1,213 +1,26 @@
|
|||||||
"""The Mazda Connected Services integration."""
|
"""The Mazda Connected Services integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from datetime import timedelta
|
from homeassistant.core import HomeAssistant
|
||||||
import logging
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from pymazda import (
|
DOMAIN = "mazda"
|
||||||
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,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def with_timeout(task, timeout_seconds=30):
|
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||||
"""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:
|
|
||||||
"""Set up Mazda Connected Services from a config entry."""
|
"""Set up Mazda Connected Services from a config entry."""
|
||||||
email = entry.data[CONF_EMAIL]
|
ir.async_create_issue(
|
||||||
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(
|
|
||||||
hass,
|
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,
|
DOMAIN,
|
||||||
"send_poi",
|
DOMAIN,
|
||||||
async_handle_service_call,
|
is_fixable=False,
|
||||||
schema=service_schema_send_poi,
|
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
|
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:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""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
|
return True
|
||||||
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']}"
|
|
||||||
|
@ -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)
|
|
@ -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
|
|
||||||
)
|
|
@ -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()
|
|
@ -1,110 +1,11 @@
|
|||||||
"""Config flow for Mazda Connected Services integration."""
|
"""The Mazda Connected Services integration."""
|
||||||
from collections.abc import Mapping
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import aiohttp
|
from homeassistant.config_entries import ConfigFlow
|
||||||
from pymazda import (
|
|
||||||
Client as MazdaAPI,
|
|
||||||
MazdaAccountLockedException,
|
|
||||||
MazdaAuthenticationException,
|
|
||||||
)
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from . import DOMAIN
|
||||||
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),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class MazdaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Mazda Connected Services."""
|
"""Handle a config flow for Mazda Connected Services."""
|
||||||
|
|
||||||
VERSION = 1
|
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()
|
|
||||||
|
@ -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"}
|
|
@ -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"]
|
|
@ -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
|
|
@ -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()
|
|
@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"domain": "mazda",
|
"domain": "mazda",
|
||||||
"name": "Mazda Connected Services",
|
"name": "Mazda Connected Services",
|
||||||
"codeowners": ["@bdr99"],
|
"codeowners": [],
|
||||||
"config_flow": true,
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||||
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pymazda"],
|
"requirements": []
|
||||||
"quality_scale": "platinum",
|
|
||||||
"requirements": ["pymazda==0.3.11"]
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
@ -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:
|
|
@ -1,139 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"issues": {
|
||||||
"abort": {
|
"integration_removed": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
"title": "The Mazda integration has been removed",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
"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})."
|
||||||
},
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
@ -180,7 +180,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@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:
|
def state_message_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle a new received MQTT state message."""
|
"""Handle a new received MQTT state message."""
|
||||||
# auto-expire enabled?
|
# auto-expire enabled?
|
||||||
|
@ -277,7 +277,9 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@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)
|
@log_messages(self.hass, self.entity_id)
|
||||||
def message_received(msg: ReceiveMessage) -> None:
|
def message_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle new MQTT messages."""
|
"""Handle new MQTT messages."""
|
||||||
|
@ -14,5 +14,5 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pkce", "pymyq"],
|
"loggers": ["pkce", "pymyq"],
|
||||||
"requirements": ["python-myq==3.1.11"]
|
"requirements": ["python-myq==3.1.13"]
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ from typing import Any
|
|||||||
from mysensors import BaseAsyncGateway, Sensor
|
from mysensors import BaseAsyncGateway, Sensor
|
||||||
from mysensors.sensor import ChildSensor
|
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.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
@ -212,6 +212,8 @@ class MySensorsChildEntity(MySensorNodeEntity):
|
|||||||
|
|
||||||
attr[ATTR_CHILD_ID] = self.child_id
|
attr[ATTR_CHILD_ID] = self.child_id
|
||||||
attr[ATTR_DESCRIPTION] = self._child.description
|
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
|
set_req = self.gateway.const.SetReq
|
||||||
for value_type, value in self._values.items():
|
for value_type, value in self._values.items():
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"submit_movie_request": {
|
"submit_movie_request": {
|
||||||
"name": "Sumbit movie request",
|
"name": "Submit movie request",
|
||||||
"description": "Searches for a movie and requests the first result.",
|
"description": "Searches for a movie and requests the first result.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"name": {
|
"name": {
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["haphilipsjs"],
|
"loggers": ["haphilipsjs"],
|
||||||
"requirements": ["ha-philipsjs==3.1.0"]
|
"requirements": ["ha-philipsjs==3.1.1"]
|
||||||
}
|
}
|
||||||
|
@ -259,7 +259,7 @@
|
|||||||
"name": "DHW comfort mode"
|
"name": "DHW comfort mode"
|
||||||
},
|
},
|
||||||
"lock": {
|
"lock": {
|
||||||
"name": "[%key:component::lock::entity_component::_::name%]"
|
"name": "[%key:component::lock::title%]"
|
||||||
},
|
},
|
||||||
"relay": {
|
"relay": {
|
||||||
"name": "Relay"
|
"name": "Relay"
|
||||||
|
@ -11,5 +11,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
|
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioqsw"],
|
"loggers": ["aioqsw"],
|
||||||
"requirements": ["aioqsw==0.3.4"]
|
"requirements": ["aioqsw==0.3.5"]
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], BinarySensorE
|
|||||||
"""Initialize the Rain Bird sensor."""
|
"""Initialize the Rain Bird sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = description
|
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_unique_id = f"{coordinator.unique_id}-{description.key}"
|
||||||
self._attr_device_info = coordinator.device_info
|
self._attr_device_info = coordinator.device_info
|
||||||
else:
|
else:
|
||||||
|
@ -84,7 +84,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
|
|||||||
@property
|
@property
|
||||||
def device_info(self) -> DeviceInfo | None:
|
def device_info(self) -> DeviceInfo | None:
|
||||||
"""Return information about the device."""
|
"""Return information about the device."""
|
||||||
if not self._unique_id:
|
if self._unique_id is None:
|
||||||
return None
|
return None
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
name=self.device_name,
|
name=self.device_name,
|
||||||
|
@ -526,7 +526,7 @@ def _compile_statistics(
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
compiled: PlatformCompiledStatistics = platform_compile_statistics(
|
compiled: PlatformCompiledStatistics = platform_compile_statistics(
|
||||||
instance.hass, start, end
|
instance.hass, session, start, end
|
||||||
)
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Statistics for %s during %s-%s: %s",
|
"Statistics for %s during %s-%s: %s",
|
||||||
@ -1871,7 +1871,7 @@ def get_latest_short_term_statistics_by_ids(
|
|||||||
return list(
|
return list(
|
||||||
cast(
|
cast(
|
||||||
Sequence[Row],
|
Sequence[Row],
|
||||||
execute_stmt_lambda_element(session, stmt, orm_rows=False),
|
execute_stmt_lambda_element(session, stmt),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1887,75 +1887,69 @@ def _latest_short_term_statistics_by_ids_stmt(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_latest_short_term_statistics(
|
def get_latest_short_term_statistics_with_session(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
session: Session,
|
||||||
statistic_ids: set[str],
|
statistic_ids: set[str],
|
||||||
types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]],
|
types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]],
|
||||||
metadata: dict[str, tuple[int, StatisticMetaData]] | None = None,
|
metadata: dict[str, tuple[int, StatisticMetaData]] | None = None,
|
||||||
) -> dict[str, list[StatisticsRow]]:
|
) -> dict[str, list[StatisticsRow]]:
|
||||||
"""Return the latest short term statistics for a list of statistic_ids."""
|
"""Return the latest short term statistics for a list of statistic_ids with a session."""
|
||||||
with session_scope(hass=hass, read_only=True) as session:
|
# Fetch metadata for the given statistic_ids
|
||||||
# Fetch metadata for the given statistic_ids
|
if not metadata:
|
||||||
if not metadata:
|
metadata = get_instance(hass).statistics_meta_manager.get_many(
|
||||||
metadata = get_instance(hass).statistics_meta_manager.get_many(
|
session, statistic_ids=statistic_ids
|
||||||
session, statistic_ids=statistic_ids
|
|
||||||
)
|
|
||||||
if not metadata:
|
|
||||||
return {}
|
|
||||||
metadata_ids = set(
|
|
||||||
_extract_metadata_and_discard_impossible_columns(metadata, types)
|
|
||||||
)
|
)
|
||||||
run_cache = get_short_term_statistics_run_cache(hass)
|
if not metadata:
|
||||||
# Try to find the latest short term statistics ids for the metadata_ids
|
return {}
|
||||||
# from the run cache first if we have it. If the run cache references
|
metadata_ids = set(
|
||||||
# a non-existent id because of a purge, we will detect it missing in the
|
_extract_metadata_and_discard_impossible_columns(metadata, types)
|
||||||
# next step and run a query to re-populate the cache.
|
)
|
||||||
stats: list[Row] = []
|
run_cache = get_short_term_statistics_run_cache(hass)
|
||||||
if metadata_id_to_id := run_cache.get_latest_ids(metadata_ids):
|
# Try to find the latest short term statistics ids for the metadata_ids
|
||||||
stats = get_latest_short_term_statistics_by_ids(
|
# from the run cache first if we have it. If the run cache references
|
||||||
session, metadata_id_to_id.values()
|
# a non-existent id because of a purge, we will detect it missing in the
|
||||||
)
|
# next step and run a query to re-populate the cache.
|
||||||
# If we are missing some metadata_ids in the run cache, we need run a query
|
stats: list[Row] = []
|
||||||
# to populate the cache for each metadata_id, and then run another query
|
if metadata_id_to_id := run_cache.get_latest_ids(metadata_ids):
|
||||||
# to get the latest short term statistics for the missing metadata_ids.
|
stats = get_latest_short_term_statistics_by_ids(
|
||||||
if (missing_metadata_ids := metadata_ids - set(metadata_id_to_id)) and (
|
session, metadata_id_to_id.values()
|
||||||
found_latest_ids := {
|
)
|
||||||
latest_id
|
# If we are missing some metadata_ids in the run cache, we need run a query
|
||||||
for metadata_id in missing_metadata_ids
|
# to populate the cache for each metadata_id, and then run another query
|
||||||
if (
|
# to get the latest short term statistics for the missing metadata_ids.
|
||||||
latest_id := cache_latest_short_term_statistic_id_for_metadata_id(
|
if (missing_metadata_ids := metadata_ids - set(metadata_id_to_id)) and (
|
||||||
# orm_rows=False is used here because we are in
|
found_latest_ids := {
|
||||||
# a read-only session, and there will never be
|
latest_id
|
||||||
# any pending inserts in the session.
|
for metadata_id in missing_metadata_ids
|
||||||
run_cache,
|
if (
|
||||||
session,
|
latest_id := cache_latest_short_term_statistic_id_for_metadata_id(
|
||||||
metadata_id,
|
run_cache,
|
||||||
orm_rows=False,
|
session,
|
||||||
)
|
metadata_id,
|
||||||
)
|
)
|
||||||
is not None
|
|
||||||
}
|
|
||||||
):
|
|
||||||
stats.extend(
|
|
||||||
get_latest_short_term_statistics_by_ids(session, found_latest_ids)
|
|
||||||
)
|
)
|
||||||
|
is not None
|
||||||
|
}
|
||||||
|
):
|
||||||
|
stats.extend(get_latest_short_term_statistics_by_ids(session, found_latest_ids))
|
||||||
|
|
||||||
if not stats:
|
if not stats:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Return statistics combined with metadata
|
# Return statistics combined with metadata
|
||||||
return _sorted_statistics_to_dict(
|
return _sorted_statistics_to_dict(
|
||||||
hass,
|
hass,
|
||||||
session,
|
session,
|
||||||
stats,
|
stats,
|
||||||
statistic_ids,
|
statistic_ids,
|
||||||
metadata,
|
metadata,
|
||||||
False,
|
False,
|
||||||
StatisticsShortTerm,
|
StatisticsShortTerm,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
types,
|
types,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _generate_statistics_at_time_stmt(
|
def _generate_statistics_at_time_stmt(
|
||||||
@ -2316,14 +2310,8 @@ def _import_statistics_with_session(
|
|||||||
# We just inserted new short term statistics, so we need to update the
|
# We just inserted new short term statistics, so we need to update the
|
||||||
# ShortTermStatisticsRunCache with the latest id for the metadata_id
|
# ShortTermStatisticsRunCache with the latest id for the metadata_id
|
||||||
run_cache = get_short_term_statistics_run_cache(instance.hass)
|
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(
|
cache_latest_short_term_statistic_id_for_metadata_id(
|
||||||
run_cache, session, metadata_id, orm_rows=True
|
run_cache, session, metadata_id
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -2341,7 +2329,6 @@ def cache_latest_short_term_statistic_id_for_metadata_id(
|
|||||||
run_cache: ShortTermStatisticsRunCache,
|
run_cache: ShortTermStatisticsRunCache,
|
||||||
session: Session,
|
session: Session,
|
||||||
metadata_id: int,
|
metadata_id: int,
|
||||||
orm_rows: bool,
|
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
"""Cache the latest short term statistic for a given metadata_id.
|
"""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(
|
if latest := cast(
|
||||||
Sequence[Row],
|
Sequence[Row],
|
||||||
execute_stmt_lambda_element(
|
execute_stmt_lambda_element(
|
||||||
session,
|
session, _find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id)
|
||||||
_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.
|
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
id_: int = latest[0].id
|
id_: int = latest[0].id
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"complete_task": {
|
"complete_task": {
|
||||||
"name": "Complete task",
|
"name": "Complete task",
|
||||||
"description": "Completes a tasks that was privously created.",
|
"description": "Completes a task that was previously created.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
|
@ -92,7 +92,6 @@ class RoonDevice(MediaPlayerEntity):
|
|||||||
MediaPlayerEntityFeature.BROWSE_MEDIA
|
MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||||
| MediaPlayerEntityFeature.GROUPING
|
| MediaPlayerEntityFeature.GROUPING
|
||||||
| MediaPlayerEntityFeature.PAUSE
|
| MediaPlayerEntityFeature.PAUSE
|
||||||
| MediaPlayerEntityFeature.VOLUME_SET
|
|
||||||
| MediaPlayerEntityFeature.STOP
|
| MediaPlayerEntityFeature.STOP
|
||||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||||
@ -104,7 +103,6 @@ class RoonDevice(MediaPlayerEntity):
|
|||||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||||
| MediaPlayerEntityFeature.PLAY
|
| MediaPlayerEntityFeature.PLAY
|
||||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, server, player_data):
|
def __init__(self, server, player_data):
|
||||||
@ -124,6 +122,8 @@ class RoonDevice(MediaPlayerEntity):
|
|||||||
self._attr_shuffle = False
|
self._attr_shuffle = False
|
||||||
self._attr_media_image_url = None
|
self._attr_media_image_url = None
|
||||||
self._attr_volume_level = 0
|
self._attr_volume_level = 0
|
||||||
|
self._volume_fixed = True
|
||||||
|
self._volume_incremental = False
|
||||||
self.update_data(player_data)
|
self.update_data(player_data)
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
@ -190,12 +190,21 @@ class RoonDevice(MediaPlayerEntity):
|
|||||||
"level": 0,
|
"level": 0,
|
||||||
"step": 0,
|
"step": 0,
|
||||||
"muted": False,
|
"muted": False,
|
||||||
|
"fixed": True,
|
||||||
|
"incremental": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
volume_data = player_data["volume"]
|
volume_data = player_data["volume"]
|
||||||
volume_muted = volume_data["is_muted"]
|
except KeyError:
|
||||||
volume_step = convert(volume_data["step"], int, 0)
|
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_max = volume_data["max"]
|
||||||
volume_min = volume_data["min"]
|
volume_min = volume_data["min"]
|
||||||
raw_level = convert(volume_data["value"], float, 0)
|
raw_level = convert(volume_data["value"], float, 0)
|
||||||
@ -204,15 +213,9 @@ class RoonDevice(MediaPlayerEntity):
|
|||||||
volume_percentage_factor = volume_range / 100
|
volume_percentage_factor = volume_range / 100
|
||||||
|
|
||||||
level = (raw_level - volume_min) / volume_percentage_factor
|
level = (raw_level - volume_min) / volume_percentage_factor
|
||||||
volume_level = convert(level, int, 0) / 100
|
volume["level"] = convert(level, int, 0) / 100
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# catch KeyError
|
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
volume["muted"] = volume_muted
|
|
||||||
volume["step"] = volume_step
|
|
||||||
volume["level"] = volume_level
|
|
||||||
|
|
||||||
return volume
|
return volume
|
||||||
|
|
||||||
@ -288,6 +291,16 @@ class RoonDevice(MediaPlayerEntity):
|
|||||||
self._attr_is_volume_muted = volume["muted"]
|
self._attr_is_volume_muted = volume["muted"]
|
||||||
self._attr_volume_step = volume["step"]
|
self._attr_volume_step = volume["step"]
|
||||||
self._attr_volume_level = volume["level"]
|
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)
|
now_playing = self._parse_now_playing(self.player_data)
|
||||||
self._attr_media_title = now_playing["title"]
|
self._attr_media_title = now_playing["title"]
|
||||||
@ -359,11 +372,17 @@ class RoonDevice(MediaPlayerEntity):
|
|||||||
|
|
||||||
def volume_up(self) -> None:
|
def volume_up(self) -> None:
|
||||||
"""Send new volume_level to device."""
|
"""Send new volume_level to device."""
|
||||||
self._server.roonapi.change_volume_percent(self.output_id, 3)
|
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:
|
def volume_down(self) -> None:
|
||||||
"""Send new volume_level to device."""
|
"""Send new volume_level to device."""
|
||||||
self._server.roonapi.change_volume_percent(self.output_id, -3)
|
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:
|
def turn_on(self) -> None:
|
||||||
"""Turn on device (if supported)."""
|
"""Turn on device (if supported)."""
|
||||||
|
@ -15,5 +15,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/screenlogic",
|
"documentation": "https://www.home-assistant.io/integrations/screenlogic",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["screenlogicpy"],
|
"loggers": ["screenlogicpy"],
|
||||||
"requirements": ["screenlogicpy==0.9.1"]
|
"requirements": ["screenlogicpy==0.9.2"]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Support for a ScreenLogic number entity."""
|
"""Support for a ScreenLogic number entity."""
|
||||||
from collections.abc import Callable
|
import asyncio
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -105,13 +106,13 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a ScreenLogic number entity."""
|
"""Initialize a ScreenLogic number entity."""
|
||||||
super().__init__(coordinator, entity_description)
|
super().__init__(coordinator, entity_description)
|
||||||
if not callable(
|
if not asyncio.iscoroutinefunction(
|
||||||
func := getattr(self.gateway, entity_description.set_value_name)
|
func := getattr(self.gateway, entity_description.set_value_name)
|
||||||
):
|
):
|
||||||
raise TypeError(
|
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._set_value_args = entity_description.set_value_args
|
||||||
self._attr_native_unit_of_measurement = get_ha_unit(
|
self._attr_native_unit_of_measurement = get_ha_unit(
|
||||||
self.entity_data.get(ATTR.UNIT)
|
self.entity_data.get(ATTR.UNIT)
|
||||||
@ -145,9 +146,12 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity):
|
|||||||
data_key = data_path[-1]
|
data_key = data_path[-1]
|
||||||
args[data_key] = self.coordinator.gateway.get_value(*data_path, strict=True)
|
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
|
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)
|
_LOGGER.debug("Set '%s' to %s", self._data_key, value)
|
||||||
await self._async_refresh()
|
await self._async_refresh()
|
||||||
else:
|
else:
|
||||||
|
@ -16,7 +16,6 @@ from homeassistant.components.recorder import (
|
|||||||
get_instance,
|
get_instance,
|
||||||
history,
|
history,
|
||||||
statistics,
|
statistics,
|
||||||
util as recorder_util,
|
|
||||||
)
|
)
|
||||||
from homeassistant.components.recorder.models import (
|
from homeassistant.components.recorder.models import (
|
||||||
StatisticData,
|
StatisticData,
|
||||||
@ -383,27 +382,7 @@ def _timestamp_to_isoformat_or_none(timestamp: float | None) -> str | None:
|
|||||||
return dt_util.utc_from_timestamp(timestamp).isoformat()
|
return dt_util.utc_from_timestamp(timestamp).isoformat()
|
||||||
|
|
||||||
|
|
||||||
def compile_statistics(
|
def compile_statistics( # noqa: C901
|
||||||
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
|
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
session: Session,
|
session: Session,
|
||||||
start: datetime.datetime,
|
start: datetime.datetime,
|
||||||
@ -480,8 +459,8 @@ def _compile_statistics( # noqa: C901
|
|||||||
if "sum" in wanted_statistics[entity_id]:
|
if "sum" in wanted_statistics[entity_id]:
|
||||||
to_query.add(entity_id)
|
to_query.add(entity_id)
|
||||||
|
|
||||||
last_stats = statistics.get_latest_short_term_statistics(
|
last_stats = statistics.get_latest_short_term_statistics_with_session(
|
||||||
hass, to_query, {"last_reset", "state", "sum"}, metadata=old_metadatas
|
hass, session, to_query, {"last_reset", "state", "sum"}, metadata=old_metadatas
|
||||||
)
|
)
|
||||||
for ( # pylint: disable=too-many-nested-blocks
|
for ( # pylint: disable=too-many-nested-blocks
|
||||||
entity_id,
|
entity_id,
|
||||||
|
@ -166,7 +166,7 @@ class SlackNotificationService(BaseNotificationService):
|
|||||||
filename=filename,
|
filename=filename,
|
||||||
initial_comment=message,
|
initial_comment=message,
|
||||||
title=title or filename,
|
title=title or filename,
|
||||||
thread_ts=thread_ts,
|
thread_ts=thread_ts or "",
|
||||||
)
|
)
|
||||||
except (SlackApiError, ClientError) as err:
|
except (SlackApiError, ClientError) as err:
|
||||||
_LOGGER.error("Error while uploading file-based message: %r", err)
|
_LOGGER.error("Error while uploading file-based message: %r", err)
|
||||||
|
@ -274,8 +274,6 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = {
|
|||||||
"grid_power_factor_excitation": SensorEntityDescription(
|
"grid_power_factor_excitation": SensorEntityDescription(
|
||||||
key="grid_power_factor_excitation",
|
key="grid_power_factor_excitation",
|
||||||
name="Grid Power Factor Excitation",
|
name="Grid Power Factor Excitation",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.POWER_FACTOR,
|
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
|
@ -10,6 +10,6 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["systembridgeconnector"],
|
"loggers": ["systembridgeconnector"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["systembridgeconnector==3.8.2"],
|
"requirements": ["systembridgeconnector==3.8.4"],
|
||||||
"zeroconf": ["_system-bridge._tcp.local."]
|
"zeroconf": ["_system-bridge._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
import asyncio
|
||||||
from collections.abc import AsyncIterable
|
from collections.abc import AsyncIterable
|
||||||
import logging
|
import logging
|
||||||
from typing import final
|
from typing import final
|
||||||
@ -34,6 +35,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
TIMEOUT_FETCH_WAKE_WORDS = 10
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_default_entity(hass: HomeAssistant) -> str | None:
|
def async_default_entity(hass: HomeAssistant) -> str | None:
|
||||||
@ -86,9 +89,8 @@ class WakeWordDetectionEntity(RestoreEntity):
|
|||||||
"""Return the state of the entity."""
|
"""Return the state of the entity."""
|
||||||
return self.__last_detected
|
return self.__last_detected
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def supported_wake_words(self) -> list[WakeWord]:
|
async def get_supported_wake_words(self) -> list[WakeWord]:
|
||||||
"""Return a list of supported wake words."""
|
"""Return a list of supported wake words."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -133,8 +135,9 @@ class WakeWordDetectionEntity(RestoreEntity):
|
|||||||
vol.Required("entity_id"): cv.entity_domain(DOMAIN),
|
vol.Required("entity_id"): cv.entity_domain(DOMAIN),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
@callback
|
@callback
|
||||||
def websocket_entity_info(
|
async def websocket_entity_info(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Get info about wake word entity."""
|
"""Get info about wake word entity."""
|
||||||
@ -147,7 +150,16 @@ def websocket_entity_info(
|
|||||||
)
|
)
|
||||||
return
|
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(
|
connection.send_result(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
{"wake_words": entity.supported_wake_words},
|
{"wake_words": wake_words},
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,7 @@ from enum import StrEnum
|
|||||||
|
|
||||||
DOMAIN = "wallbox"
|
DOMAIN = "wallbox"
|
||||||
|
|
||||||
BIDIRECTIONAL_MODEL_PREFIXES = ["QSX"]
|
BIDIRECTIONAL_MODEL_PREFIXES = ["QS"]
|
||||||
|
|
||||||
CODE_KEY = "code"
|
CODE_KEY = "code"
|
||||||
CONF_STATION = "station"
|
CONF_STATION = "station"
|
||||||
|
@ -79,7 +79,7 @@ class WallboxNumber(WallboxEntity, NumberEntity):
|
|||||||
self._coordinator = coordinator
|
self._coordinator = coordinator
|
||||||
self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}"
|
self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}"
|
||||||
self._is_bidirectional = (
|
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
|
in BIDIRECTIONAL_MODEL_PREFIXES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyweatherflowudp"],
|
"loggers": ["pyweatherflowudp"],
|
||||||
"requirements": ["pyweatherflowudp==1.4.3"]
|
"requirements": ["pyweatherflowudp==1.4.5"]
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ class WeatherKitFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
|
user_input[CONF_KEY_PEM] = self._fix_key_input(user_input[CONF_KEY_PEM])
|
||||||
await self._test_config(user_input)
|
await self._test_config(user_input)
|
||||||
except WeatherKitUnsupportedLocationError as exception:
|
except WeatherKitUnsupportedLocationError as exception:
|
||||||
LOGGER.error(exception)
|
LOGGER.error(exception)
|
||||||
@ -104,6 +105,25 @@ class WeatherKitFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
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:
|
async def _test_config(self, user_input: dict[str, Any]) -> None:
|
||||||
"""Validate credentials."""
|
"""Validate credentials."""
|
||||||
client = WeatherKitApiClient(
|
client = WeatherKitApiClient(
|
||||||
|
@ -4,8 +4,10 @@ For more details about this platform, please refer to the documentation at
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp.hdrs import METH_HEAD, METH_POST
|
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 homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .api import ConfigEntryWithingsApi
|
from .api import ConfigEntryWithingsApi
|
||||||
from .const import (
|
from .const import CONF_PROFILES, CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN, LOGGER
|
||||||
CONF_CLOUDHOOK_URL,
|
|
||||||
CONF_PROFILES,
|
|
||||||
CONF_USE_WEBHOOK,
|
|
||||||
DEFAULT_TITLE,
|
|
||||||
DOMAIN,
|
|
||||||
LOGGER,
|
|
||||||
)
|
|
||||||
from .coordinator import WithingsDataUpdateCoordinator
|
from .coordinator import WithingsDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||||
@ -78,6 +73,9 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
},
|
},
|
||||||
extra=vol.ALLOW_EXTRA,
|
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:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
@ -141,13 +139,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
) -> None:
|
) -> None:
|
||||||
LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID])
|
LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID])
|
||||||
webhook_unregister(hass, 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(
|
async def register_webhook(
|
||||||
_: Any,
|
_: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
if cloud.async_active_subscription(hass):
|
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:
|
else:
|
||||||
webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID])
|
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),
|
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)
|
LOGGER.debug("Register Withings webhook: %s", webhook_url)
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
|
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))
|
entry.async_on_unload(async_call_later(hass, 1, register_webhook))
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -208,12 +207,54 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def async_subscribe_webhooks(
|
||||||
"""Handle options update."""
|
client: ConfigEntryWithingsApi, webhook_url: str
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
) -> 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."""
|
"""Generate the full URL for a webhook_id."""
|
||||||
if CONF_CLOUDHOOK_URL not in entry.data:
|
if CONF_CLOUDHOOK_URL not in entry.data:
|
||||||
webhook_id = entry.data[CONF_WEBHOOK_ID]
|
webhook_id = entry.data[CONF_WEBHOOK_ID]
|
||||||
|
@ -76,6 +76,7 @@ class WithingsFlowHandler(
|
|||||||
self.hass.config_entries.async_update_entry(
|
self.hass.config_entries.async_update_entry(
|
||||||
self.reauth_entry, data={**self.reauth_entry.data, **data}
|
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="reauth_successful")
|
||||||
|
|
||||||
return self.async_abort(reason="wrong_account")
|
return self.async_abort(reason="wrong_account")
|
||||||
|
@ -5,7 +5,6 @@ import logging
|
|||||||
DEFAULT_TITLE = "Withings"
|
DEFAULT_TITLE = "Withings"
|
||||||
CONF_PROFILES = "profiles"
|
CONF_PROFILES = "profiles"
|
||||||
CONF_USE_WEBHOOK = "use_webhook"
|
CONF_USE_WEBHOOK = "use_webhook"
|
||||||
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
|
||||||
|
|
||||||
DATA_MANAGER = "data_manager"
|
DATA_MANAGER = "data_manager"
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""Withings coordinator."""
|
"""Withings coordinator."""
|
||||||
import asyncio
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -24,9 +23,6 @@ from homeassistant.util import dt as dt_util
|
|||||||
from .api import ConfigEntryWithingsApi
|
from .api import ConfigEntryWithingsApi
|
||||||
from .const import LOGGER, Measurement
|
from .const import LOGGER, Measurement
|
||||||
|
|
||||||
SUBSCRIBE_DELAY = timedelta(seconds=5)
|
|
||||||
UNSUBSCRIBE_DELAY = timedelta(seconds=1)
|
|
||||||
|
|
||||||
WITHINGS_MEASURE_TYPE_MAP: dict[
|
WITHINGS_MEASURE_TYPE_MAP: dict[
|
||||||
NotifyAppli | GetSleepSummaryField | MeasureType, Measurement
|
NotifyAppli | GetSleepSummaryField | MeasureType, Measurement
|
||||||
] = {
|
] = {
|
||||||
@ -84,55 +80,12 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
|
|||||||
super().__init__(hass, LOGGER, name="Withings", update_interval=UPDATE_INTERVAL)
|
super().__init__(hass, LOGGER, name="Withings", update_interval=UPDATE_INTERVAL)
|
||||||
self._client = client
|
self._client = client
|
||||||
|
|
||||||
async def async_subscribe_webhooks(self, webhook_url: str) -> None:
|
def webhook_subscription_listener(self, connected: bool) -> None:
|
||||||
"""Subscribe to webhooks."""
|
"""Call when webhook status changed."""
|
||||||
await self.async_unsubscribe_webhooks()
|
if connected:
|
||||||
|
self.update_interval = None
|
||||||
current_webhooks = await self._client.async_notify_list()
|
else:
|
||||||
|
self.update_interval = UPDATE_INTERVAL
|
||||||
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)
|
|
||||||
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
|
|
||||||
)
|
|
||||||
self.update_interval = UPDATE_INTERVAL
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[Measurement, Any]:
|
async def _async_update_data(self) -> dict[Measurement, Any]:
|
||||||
try:
|
try:
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||||
"already_configured": "Configuration updated for profile.",
|
"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": {
|
"create_entry": {
|
||||||
"default": "Successfully authenticated with Withings."
|
"default": "Successfully authenticated with Withings."
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .data import WyomingService
|
from .data import WyomingService, load_wyoming_info
|
||||||
from .error import WyomingError
|
from .error import WyomingError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -28,7 +28,7 @@ async def async_setup_entry(
|
|||||||
service: WyomingService = hass.data[DOMAIN][config_entry.entry_id]
|
service: WyomingService = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
WyomingWakeWordProvider(config_entry, service),
|
WyomingWakeWordProvider(hass, config_entry, service),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,10 +38,12 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
service: WyomingService,
|
service: WyomingService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up provider."""
|
"""Set up provider."""
|
||||||
|
self.hass = hass
|
||||||
self.service = service
|
self.service = service
|
||||||
wake_service = service.info.wake[0]
|
wake_service = service.info.wake[0]
|
||||||
|
|
||||||
@ -52,9 +54,19 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity):
|
|||||||
self._attr_name = wake_service.name
|
self._attr_name = wake_service.name
|
||||||
self._attr_unique_id = f"{config_entry.entry_id}-wake_word"
|
self._attr_unique_id = f"{config_entry.entry_id}-wake_word"
|
||||||
|
|
||||||
@property
|
async def get_supported_wake_words(self) -> list[wake_word.WakeWord]:
|
||||||
def supported_wake_words(self) -> list[wake_word.WakeWord]:
|
|
||||||
"""Return a list of supported wake words."""
|
"""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
|
return self._supported_wake_words
|
||||||
|
|
||||||
async def _async_process_audio_stream(
|
async def _async_process_audio_stream(
|
||||||
|
@ -7,7 +7,7 @@ from typing import Final
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2023
|
MAJOR_VERSION: Final = 2023
|
||||||
MINOR_VERSION: Final = 10
|
MINOR_VERSION: Final = 10
|
||||||
PATCH_VERSION: Final = "1"
|
PATCH_VERSION: Final = "2"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||||
|
@ -270,7 +270,6 @@ FLOWS = {
|
|||||||
"lyric",
|
"lyric",
|
||||||
"mailgun",
|
"mailgun",
|
||||||
"matter",
|
"matter",
|
||||||
"mazda",
|
|
||||||
"meater",
|
"meater",
|
||||||
"medcom_ble",
|
"medcom_ble",
|
||||||
"melcloud",
|
"melcloud",
|
||||||
|
@ -3258,12 +3258,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
},
|
},
|
||||||
"mazda": {
|
|
||||||
"name": "Mazda Connected Services",
|
|
||||||
"integration_type": "hub",
|
|
||||||
"config_flow": true,
|
|
||||||
"iot_class": "cloud_polling"
|
|
||||||
},
|
|
||||||
"meater": {
|
"meater": {
|
||||||
"name": "Meater",
|
"name": "Meater",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
@ -318,6 +318,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
|
|||||||
_LOGGER.error("Timeout resolving OAuth token: %s", err)
|
_LOGGER.error("Timeout resolving OAuth token: %s", err)
|
||||||
return self.async_abort(reason="oauth2_timeout")
|
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
|
# Force int for non-compliant oauth2 providers
|
||||||
try:
|
try:
|
||||||
token["expires_in"] = int(token["expires_in"])
|
token["expires_in"] = int(token["expires_in"])
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
aiodiscover==1.5.1
|
aiodiscover==1.5.1
|
||||||
aiohttp==3.8.5
|
aiohttp==3.8.6
|
||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
astral==2.2
|
astral==2.2
|
||||||
async-upnp-client==0.36.1
|
async-upnp-client==0.36.1
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2023.10.1"
|
version = "2023.10.2"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
@ -23,7 +23,7 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
requires-python = ">=3.11.0"
|
requires-python = ">=3.11.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp==3.8.5",
|
"aiohttp==3.8.6",
|
||||||
"astral==2.2",
|
"astral==2.2",
|
||||||
"attrs==23.1.0",
|
"attrs==23.1.0",
|
||||||
"atomicwrites-homeassistant==1.4.1",
|
"atomicwrites-homeassistant==1.4.1",
|
||||||
@ -437,7 +437,7 @@ filterwarnings = [
|
|||||||
# -- design choice 3rd party
|
# -- design choice 3rd party
|
||||||
# https://github.com/gwww/elkm1/blob/2.2.5/elkm1_lib/util.py#L8-L19
|
# 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",
|
"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",
|
"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
|
# https://github.com/bachya/regenmaschine/blob/2023.08.0/regenmaschine/client.py#L51
|
||||||
"ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client",
|
"ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
-c homeassistant/package_constraints.txt
|
-c homeassistant/package_constraints.txt
|
||||||
|
|
||||||
# Home Assistant Core
|
# Home Assistant Core
|
||||||
aiohttp==3.8.5
|
aiohttp==3.8.6
|
||||||
astral==2.2
|
astral==2.2
|
||||||
attrs==23.1.0
|
attrs==23.1.0
|
||||||
atomicwrites-homeassistant==1.4.1
|
atomicwrites-homeassistant==1.4.1
|
||||||
|
@ -64,7 +64,7 @@ PyFlick==0.0.2
|
|||||||
PyFlume==0.6.5
|
PyFlume==0.6.5
|
||||||
|
|
||||||
# homeassistant.components.fronius
|
# homeassistant.components.fronius
|
||||||
PyFronius==0.7.1
|
PyFronius==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.mvglive
|
# homeassistant.components.mvglive
|
||||||
PyMVGLive==1.1.4
|
PyMVGLive==1.1.4
|
||||||
@ -324,7 +324,7 @@ aiopvpc==4.2.2
|
|||||||
aiopyarr==23.4.0
|
aiopyarr==23.4.0
|
||||||
|
|
||||||
# homeassistant.components.qnap_qsw
|
# homeassistant.components.qnap_qsw
|
||||||
aioqsw==0.3.4
|
aioqsw==0.3.5
|
||||||
|
|
||||||
# homeassistant.components.recollect_waste
|
# homeassistant.components.recollect_waste
|
||||||
aiorecollect==2023.09.0
|
aiorecollect==2023.09.0
|
||||||
@ -515,7 +515,7 @@ beautifulsoup4==4.12.2
|
|||||||
bellows==0.36.5
|
bellows==0.36.5
|
||||||
|
|
||||||
# homeassistant.components.bmw_connected_drive
|
# homeassistant.components.bmw_connected_drive
|
||||||
bimmer-connected==0.14.0
|
bimmer-connected==0.14.1
|
||||||
|
|
||||||
# homeassistant.components.bizkaibus
|
# homeassistant.components.bizkaibus
|
||||||
bizkaibus==0.1.1
|
bizkaibus==0.1.1
|
||||||
@ -749,7 +749,7 @@ enocean==0.50
|
|||||||
enturclient==0.2.4
|
enturclient==0.2.4
|
||||||
|
|
||||||
# homeassistant.components.environment_canada
|
# homeassistant.components.environment_canada
|
||||||
env-canada==0.5.37
|
env-canada==0.6.0
|
||||||
|
|
||||||
# homeassistant.components.season
|
# homeassistant.components.season
|
||||||
ephem==4.1.2
|
ephem==4.1.2
|
||||||
@ -767,7 +767,7 @@ esphome-dashboard-api==1.2.3
|
|||||||
eternalegypt==0.0.16
|
eternalegypt==0.0.16
|
||||||
|
|
||||||
# homeassistant.components.eufylife_ble
|
# homeassistant.components.eufylife_ble
|
||||||
eufylife-ble-client==0.1.7
|
eufylife-ble-client==0.1.8
|
||||||
|
|
||||||
# homeassistant.components.keyboard_remote
|
# homeassistant.components.keyboard_remote
|
||||||
# evdev==1.6.1
|
# evdev==1.6.1
|
||||||
@ -955,7 +955,7 @@ ha-ffmpeg==3.1.0
|
|||||||
ha-iotawattpy==0.1.1
|
ha-iotawattpy==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.philips_js
|
# homeassistant.components.philips_js
|
||||||
ha-philipsjs==3.1.0
|
ha-philipsjs==3.1.1
|
||||||
|
|
||||||
# homeassistant.components.habitica
|
# homeassistant.components.habitica
|
||||||
habitipy==0.2.0
|
habitipy==0.2.0
|
||||||
@ -1042,7 +1042,7 @@ ical==5.0.1
|
|||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
|
|
||||||
# homeassistant.components.idasen_desk
|
# homeassistant.components.idasen_desk
|
||||||
idasen-ha==1.4
|
idasen-ha==1.4.1
|
||||||
|
|
||||||
# homeassistant.components.network
|
# homeassistant.components.network
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
@ -1538,7 +1538,7 @@ pyCEC==0.5.2
|
|||||||
pyControl4==1.1.0
|
pyControl4==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.duotecno
|
# homeassistant.components.duotecno
|
||||||
pyDuotecno==2023.9.0
|
pyDuotecno==2023.10.0
|
||||||
|
|
||||||
# homeassistant.components.eight_sleep
|
# homeassistant.components.eight_sleep
|
||||||
pyEight==0.3.2
|
pyEight==0.3.2
|
||||||
@ -1833,7 +1833,7 @@ pylitejet==0.5.0
|
|||||||
pylitterbot==2023.4.9
|
pylitterbot==2023.4.9
|
||||||
|
|
||||||
# homeassistant.components.lutron_caseta
|
# homeassistant.components.lutron_caseta
|
||||||
pylutron-caseta==0.18.2
|
pylutron-caseta==0.18.3
|
||||||
|
|
||||||
# homeassistant.components.lutron
|
# homeassistant.components.lutron
|
||||||
pylutron==0.2.8
|
pylutron==0.2.8
|
||||||
@ -1844,9 +1844,6 @@ pymailgunner==1.4
|
|||||||
# homeassistant.components.firmata
|
# homeassistant.components.firmata
|
||||||
pymata-express==1.19
|
pymata-express==1.19
|
||||||
|
|
||||||
# homeassistant.components.mazda
|
|
||||||
pymazda==0.3.11
|
|
||||||
|
|
||||||
# homeassistant.components.mediaroom
|
# homeassistant.components.mediaroom
|
||||||
pymediaroom==0.6.5.4
|
pymediaroom==0.6.5.4
|
||||||
|
|
||||||
@ -2149,7 +2146,7 @@ python-miio==0.5.12
|
|||||||
python-mpd2==3.0.5
|
python-mpd2==3.0.5
|
||||||
|
|
||||||
# homeassistant.components.myq
|
# homeassistant.components.myq
|
||||||
python-myq==3.1.11
|
python-myq==3.1.13
|
||||||
|
|
||||||
# homeassistant.components.mystrom
|
# homeassistant.components.mystrom
|
||||||
python-mystrom==2.2.0
|
python-mystrom==2.2.0
|
||||||
@ -2249,7 +2246,7 @@ pyvolumio==0.1.5
|
|||||||
pywaze==0.5.1
|
pywaze==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.weatherflow
|
# homeassistant.components.weatherflow
|
||||||
pyweatherflowudp==1.4.3
|
pyweatherflowudp==1.4.5
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.9.2
|
||||||
@ -2375,7 +2372,7 @@ satel-integra==0.3.7
|
|||||||
scapy==2.5.0
|
scapy==2.5.0
|
||||||
|
|
||||||
# homeassistant.components.screenlogic
|
# homeassistant.components.screenlogic
|
||||||
screenlogicpy==0.9.1
|
screenlogicpy==0.9.2
|
||||||
|
|
||||||
# homeassistant.components.scsgate
|
# homeassistant.components.scsgate
|
||||||
scsgate==0.1.0
|
scsgate==0.1.0
|
||||||
@ -2523,7 +2520,7 @@ switchbot-api==1.1.0
|
|||||||
synology-srm==0.2.0
|
synology-srm==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.system_bridge
|
# homeassistant.components.system_bridge
|
||||||
systembridgeconnector==3.8.2
|
systembridgeconnector==3.8.4
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.2.0
|
tailscale==0.2.0
|
||||||
|
@ -57,7 +57,7 @@ PyFlick==0.0.2
|
|||||||
PyFlume==0.6.5
|
PyFlume==0.6.5
|
||||||
|
|
||||||
# homeassistant.components.fronius
|
# homeassistant.components.fronius
|
||||||
PyFronius==0.7.1
|
PyFronius==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.met_eireann
|
# homeassistant.components.met_eireann
|
||||||
PyMetEireann==2021.8.0
|
PyMetEireann==2021.8.0
|
||||||
@ -299,7 +299,7 @@ aiopvpc==4.2.2
|
|||||||
aiopyarr==23.4.0
|
aiopyarr==23.4.0
|
||||||
|
|
||||||
# homeassistant.components.qnap_qsw
|
# homeassistant.components.qnap_qsw
|
||||||
aioqsw==0.3.4
|
aioqsw==0.3.5
|
||||||
|
|
||||||
# homeassistant.components.recollect_waste
|
# homeassistant.components.recollect_waste
|
||||||
aiorecollect==2023.09.0
|
aiorecollect==2023.09.0
|
||||||
@ -439,7 +439,7 @@ beautifulsoup4==4.12.2
|
|||||||
bellows==0.36.5
|
bellows==0.36.5
|
||||||
|
|
||||||
# homeassistant.components.bmw_connected_drive
|
# homeassistant.components.bmw_connected_drive
|
||||||
bimmer-connected==0.14.0
|
bimmer-connected==0.14.1
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bleak-retry-connector==3.2.1
|
bleak-retry-connector==3.2.1
|
||||||
@ -605,7 +605,7 @@ energyzero==0.5.0
|
|||||||
enocean==0.50
|
enocean==0.50
|
||||||
|
|
||||||
# homeassistant.components.environment_canada
|
# homeassistant.components.environment_canada
|
||||||
env-canada==0.5.37
|
env-canada==0.6.0
|
||||||
|
|
||||||
# homeassistant.components.season
|
# homeassistant.components.season
|
||||||
ephem==4.1.2
|
ephem==4.1.2
|
||||||
@ -617,7 +617,7 @@ epson-projector==0.5.1
|
|||||||
esphome-dashboard-api==1.2.3
|
esphome-dashboard-api==1.2.3
|
||||||
|
|
||||||
# homeassistant.components.eufylife_ble
|
# homeassistant.components.eufylife_ble
|
||||||
eufylife-ble-client==0.1.7
|
eufylife-ble-client==0.1.8
|
||||||
|
|
||||||
# homeassistant.components.faa_delays
|
# homeassistant.components.faa_delays
|
||||||
faadelays==2023.9.1
|
faadelays==2023.9.1
|
||||||
@ -756,7 +756,7 @@ ha-ffmpeg==3.1.0
|
|||||||
ha-iotawattpy==0.1.1
|
ha-iotawattpy==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.philips_js
|
# homeassistant.components.philips_js
|
||||||
ha-philipsjs==3.1.0
|
ha-philipsjs==3.1.1
|
||||||
|
|
||||||
# homeassistant.components.habitica
|
# homeassistant.components.habitica
|
||||||
habitipy==0.2.0
|
habitipy==0.2.0
|
||||||
@ -822,7 +822,7 @@ ical==5.0.1
|
|||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
|
|
||||||
# homeassistant.components.idasen_desk
|
# homeassistant.components.idasen_desk
|
||||||
idasen-ha==1.4
|
idasen-ha==1.4.1
|
||||||
|
|
||||||
# homeassistant.components.network
|
# homeassistant.components.network
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
@ -1171,7 +1171,7 @@ pyCEC==0.5.2
|
|||||||
pyControl4==1.1.0
|
pyControl4==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.duotecno
|
# homeassistant.components.duotecno
|
||||||
pyDuotecno==2023.9.0
|
pyDuotecno==2023.10.0
|
||||||
|
|
||||||
# homeassistant.components.eight_sleep
|
# homeassistant.components.eight_sleep
|
||||||
pyEight==0.3.2
|
pyEight==0.3.2
|
||||||
@ -1379,7 +1379,7 @@ pylitejet==0.5.0
|
|||||||
pylitterbot==2023.4.9
|
pylitterbot==2023.4.9
|
||||||
|
|
||||||
# homeassistant.components.lutron_caseta
|
# homeassistant.components.lutron_caseta
|
||||||
pylutron-caseta==0.18.2
|
pylutron-caseta==0.18.3
|
||||||
|
|
||||||
# homeassistant.components.mailgun
|
# homeassistant.components.mailgun
|
||||||
pymailgunner==1.4
|
pymailgunner==1.4
|
||||||
@ -1387,9 +1387,6 @@ pymailgunner==1.4
|
|||||||
# homeassistant.components.firmata
|
# homeassistant.components.firmata
|
||||||
pymata-express==1.19
|
pymata-express==1.19
|
||||||
|
|
||||||
# homeassistant.components.mazda
|
|
||||||
pymazda==0.3.11
|
|
||||||
|
|
||||||
# homeassistant.components.melcloud
|
# homeassistant.components.melcloud
|
||||||
pymelcloud==2.5.8
|
pymelcloud==2.5.8
|
||||||
|
|
||||||
@ -1599,7 +1596,7 @@ python-matter-server==3.7.0
|
|||||||
python-miio==0.5.12
|
python-miio==0.5.12
|
||||||
|
|
||||||
# homeassistant.components.myq
|
# homeassistant.components.myq
|
||||||
python-myq==3.1.11
|
python-myq==3.1.13
|
||||||
|
|
||||||
# homeassistant.components.mystrom
|
# homeassistant.components.mystrom
|
||||||
python-mystrom==2.2.0
|
python-mystrom==2.2.0
|
||||||
@ -1675,7 +1672,7 @@ pyvolumio==0.1.5
|
|||||||
pywaze==0.5.1
|
pywaze==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.weatherflow
|
# homeassistant.components.weatherflow
|
||||||
pyweatherflowudp==1.4.3
|
pyweatherflowudp==1.4.5
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.9.2
|
||||||
@ -1762,7 +1759,7 @@ samsungtvws[async,encrypted]==2.6.0
|
|||||||
scapy==2.5.0
|
scapy==2.5.0
|
||||||
|
|
||||||
# homeassistant.components.screenlogic
|
# homeassistant.components.screenlogic
|
||||||
screenlogicpy==0.9.1
|
screenlogicpy==0.9.2
|
||||||
|
|
||||||
# homeassistant.components.backup
|
# homeassistant.components.backup
|
||||||
securetar==2023.3.0
|
securetar==2023.3.0
|
||||||
@ -1877,7 +1874,7 @@ surepy==0.8.0
|
|||||||
switchbot-api==1.1.0
|
switchbot-api==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.system_bridge
|
# homeassistant.components.system_bridge
|
||||||
systembridgeconnector==3.8.2
|
systembridgeconnector==3.8.4
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.2.0
|
tailscale==0.2.0
|
||||||
|
@ -615,5 +615,5 @@ async def test_airzone_climate_set_temp_range(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
state = hass.states.get("climate.dkn_plus")
|
state = hass.states.get("climate.dkn_plus")
|
||||||
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0
|
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 20.0
|
||||||
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 20.0
|
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 25.0
|
||||||
|
@ -181,8 +181,7 @@ class MockWakeWordEntity(wake_word.WakeWordDetectionEntity):
|
|||||||
url_path = "wake_word.test"
|
url_path = "wake_word.test"
|
||||||
_attr_name = "test"
|
_attr_name = "test"
|
||||||
|
|
||||||
@property
|
async def get_supported_wake_words(self) -> list[wake_word.WakeWord]:
|
||||||
def supported_wake_words(self) -> list[wake_word.WakeWord]:
|
|
||||||
"""Return a list of supported wake words."""
|
"""Return a list of supported wake words."""
|
||||||
return [wake_word.WakeWord(id="test_ww", name="Test Wake Word")]
|
return [wake_word.WakeWord(id="test_ww", name="Test Wake Word")]
|
||||||
|
|
||||||
@ -191,7 +190,7 @@ class MockWakeWordEntity(wake_word.WakeWordDetectionEntity):
|
|||||||
) -> wake_word.DetectionResult | None:
|
) -> wake_word.DetectionResult | None:
|
||||||
"""Try to detect wake word(s) in an audio stream with timestamps."""
|
"""Try to detect wake word(s) in an audio stream with timestamps."""
|
||||||
if wake_word_id is None:
|
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:
|
async for chunk, timestamp in stream:
|
||||||
if chunk.startswith(b"wake word"):
|
if chunk.startswith(b"wake word"):
|
||||||
return wake_word.DetectionResult(
|
return wake_word.DetectionResult(
|
||||||
|
@ -824,6 +824,11 @@
|
|||||||
}),
|
}),
|
||||||
'has_combustion_drivetrain': False,
|
'has_combustion_drivetrain': False,
|
||||||
'has_electric_drivetrain': True,
|
'has_electric_drivetrain': True,
|
||||||
|
'headunit': dict({
|
||||||
|
'headunit_type': 'MGU',
|
||||||
|
'idrive_version': 'ID8',
|
||||||
|
'software_version': '07/2021.00',
|
||||||
|
}),
|
||||||
'is_charging_plan_supported': True,
|
'is_charging_plan_supported': True,
|
||||||
'is_lsc_enabled': True,
|
'is_lsc_enabled': True,
|
||||||
'is_remote_charge_start_enabled': True,
|
'is_remote_charge_start_enabled': True,
|
||||||
@ -1685,6 +1690,11 @@
|
|||||||
}),
|
}),
|
||||||
'has_combustion_drivetrain': False,
|
'has_combustion_drivetrain': False,
|
||||||
'has_electric_drivetrain': True,
|
'has_electric_drivetrain': True,
|
||||||
|
'headunit': dict({
|
||||||
|
'headunit_type': 'MGU',
|
||||||
|
'idrive_version': 'ID8',
|
||||||
|
'software_version': '11/2021.70',
|
||||||
|
}),
|
||||||
'is_charging_plan_supported': True,
|
'is_charging_plan_supported': True,
|
||||||
'is_lsc_enabled': True,
|
'is_lsc_enabled': True,
|
||||||
'is_remote_charge_start_enabled': False,
|
'is_remote_charge_start_enabled': False,
|
||||||
@ -2318,6 +2328,11 @@
|
|||||||
}),
|
}),
|
||||||
'has_combustion_drivetrain': True,
|
'has_combustion_drivetrain': True,
|
||||||
'has_electric_drivetrain': False,
|
'has_electric_drivetrain': False,
|
||||||
|
'headunit': dict({
|
||||||
|
'headunit_type': 'MGU',
|
||||||
|
'idrive_version': 'ID7',
|
||||||
|
'software_version': '07/2021.70',
|
||||||
|
}),
|
||||||
'is_charging_plan_supported': False,
|
'is_charging_plan_supported': False,
|
||||||
'is_lsc_enabled': True,
|
'is_lsc_enabled': True,
|
||||||
'is_remote_charge_start_enabled': False,
|
'is_remote_charge_start_enabled': False,
|
||||||
@ -3015,6 +3030,11 @@
|
|||||||
}),
|
}),
|
||||||
'has_combustion_drivetrain': True,
|
'has_combustion_drivetrain': True,
|
||||||
'has_electric_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_charging_plan_supported': True,
|
||||||
'is_lsc_enabled': True,
|
'is_lsc_enabled': True,
|
||||||
'is_remote_charge_start_enabled': False,
|
'is_remote_charge_start_enabled': False,
|
||||||
@ -5346,6 +5366,11 @@
|
|||||||
}),
|
}),
|
||||||
'has_combustion_drivetrain': True,
|
'has_combustion_drivetrain': True,
|
||||||
'has_electric_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_charging_plan_supported': True,
|
||||||
'is_lsc_enabled': True,
|
'is_lsc_enabled': True,
|
||||||
'is_remote_charge_start_enabled': False,
|
'is_remote_charge_start_enabled': False,
|
||||||
|
@ -6,6 +6,7 @@ from typing import Any
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.energy import data
|
from homeassistant.components.energy import data
|
||||||
|
from homeassistant.components.recorder.util import session_scope
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
ATTR_LAST_RESET,
|
ATTR_LAST_RESET,
|
||||||
ATTR_STATE_CLASS,
|
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."""
|
"""Test energy cost price from total_increasing type sensor entity."""
|
||||||
|
|
||||||
def _compile_statistics(_):
|
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 = {
|
energy_attributes = {
|
||||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
|
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
@ -365,9 +369,10 @@ async def test_cost_sensor_price_entity_total(
|
|||||||
"""Test energy cost price from total type sensor entity."""
|
"""Test energy cost price from total type sensor entity."""
|
||||||
|
|
||||||
def _compile_statistics(_):
|
def _compile_statistics(_):
|
||||||
return compile_statistics(
|
with session_scope(hass=hass) as session:
|
||||||
hass, now, now + timedelta(seconds=0.17)
|
return compile_statistics(
|
||||||
).platform_stats
|
hass, session, now, now + timedelta(seconds=0.17)
|
||||||
|
).platform_stats
|
||||||
|
|
||||||
energy_attributes = {
|
energy_attributes = {
|
||||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
|
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
@ -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."""
|
"""Test energy cost price from total type sensor entity with no last_reset."""
|
||||||
|
|
||||||
def _compile_statistics(_):
|
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 = {
|
energy_attributes = {
|
||||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
|
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
|
@ -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(
|
async def test_add_event_failure(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from bleak import BleakError
|
from bleak import BleakError
|
||||||
|
from idasen_ha import AuthFailedError
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries
|
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(
|
async def test_user_step_cannot_connect(
|
||||||
hass: HomeAssistant, exception: Exception
|
hass: HomeAssistant, exception: Exception
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test user step and we cannot connect."""
|
"""Test user step with a cannot connect error."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
|
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
|
||||||
return_value=[IDASEN_DISCOVERY_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
|
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:
|
async def test_user_step_unknown_exception(hass: HomeAssistant) -> None:
|
||||||
"""Test user step with an unknown exception."""
|
"""Test user step with an unknown exception."""
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -1,80 +1 @@
|
|||||||
"""Tests for the Mazda Connected Services integration."""
|
"""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
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"temperature": 20,
|
|
||||||
"temperatureUnit": "C",
|
|
||||||
"frontDefroster": true,
|
|
||||||
"rearDefroster": false
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
@ -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"
|
|
@ -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"
|
|
@ -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
|
|
||||||
)
|
|
@ -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"
|
|
@ -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"
|
|
@ -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)
|
|
@ -1,365 +1,50 @@
|
|||||||
"""Tests for the Mazda Connected Services integration."""
|
"""Tests for the Mazda Connected Services integration."""
|
||||||
from datetime import timedelta
|
|
||||||
import json
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from pymazda import MazdaAuthenticationException, MazdaException
|
from homeassistant.components.mazda import DOMAIN
|
||||||
import pytest
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.mazda.const import DOMAIN
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
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.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers import device_registry as dr
|
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from . import init_integration
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_config_entry_not_ready(hass: HomeAssistant) -> None:
|
async def test_mazda_repair_issue(
|
||||||
"""Test the Mazda configuration entry not ready."""
|
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||||
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
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test service calls."""
|
"""Test the Mazda configuration entry loading/unloading handles the repair."""
|
||||||
client_mock = await init_integration(hass)
|
config_entry_1 = MockConfigEntry(
|
||||||
|
title="Example 1",
|
||||||
device_registry = dr.async_get(hass)
|
domain=DOMAIN,
|
||||||
reg_device = device_registry.async_get_device(
|
|
||||||
identifiers={(DOMAIN, "JM000000000000000")},
|
|
||||||
)
|
)
|
||||||
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
|
# Add a second one
|
||||||
|
config_entry_2 = MockConfigEntry(
|
||||||
await hass.services.async_call(DOMAIN, service, service_data, blocking=True)
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
api_method = getattr(client_mock, service)
|
assert config_entry_2.state is ConfigEntryState.LOADED
|
||||||
api_method.assert_called_once_with(*expected_args)
|
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
|
||||||
|
|
||||||
|
# Remove the first one
|
||||||
|
await hass.config_entries.async_remove(config_entry_1.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
async def test_service_invalid_device_id(hass: HomeAssistant) -> None:
|
assert config_entry_1.state is ConfigEntryState.NOT_LOADED
|
||||||
"""Test service call when the specified device ID is invalid."""
|
assert config_entry_2.state is ConfigEntryState.LOADED
|
||||||
await init_integration(hass)
|
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
|
||||||
|
|
||||||
with pytest.raises(vol.error.MultipleInvalid) as err:
|
# Remove the second one
|
||||||
await hass.services.async_call(
|
await hass.config_entries.async_remove(config_entry_2.entry_id)
|
||||||
DOMAIN,
|
await hass.async_block_till_done()
|
||||||
"send_poi",
|
|
||||||
{
|
|
||||||
"device_id": "invalid",
|
|
||||||
"latitude": 1.2345,
|
|
||||||
"longitude": 6.7890,
|
|
||||||
"poi_name": "poi_name",
|
|
||||||
},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
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.NOT_LOADED
|
||||||
|
assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None
|
||||||
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,
|
|
||||||
)
|
|
||||||
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"
|
|
||||||
|
@ -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()
|
|
@ -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"
|
|
@ -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()
|
|
@ -123,7 +123,6 @@ async def test_setting_sensor_value_expires_availability_topic(
|
|||||||
"name": "test",
|
"name": "test",
|
||||||
"state_topic": "test-topic",
|
"state_topic": "test-topic",
|
||||||
"expire_after": 4,
|
"expire_after": 4,
|
||||||
"force_update": True,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,6 +199,18 @@ async def expires_helper(hass: HomeAssistant) -> None:
|
|||||||
state = hass.states.get("binary_sensor.test")
|
state = hass.states.get("binary_sensor.test")
|
||||||
assert state.state == STATE_UNAVAILABLE
|
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(
|
async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -339,7 +339,6 @@ async def test_setting_sensor_value_expires_availability_topic(
|
|||||||
"state_topic": "test-topic",
|
"state_topic": "test-topic",
|
||||||
"unit_of_measurement": "fav unit",
|
"unit_of_measurement": "fav unit",
|
||||||
"expire_after": "4",
|
"expire_after": "4",
|
||||||
"force_update": True,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -413,6 +412,18 @@ async def expires_helper(hass: HomeAssistant) -> None:
|
|||||||
state = hass.states.get("sensor.test")
|
state = hass.states.get("sensor.test")
|
||||||
assert state.state == STATE_UNAVAILABLE
|
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(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
|
@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||||||
from mysensors.sensor import Sensor
|
from mysensors.sensor import Sensor
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
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
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ async def test_door_sensor(
|
|||||||
assert state
|
assert state
|
||||||
assert state.state == "off"
|
assert state.state == "off"
|
||||||
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR
|
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
receive_message("1;1;1;0;16;1\n")
|
receive_message("1;1;1;0;16;1\n")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.components.climate import (
|
|||||||
SERVICE_SET_TEMPERATURE,
|
SERVICE_SET_TEMPERATURE,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_ENTITY_ID
|
from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ async def test_hvac_node_auto(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == HVACMode.OFF
|
assert state.state == HVACMode.OFF
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
# Test set hvac mode auto
|
# Test set hvac mode auto
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
@ -150,6 +151,7 @@ async def test_hvac_node_heat(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == HVACMode.OFF
|
assert state.state == HVACMode.OFF
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
# Test set hvac mode heat
|
# Test set hvac mode heat
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
@ -259,6 +261,7 @@ async def test_hvac_node_cool(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == HVACMode.OFF
|
assert state.state == HVACMode.OFF
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
# Test set hvac mode heat
|
# Test set hvac mode heat
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.components.cover import (
|
|||||||
STATE_OPEN,
|
STATE_OPEN,
|
||||||
STATE_OPENING,
|
STATE_OPENING,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_ENTITY_ID
|
from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ async def test_cover_node_percentage(
|
|||||||
assert state
|
assert state
|
||||||
assert state.state == STATE_CLOSED
|
assert state.state == STATE_CLOSED
|
||||||
assert state.attributes[ATTR_CURRENT_POSITION] == 0
|
assert state.attributes[ATTR_CURRENT_POSITION] == 0
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
COVER_DOMAIN,
|
COVER_DOMAIN,
|
||||||
|
@ -6,7 +6,12 @@ from collections.abc import Callable
|
|||||||
from mysensors.sensor import Sensor
|
from mysensors.sensor import Sensor
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SourceType
|
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
|
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_SOURCE_TYPE] == SourceType.GPS
|
||||||
assert state.attributes[ATTR_LATITUDE] == float(latitude)
|
assert state.attributes[ATTR_LATITUDE] == float(latitude)
|
||||||
assert state.attributes[ATTR_LONGITUDE] == float(longitude)
|
assert state.attributes[ATTR_LONGITUDE] == float(longitude)
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
latitude = "40.782"
|
latitude = "40.782"
|
||||||
longitude = "-73.965"
|
longitude = "-73.965"
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.components.light import (
|
|||||||
ATTR_RGBW_COLOR,
|
ATTR_RGBW_COLOR,
|
||||||
DOMAIN as LIGHT_DOMAIN,
|
DOMAIN as LIGHT_DOMAIN,
|
||||||
)
|
)
|
||||||
|
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ async def test_dimmer_node(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "off"
|
assert state.state == "off"
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
# Test turn on
|
# Test turn on
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
@ -108,6 +110,7 @@ async def test_rgb_node(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "off"
|
assert state.state == "off"
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
# Test turn on
|
# Test turn on
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
@ -218,6 +221,7 @@ async def test_rgbw_node(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "off"
|
assert state.state == "off"
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
# Test turn on
|
# Test turn on
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -14,7 +14,12 @@ from homeassistant.components.remote import (
|
|||||||
SERVICE_LEARN_COMMAND,
|
SERVICE_LEARN_COMMAND,
|
||||||
SERVICE_SEND_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
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +36,7 @@ async def test_ir_transceiver(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "off"
|
assert state.state == "off"
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
# Test turn on
|
# Test turn on
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_BATTERY_LEVEL,
|
||||||
ATTR_DEVICE_CLASS,
|
ATTR_DEVICE_CLASS,
|
||||||
ATTR_ICON,
|
ATTR_ICON,
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
@ -41,6 +42,7 @@ async def test_gps_sensor(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "40.741894,-73.989311,12"
|
assert state.state == "40.741894,-73.989311,12"
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
altitude = 0
|
altitude = 0
|
||||||
new_coords = "40.782,-73.965"
|
new_coords = "40.782,-73.965"
|
||||||
@ -67,6 +69,7 @@ async def test_ir_transceiver(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "test_code"
|
assert state.state == "test_code"
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
receive_message("1;1;1;0;50;new_code\n")
|
receive_message("1;1;1;0;50;new_code\n")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -87,6 +90,7 @@ async def test_battery_entity(
|
|||||||
state = hass.states.get(battery_entity_id)
|
state = hass.states.get(battery_entity_id)
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "42"
|
assert state.state == "42"
|
||||||
|
assert ATTR_BATTERY_LEVEL not in state.attributes
|
||||||
|
|
||||||
receive_message("1;255;3;0;0;84\n")
|
receive_message("1;255;3;0;0;84\n")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -94,6 +98,7 @@ async def test_battery_entity(
|
|||||||
state = hass.states.get(battery_entity_id)
|
state = hass.states.get(battery_entity_id)
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "84"
|
assert state.state == "84"
|
||||||
|
assert ATTR_BATTERY_LEVEL not in state.attributes
|
||||||
|
|
||||||
|
|
||||||
async def test_power_sensor(
|
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_DEVICE_CLASS] == SensorDeviceClass.POWER
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT
|
||||||
assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT
|
assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_energy_sensor(
|
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_DEVICE_CLASS] == SensorDeviceClass.ENERGY
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.KILO_WATT_HOUR
|
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_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_sound_sensor(
|
async def test_sound_sensor(
|
||||||
@ -144,6 +151,7 @@ async def test_sound_sensor(
|
|||||||
assert state.state == "10"
|
assert state.state == "10"
|
||||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.SOUND_PRESSURE
|
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.SOUND_PRESSURE
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "dB"
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "dB"
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_distance_sensor(
|
async def test_distance_sensor(
|
||||||
@ -161,6 +169,7 @@ async def test_distance_sensor(
|
|||||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DISTANCE
|
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DISTANCE
|
||||||
assert ATTR_ICON not in state.attributes
|
assert ATTR_ICON not in state.attributes
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "cm"
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "cm"
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -193,3 +202,4 @@ async def test_temperature_sensor(
|
|||||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
|
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit
|
||||||
assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT
|
assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
@ -7,6 +7,7 @@ from unittest.mock import MagicMock, call
|
|||||||
from mysensors.sensor import Sensor
|
from mysensors.sensor import Sensor
|
||||||
|
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ async def test_relay_node(
|
|||||||
|
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "off"
|
assert state.state == "off"
|
||||||
|
assert state.attributes[ATTR_BATTERY_LEVEL] == 0
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SWITCH_DOMAIN,
|
SWITCH_DOMAIN,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user