mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add husqvarna automower ble integration (#108326)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
parent
759fe54132
commit
b3cb2ac3ee
@ -659,6 +659,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
||||
/tests/components/husqvarna_automower/ @Thomas55555
|
||||
/homeassistant/components/husqvarna_automower_ble/ @alistair23
|
||||
/tests/components/husqvarna_automower_ble/ @alistair23
|
||||
/homeassistant/components/huum/ @frwickst
|
||||
/tests/components/huum/ @frwickst
|
||||
/homeassistant/components/hvv_departures/ @vigonotion
|
||||
|
5
homeassistant/brands/husqvarna.json
Normal file
5
homeassistant/brands/husqvarna.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "husqvarna",
|
||||
"name": "Husqvarna",
|
||||
"integrations": ["husqvarna_automower", "husqvarna_automower_ble"]
|
||||
}
|
63
homeassistant/components/husqvarna_automower_ble/__init__.py
Normal file
63
homeassistant/components/husqvarna_automower_ble/__init__.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""The Husqvarna Autoconnect Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from automower_ble.mower import Mower
|
||||
from bleak import BleakError
|
||||
from bleak_retry_connector import close_stale_connections_by_address, get_device
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import LOGGER
|
||||
from .coordinator import HusqvarnaCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.LAWN_MOWER,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Husqvarna Autoconnect Bluetooth from a config entry."""
|
||||
address = entry.data[CONF_ADDRESS]
|
||||
channel_id = entry.data[CONF_CLIENT_ID]
|
||||
|
||||
mower = Mower(channel_id, address)
|
||||
|
||||
await close_stale_connections_by_address(address)
|
||||
|
||||
LOGGER.debug("connecting to %s with channel ID %s", address, str(channel_id))
|
||||
try:
|
||||
device = bluetooth.async_ble_device_from_address(
|
||||
hass, address, connectable=True
|
||||
) or await get_device(address)
|
||||
if not await mower.connect(device):
|
||||
raise ConfigEntryNotReady
|
||||
except (TimeoutError, BleakError) as exception:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to device {address} due to {exception}"
|
||||
) from exception
|
||||
LOGGER.debug("connected and paired")
|
||||
|
||||
model = await mower.get_model()
|
||||
LOGGER.debug("Connected to Automower: %s", model)
|
||||
|
||||
coordinator = HusqvarnaCoordinator(hass, mower, address, channel_id, model)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
coordinator: HusqvarnaCoordinator = entry.runtime_data
|
||||
await coordinator.async_shutdown()
|
||||
|
||||
return unload_ok
|
121
homeassistant/components/husqvarna_automower_ble/config_flow.py
Normal file
121
homeassistant/components/husqvarna_automower_ble/config_flow.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""Config flow for Husqvarna Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from automower_ble.mower import Mower
|
||||
from bleak import BleakError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth import BluetoothServiceInfo
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
def _is_supported(discovery_info: BluetoothServiceInfo):
|
||||
"""Check if device is supported."""
|
||||
|
||||
LOGGER.debug(
|
||||
"%s manufacturer data: %s",
|
||||
discovery_info.address,
|
||||
discovery_info.manufacturer_data,
|
||||
)
|
||||
|
||||
manufacturer = any(key == 1062 for key in discovery_info.manufacturer_data)
|
||||
service_husqvarna = any(
|
||||
service == "98bd0001-0b0e-421a-84e5-ddbf75dc6de4"
|
||||
for service in discovery_info.service_uuids
|
||||
)
|
||||
service_generic = any(
|
||||
service == "00001800-0000-1000-8000-00805f9b34fb"
|
||||
for service in discovery_info.service_uuids
|
||||
)
|
||||
|
||||
return manufacturer and service_husqvarna and service_generic
|
||||
|
||||
|
||||
class HusqvarnaAutomowerBleConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Husqvarna Bluetooth."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self.address: str | None
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the bluetooth discovery step."""
|
||||
|
||||
LOGGER.debug("Discovered device: %s", discovery_info)
|
||||
if not _is_supported(discovery_info):
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
self.address = discovery_info.address
|
||||
await self.async_set_unique_id(self.address)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
assert self.address
|
||||
|
||||
device = bluetooth.async_ble_device_from_address(
|
||||
self.hass, self.address, connectable=True
|
||||
)
|
||||
channel_id = random.randint(1, 0xFFFFFFFF)
|
||||
|
||||
try:
|
||||
(manufacturer, device_type, model) = await Mower(
|
||||
channel_id, self.address
|
||||
).probe_gatts(device)
|
||||
except (BleakError, TimeoutError) as exception:
|
||||
LOGGER.exception("Failed to connect to device: %s", exception)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
title = manufacturer + " " + device_type
|
||||
|
||||
LOGGER.debug("Found device: %s", title)
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=title,
|
||||
data={CONF_ADDRESS: self.address, CONF_CLIENT_ID: channel_id},
|
||||
)
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
"name": title,
|
||||
}
|
||||
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
description_placeholders=self.context["title_placeholders"],
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is not None:
|
||||
self.address = user_input[CONF_ADDRESS]
|
||||
await self.async_set_unique_id(self.address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self.async_step_confirm()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): str,
|
||||
},
|
||||
),
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
"""Constants for the Husqvarna Automower Bluetooth integration."""
|
||||
|
||||
import logging
|
||||
|
||||
DOMAIN = "husqvarna_automower_ble"
|
||||
MANUFACTURER = "Husqvarna"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
100
homeassistant/components/husqvarna_automower_ble/coordinator.py
Normal file
100
homeassistant/components/husqvarna_automower_ble/coordinator.py
Normal file
@ -0,0 +1,100 @@
|
||||
"""Provides the DataUpdateCoordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from automower_ble.mower import Mower
|
||||
from bleak import BleakError
|
||||
from bleak_retry_connector import close_stale_connections_by_address
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, bytes]]):
|
||||
"""Class to manage fetching data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
mower: Mower,
|
||||
address: str,
|
||||
channel_id: str,
|
||||
model: str,
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.address = address
|
||||
self.channel_id = channel_id
|
||||
self.model = model
|
||||
self.mower = mower
|
||||
|
||||
async def async_shutdown(self) -> None:
|
||||
"""Shutdown coordinator and any connection."""
|
||||
LOGGER.debug("Shutdown")
|
||||
await super().async_shutdown()
|
||||
if self.mower.is_connected():
|
||||
await self.mower.disconnect()
|
||||
|
||||
async def _async_find_device(self):
|
||||
LOGGER.debug("Trying to reconnect")
|
||||
await close_stale_connections_by_address(self.address)
|
||||
|
||||
device = bluetooth.async_ble_device_from_address(
|
||||
self.hass, self.address, connectable=True
|
||||
)
|
||||
|
||||
try:
|
||||
if not await self.mower.connect(device):
|
||||
raise UpdateFailed("Failed to connect")
|
||||
except BleakError as err:
|
||||
raise UpdateFailed("Failed to connect") from err
|
||||
|
||||
async def _async_update_data(self) -> dict[str, bytes]:
|
||||
"""Poll the device."""
|
||||
LOGGER.debug("Polling device")
|
||||
|
||||
data: dict[str, bytes] = {}
|
||||
|
||||
try:
|
||||
if not self.mower.is_connected():
|
||||
await self._async_find_device()
|
||||
except BleakError as err:
|
||||
raise UpdateFailed("Failed to connect") from err
|
||||
|
||||
try:
|
||||
data["battery_level"] = await self.mower.battery_level()
|
||||
LOGGER.debug(data["battery_level"])
|
||||
if data["battery_level"] is None:
|
||||
await self._async_find_device()
|
||||
raise UpdateFailed("Error getting data from device")
|
||||
|
||||
data["activity"] = await self.mower.mower_activity()
|
||||
LOGGER.debug(data["activity"])
|
||||
if data["activity"] is None:
|
||||
await self._async_find_device()
|
||||
raise UpdateFailed("Error getting data from device")
|
||||
|
||||
data["state"] = await self.mower.mower_state()
|
||||
LOGGER.debug(data["state"])
|
||||
if data["state"] is None:
|
||||
await self._async_find_device()
|
||||
raise UpdateFailed("Error getting data from device")
|
||||
|
||||
except BleakError as err:
|
||||
LOGGER.error("Error getting data from device")
|
||||
await self._async_find_device()
|
||||
raise UpdateFailed("Error getting data from device") from err
|
||||
|
||||
return data
|
30
homeassistant/components/husqvarna_automower_ble/entity.py
Normal file
30
homeassistant/components/husqvarna_automower_ble/entity.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""Provides the HusqvarnaAutomowerBleEntity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import HusqvarnaCoordinator
|
||||
|
||||
|
||||
class HusqvarnaAutomowerBleEntity(CoordinatorEntity[HusqvarnaCoordinator]):
|
||||
"""HusqvarnaCoordinator entity for Husqvarna Automower Bluetooth."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: HusqvarnaCoordinator) -> None:
|
||||
"""Initialize coordinator entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{coordinator.address}_{coordinator.channel_id}")},
|
||||
manufacturer=MANUFACTURER,
|
||||
model_id=coordinator.model,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.coordinator.mower.is_connected()
|
149
homeassistant/components/husqvarna_automower_ble/lawn_mower.py
Normal file
149
homeassistant/components/husqvarna_automower_ble/lawn_mower.py
Normal file
@ -0,0 +1,149 @@
|
||||
"""The Husqvarna Autoconnect Bluetooth lawn mower platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.lawn_mower import (
|
||||
LawnMowerActivity,
|
||||
LawnMowerEntity,
|
||||
LawnMowerEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import LOGGER
|
||||
from .coordinator import HusqvarnaCoordinator
|
||||
from .entity import HusqvarnaAutomowerBleEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AutomowerLawnMower integration from a config entry."""
|
||||
coordinator: HusqvarnaCoordinator = config_entry.runtime_data
|
||||
address = coordinator.address
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
AutomowerLawnMower(
|
||||
coordinator,
|
||||
address,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class AutomowerLawnMower(HusqvarnaAutomowerBleEntity, LawnMowerEntity):
|
||||
"""Husqvarna Automower."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_supported_features = (
|
||||
LawnMowerEntityFeature.PAUSE
|
||||
| LawnMowerEntityFeature.START_MOWING
|
||||
| LawnMowerEntityFeature.DOCK
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HusqvarnaCoordinator,
|
||||
address: str,
|
||||
) -> None:
|
||||
"""Initialize the lawn mower."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = str(address)
|
||||
|
||||
def _get_activity(self) -> LawnMowerActivity | None:
|
||||
"""Return the current lawn mower activity."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
|
||||
state = str(self.coordinator.data["state"])
|
||||
activity = str(self.coordinator.data["activity"])
|
||||
|
||||
if state is None or activity is None:
|
||||
return None
|
||||
|
||||
if state == "paused":
|
||||
return LawnMowerActivity.PAUSED
|
||||
if state in ("stopped", "off", "waitForSafetyPin"):
|
||||
# This is actually stopped, but that isn't an option
|
||||
return LawnMowerActivity.ERROR
|
||||
if state in (
|
||||
"restricted",
|
||||
"inOperation",
|
||||
"unknown",
|
||||
"checkSafety",
|
||||
"pendingStart",
|
||||
):
|
||||
if activity in ("charging", "parked", "none"):
|
||||
return LawnMowerActivity.DOCKED
|
||||
if activity in ("goingOut", "mowing"):
|
||||
return LawnMowerActivity.MOWING
|
||||
if activity in ("goingHome"):
|
||||
return LawnMowerActivity.RETURNING
|
||||
return LawnMowerActivity.ERROR
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
LOGGER.debug("AutomowerLawnMower: _handle_coordinator_update")
|
||||
|
||||
self._attr_activity = self._get_activity()
|
||||
self._attr_available = self._attr_activity is not None
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
async def async_start_mowing(self) -> None:
|
||||
"""Start mowing."""
|
||||
LOGGER.debug("Starting mower")
|
||||
|
||||
if not self.coordinator.mower.is_connected():
|
||||
device = bluetooth.async_ble_device_from_address(
|
||||
self.coordinator.hass, self.coordinator.address, connectable=True
|
||||
)
|
||||
if not await self.coordinator.mower.connect(device):
|
||||
return
|
||||
|
||||
await self.coordinator.mower.mower_resume()
|
||||
if self._attr_activity is LawnMowerActivity.DOCKED:
|
||||
await self.coordinator.mower.mower_override()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
self._attr_activity = self._get_activity()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_dock(self) -> None:
|
||||
"""Start docking."""
|
||||
LOGGER.debug("Start docking")
|
||||
|
||||
if not self.coordinator.mower.is_connected():
|
||||
device = bluetooth.async_ble_device_from_address(
|
||||
self.coordinator.hass, self.coordinator.address, connectable=True
|
||||
)
|
||||
if not await self.coordinator.mower.connect(device):
|
||||
return
|
||||
|
||||
await self.coordinator.mower.mower_park()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
self._attr_activity = self._get_activity()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_pause(self) -> None:
|
||||
"""Pause mower."""
|
||||
LOGGER.debug("Pausing mower")
|
||||
|
||||
if not self.coordinator.mower.is_connected():
|
||||
device = bluetooth.async_ble_device_from_address(
|
||||
self.coordinator.hass, self.coordinator.address, connectable=True
|
||||
)
|
||||
if not await self.coordinator.mower.connect(device):
|
||||
return
|
||||
|
||||
await self.coordinator.mower.mower_pause()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
self._attr_activity = self._get_activity()
|
||||
self.async_write_ha_state()
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"domain": "husqvarna_automower_ble",
|
||||
"name": "Husqvarna Automower BLE",
|
||||
"bluetooth": [
|
||||
{
|
||||
"service_uuid": "98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
"connectable": true
|
||||
}
|
||||
],
|
||||
"codeowners": ["@alistair23"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/???",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["automower-ble==0.1.35"]
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name} ({address})",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "Device BLE address"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"description": "Do you want to set up {name}? Make sure the mower is in pairing mode"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_devices_found": "Ensure the mower is in pairing mode and try again. It can take a few attempts.",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -279,6 +279,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
|
||||
],
|
||||
"manufacturer_id": 76,
|
||||
},
|
||||
{
|
||||
"connectable": True,
|
||||
"domain": "husqvarna_automower_ble",
|
||||
"service_uuid": "98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
},
|
||||
{
|
||||
"domain": "ibeacon",
|
||||
"manufacturer_data_start": [
|
||||
|
@ -264,6 +264,7 @@ FLOWS = {
|
||||
"huisbaasje",
|
||||
"hunterdouglas_powerview",
|
||||
"husqvarna_automower",
|
||||
"husqvarna_automower_ble",
|
||||
"huum",
|
||||
"hvv_departures",
|
||||
"hydrawise",
|
||||
|
@ -2678,11 +2678,22 @@
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "motion_blinds"
|
||||
},
|
||||
"husqvarna_automower": {
|
||||
"name": "Husqvarna Automower",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push"
|
||||
"husqvarna": {
|
||||
"name": "Husqvarna",
|
||||
"integrations": {
|
||||
"husqvarna_automower": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Husqvarna Automower"
|
||||
},
|
||||
"husqvarna_automower_ble": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Husqvarna Automower BLE"
|
||||
}
|
||||
}
|
||||
},
|
||||
"huum": {
|
||||
"name": "Huum",
|
||||
|
@ -523,6 +523,9 @@ aurorapy==0.2.7
|
||||
# homeassistant.components.autarco
|
||||
autarco==3.0.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower_ble
|
||||
automower-ble==0.1.35
|
||||
|
||||
# homeassistant.components.avea
|
||||
# avea==1.5.1
|
||||
|
||||
|
@ -478,6 +478,9 @@ aurorapy==0.2.7
|
||||
# homeassistant.components.autarco
|
||||
autarco==3.0.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower_ble
|
||||
automower-ble==0.1.35
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==63
|
||||
|
||||
|
74
tests/components/husqvarna_automower_ble/__init__.py
Normal file
74
tests/components/husqvarna_automower_ble/__init__.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""Tests for the Husqvarna Automower Bluetooth integration."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||
|
||||
AUTOMOWER_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="305",
|
||||
address="00000000-0000-0000-0000-000000000003",
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"},
|
||||
service_uuids=[
|
||||
"98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
"00001800-0000-1000-8000-00805f9b34fb",
|
||||
],
|
||||
source="local",
|
||||
)
|
||||
|
||||
AUTOMOWER_UNNAMED_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name=None,
|
||||
address="00000000-0000-0000-0000-000000000004",
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"},
|
||||
service_uuids=[
|
||||
"98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
"00001800-0000-1000-8000-00805f9b34fb",
|
||||
],
|
||||
source="local",
|
||||
)
|
||||
|
||||
AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="Missing Manufacturer Data",
|
||||
address="00000000-0000-0000-0002-000000000001",
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={},
|
||||
service_uuids=[
|
||||
"98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
"00001800-0000-1000-8000-00805f9b34fb",
|
||||
],
|
||||
source="local",
|
||||
)
|
||||
|
||||
AUTOMOWER_UNSUPPORTED_GROUP_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="Unsupported Group",
|
||||
address="00000000-0000-0000-0002-000000000002",
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"},
|
||||
service_uuids=[
|
||||
"98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
],
|
||||
source="local",
|
||||
)
|
||||
|
||||
|
||||
async def setup_entry(
|
||||
hass: HomeAssistant, mock_entry: MockConfigEntry, platforms: list[Platform]
|
||||
) -> None:
|
||||
"""Make sure the device is available."""
|
||||
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO)
|
||||
|
||||
with patch("homeassistant.components.husqvarna_automower_ble.PLATFORMS", platforms):
|
||||
mock_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
82
tests/components/husqvarna_automower_ble/conftest.py
Normal file
82
tests/components/husqvarna_automower_ble/conftest.py
Normal file
@ -0,0 +1,82 @@
|
||||
"""Common fixtures for the Husqvarna Automower Bluetooth tests."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.husqvarna_automower_ble.const import DOMAIN
|
||||
from homeassistant.components.husqvarna_automower_ble.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AUTOMOWER_SERVICE_INFO
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.husqvarna_automower_ble.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def scan_step(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory
|
||||
) -> Generator[None, None, Callable[[], Awaitable[None]]]:
|
||||
"""Step system time forward."""
|
||||
|
||||
freezer.move_to("2023-01-01T01:00:00Z")
|
||||
|
||||
async def delay() -> None:
|
||||
"""Trigger delay in system."""
|
||||
freezer.tick(delta=SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return delay
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_automower_client(enable_bluetooth: None, scan_step) -> Generator[AsyncMock]:
|
||||
"""Mock a BleakClient client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.husqvarna_automower_ble.Mower",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.husqvarna_automower_ble.config_flow.Mower",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
client.connect.return_value = True
|
||||
client.is_connected.return_value = True
|
||||
client.get_model.return_value = "305"
|
||||
client.battery_level.return_value = 100
|
||||
client.mower_state.return_value = "pendingStart"
|
||||
client.mower_activity.return_value = "charging"
|
||||
client.probe_gatts.return_value = ("Husqvarna", "Automower", "305")
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Husqvarna AutoMower",
|
||||
data={
|
||||
CONF_ADDRESS: AUTOMOWER_SERVICE_INFO.address,
|
||||
CONF_CLIENT_ID: 1197489078,
|
||||
},
|
||||
unique_id=AUTOMOWER_SERVICE_INFO.address,
|
||||
)
|
@ -0,0 +1,33 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'husqvarna_automower_ble',
|
||||
'00000000-0000-0000-0000-000000000003_1197489078',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Husqvarna',
|
||||
'model': None,
|
||||
'model_id': '305',
|
||||
'name': 'Husqvarna AutoMower',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
198
tests/components/husqvarna_automower_ble/test_config_flow.py
Normal file
198
tests/components/husqvarna_automower_ble/test_config_flow.py
Normal file
@ -0,0 +1,198 @@
|
||||
"""Test the Husqvarna Bluetooth config flow."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from bleak import BleakError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.husqvarna_automower_ble.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import (
|
||||
AUTOMOWER_SERVICE_INFO,
|
||||
AUTOMOWER_UNNAMED_SERVICE_INFO,
|
||||
AUTOMOWER_UNSUPPORTED_GROUP_SERVICE_INFO,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_random() -> Mock:
|
||||
"""Mock random to generate predictable client id."""
|
||||
with patch(
|
||||
"homeassistant.components.husqvarna_automower_ble.config_flow.random"
|
||||
) as mock_random:
|
||||
mock_random.randint.return_value = 1197489078
|
||||
yield mock_random
|
||||
|
||||
|
||||
async def test_user_selection(hass: HomeAssistant) -> None:
|
||||
"""Test we can select a device."""
|
||||
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO)
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_UNNAMED_SERVICE_INFO)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ADDRESS: "00000000-0000-0000-0000-000000000001"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Husqvarna Automower"
|
||||
assert result["result"].unique_id == "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
assert result["data"] == {
|
||||
CONF_ADDRESS: "00000000-0000-0000-0000-000000000001",
|
||||
CONF_CLIENT_ID: 1197489078,
|
||||
}
|
||||
|
||||
|
||||
async def test_bluetooth(hass: HomeAssistant) -> None:
|
||||
"""Test bluetooth device discovery."""
|
||||
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
|
||||
assert result["step_id"] == "confirm"
|
||||
assert result["context"]["unique_id"] == "00000000-0000-0000-0000-000000000003"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Husqvarna Automower"
|
||||
assert result["result"].unique_id == "00000000-0000-0000-0000-000000000003"
|
||||
|
||||
assert result["data"] == {
|
||||
CONF_ADDRESS: "00000000-0000-0000-0000-000000000003",
|
||||
CONF_CLIENT_ID: 1197489078,
|
||||
}
|
||||
|
||||
|
||||
async def test_bluetooth_invalid(hass: HomeAssistant) -> None:
|
||||
"""Test bluetooth device discovery with invalid data."""
|
||||
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_UNSUPPORTED_GROUP_SERVICE_INFO)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=AUTOMOWER_UNSUPPORTED_GROUP_SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
|
||||
async def test_failed_connect(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: Mock,
|
||||
) -> None:
|
||||
"""Test we can select a device."""
|
||||
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO)
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_UNNAMED_SERVICE_INFO)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
mock_automower_client.connect.side_effect = False
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ADDRESS: "00000000-0000-0000-0000-000000000001"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Husqvarna Automower"
|
||||
assert result["result"].unique_id == "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
assert result["data"] == {
|
||||
CONF_ADDRESS: "00000000-0000-0000-0000-000000000001",
|
||||
CONF_CLIENT_ID: 1197489078,
|
||||
}
|
||||
|
||||
|
||||
async def test_duplicate_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test we can select a device."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO)
|
||||
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Test we should not discover the already configured device
|
||||
assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 0
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ADDRESS: "00000000-0000-0000-0000-000000000003"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_exception_connect(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: Mock,
|
||||
) -> None:
|
||||
"""Test we can select a device."""
|
||||
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO)
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_UNNAMED_SERVICE_INFO)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
mock_automower_client.probe_gatts.side_effect = BleakError
|
||||
|
||||
result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
71
tests/components/husqvarna_automower_ble/test_init.py
Normal file
71
tests/components/husqvarna_automower_ble/test_init.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""Test the Husqvarna Automower Bluetooth setup."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from bleak import BleakError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.husqvarna_automower_ble.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import AUTOMOWER_SERVICE_INFO
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_automower_client")
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test setup creates expected devices."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, f"{AUTOMOWER_SERVICE_INFO.address}_1197489078")}
|
||||
)
|
||||
|
||||
assert device_entry == snapshot
|
||||
|
||||
|
||||
async def test_setup_retry_connect(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setup creates expected devices."""
|
||||
|
||||
mock_automower_client.connect.return_value = False
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_failed_connect(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setup creates expected devices."""
|
||||
|
||||
mock_automower_client.connect.side_effect = BleakError
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
126
tests/components/husqvarna_automower_ble/test_lawn_mower.py
Normal file
126
tests/components/husqvarna_automower_ble/test_lawn_mower.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""Test the Husqvarna Automower Bluetooth setup."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import Mock
|
||||
|
||||
from bleak import BleakError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_automower_client")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"is_connected_side_effect",
|
||||
"is_connected_return_value",
|
||||
"connect_side_effect",
|
||||
"connect_return_value",
|
||||
),
|
||||
[
|
||||
(None, False, None, False),
|
||||
(None, False, BleakError, False),
|
||||
(None, False, None, True),
|
||||
(BleakError, False, None, True),
|
||||
],
|
||||
)
|
||||
async def test_setup_disconnect(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
is_connected_side_effect: Exception,
|
||||
is_connected_return_value: bool,
|
||||
connect_side_effect: Exception,
|
||||
connect_return_value: bool,
|
||||
) -> None:
|
||||
"""Test disconnected device."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert hass.states.get("lawn_mower.husqvarna_automower").state != STATE_UNAVAILABLE
|
||||
|
||||
mock_automower_client.is_connected.side_effect = is_connected_side_effect
|
||||
mock_automower_client.is_connected.return_value = is_connected_return_value
|
||||
mock_automower_client.connect.side_effect = connect_side_effect
|
||||
mock_automower_client.connect.return_value = connect_return_value
|
||||
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("lawn_mower.husqvarna_automower").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("attribute"),
|
||||
[
|
||||
"mower_activity",
|
||||
"mower_state",
|
||||
"battery_level",
|
||||
],
|
||||
)
|
||||
async def test_invalid_data_received(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
attribute: str,
|
||||
) -> None:
|
||||
"""Test invalid data received."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
getattr(mock_automower_client, attribute).return_value = None
|
||||
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("lawn_mower.husqvarna_automower").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("attribute"),
|
||||
[
|
||||
"mower_activity",
|
||||
"mower_state",
|
||||
"battery_level",
|
||||
],
|
||||
)
|
||||
async def test_bleak_error_data_update(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
attribute: str,
|
||||
) -> None:
|
||||
"""Test BleakError during data update."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
getattr(mock_automower_client, attribute).side_effect = BleakError
|
||||
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("lawn_mower.husqvarna_automower").state == STATE_UNAVAILABLE
|
Loading…
x
Reference in New Issue
Block a user