mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add Husqvarna Automower integration (#109073)
* Add Husqvarna Automower * Update homeassistant/components/husqvarna_automower/__init__.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/husqvarna_automower/config_flow.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/husqvarna_automower/entity.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/husqvarna_automower/entity.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/husqvarna_automower/lawn_mower.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/husqvarna_automower/lawn_mower.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * address review * add test_config_non_unique_profile * add missing const * WIP tests * tests * tests * Update homeassistant/components/husqvarna_automower/api.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/husqvarna_automower/config_flow.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/husqvarna_automower/config_flow.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update tests/components/husqvarna_automower/conftest.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * . * loop through test * Update homeassistant/components/husqvarna_automower/entity.py * Update homeassistant/components/husqvarna_automower/coordinator.py * Update homeassistant/components/husqvarna_automower/coordinator.py * Apply suggestions from code review * ruff --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
6f3be3e505
commit
6d4ab6c758
@ -584,6 +584,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/humidifier/ @home-assistant/core @Shulyaka
|
/tests/components/humidifier/ @home-assistant/core @Shulyaka
|
||||||
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||||
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||||
|
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
||||||
|
/tests/components/husqvarna_automower/ @Thomas55555
|
||||||
/homeassistant/components/huum/ @frwickst
|
/homeassistant/components/huum/ @frwickst
|
||||||
/tests/components/huum/ @frwickst
|
/tests/components/huum/ @frwickst
|
||||||
/homeassistant/components/hvv_departures/ @vigonotion
|
/homeassistant/components/hvv_departures/ @vigonotion
|
||||||
|
62
homeassistant/components/husqvarna_automower/__init__.py
Normal file
62
homeassistant/components/husqvarna_automower/__init__.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""The Husqvarna Automower integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioautomower.session import AutomowerSession
|
||||||
|
from aiohttp import ClientError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AutomowerDataUpdateCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [
|
||||||
|
Platform.LAWN_MOWER,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up this integration using UI."""
|
||||||
|
implementation = (
|
||||||
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||||
|
hass, entry
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||||
|
api_api = api.AsyncConfigEntryAuth(
|
||||||
|
aiohttp_client.async_get_clientsession(hass),
|
||||||
|
session,
|
||||||
|
)
|
||||||
|
automower_api = AutomowerSession(api_api)
|
||||||
|
try:
|
||||||
|
await api_api.async_get_access_token()
|
||||||
|
except ClientError as err:
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
coordinator = AutomowerDataUpdateCoordinator(hass, automower_api)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||||
|
entry.async_on_unload(
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown)
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Handle unload of an entry."""
|
||||||
|
coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
await coordinator.shutdown()
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
return unload_ok
|
29
homeassistant/components/husqvarna_automower/api.py
Normal file
29
homeassistant/components/husqvarna_automower/api.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""API for Husqvarna Automower bound to Home Assistant OAuth."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioautomower.auth import AbstractAuth
|
||||||
|
from aioautomower.const import API_BASE_URL
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncConfigEntryAuth(AbstractAuth):
|
||||||
|
"""Provide Husqvarna Automower authentication tied to an OAuth2 based config entry."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
websession: ClientSession,
|
||||||
|
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Husqvarna Automower auth."""
|
||||||
|
super().__init__(websession, API_BASE_URL)
|
||||||
|
self._oauth_session = oauth_session
|
||||||
|
|
||||||
|
async def async_get_access_token(self) -> str:
|
||||||
|
"""Return a valid access token."""
|
||||||
|
await self._oauth_session.async_ensure_token_valid()
|
||||||
|
return self._oauth_session.token["access_token"]
|
@ -0,0 +1,14 @@
|
|||||||
|
"""Application credentials platform for Husqvarna Automower."""
|
||||||
|
|
||||||
|
from homeassistant.components.application_credentials import AuthorizationServer
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||||
|
"""Return authorization server."""
|
||||||
|
return AuthorizationServer(
|
||||||
|
authorize_url=OAUTH2_AUTHORIZE,
|
||||||
|
token_url=OAUTH2_TOKEN,
|
||||||
|
)
|
43
homeassistant/components/husqvarna_automower/config_flow.py
Normal file
43
homeassistant/components/husqvarna_automower/config_flow.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"""Config flow to add the integration via the UI."""
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aioautomower.utils import async_structure_token
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from .const import DOMAIN, NAME
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
CONF_USER_ID = "user_id"
|
||||||
|
|
||||||
|
|
||||||
|
class HusqvarnaConfigFlowHandler(
|
||||||
|
config_entry_oauth2_flow.AbstractOAuth2FlowHandler,
|
||||||
|
domain=DOMAIN,
|
||||||
|
):
|
||||||
|
"""Handle a config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
DOMAIN = DOMAIN
|
||||||
|
|
||||||
|
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
||||||
|
"""Create an entry for the flow."""
|
||||||
|
token = data[CONF_TOKEN]
|
||||||
|
user_id = token[CONF_USER_ID]
|
||||||
|
structured_token = await async_structure_token(token[CONF_ACCESS_TOKEN])
|
||||||
|
first_name = structured_token.user.first_name
|
||||||
|
last_name = structured_token.user.last_name
|
||||||
|
await self.async_set_unique_id(user_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"{NAME} of {first_name} {last_name}",
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logger(self) -> logging.Logger:
|
||||||
|
"""Return logger."""
|
||||||
|
return logging.getLogger(__name__)
|
7
homeassistant/components/husqvarna_automower/const.py
Normal file
7
homeassistant/components/husqvarna_automower/const.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""The constants for the Husqvarna Automower integration."""
|
||||||
|
|
||||||
|
DOMAIN = "husqvarna_automower"
|
||||||
|
NAME = "Husqvarna Automower"
|
||||||
|
HUSQVARNA_URL = "https://developer.husqvarnagroup.cloud/login"
|
||||||
|
OAUTH2_AUTHORIZE = "https://api.authentication.husqvarnagroup.dev/v1/oauth2/authorize"
|
||||||
|
OAUTH2_TOKEN = "https://api.authentication.husqvarnagroup.dev/v1/oauth2/token"
|
47
homeassistant/components/husqvarna_automower/coordinator.py
Normal file
47
homeassistant/components/husqvarna_automower/coordinator.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Data UpdateCoordinator for the Husqvarna Automower integration."""
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aioautomower.model import MowerAttributes, MowerList
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .api import AsyncConfigEntryAuth
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]):
|
||||||
|
"""Class to manage fetching Husqvarna data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, api: AsyncConfigEntryAuth) -> None:
|
||||||
|
"""Initialize data updater."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(minutes=5),
|
||||||
|
)
|
||||||
|
self.api = api
|
||||||
|
|
||||||
|
self.ws_connected: bool = False
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, MowerAttributes]:
|
||||||
|
"""Subscribe for websocket and poll data from the API."""
|
||||||
|
if not self.ws_connected:
|
||||||
|
await self.api.connect()
|
||||||
|
self.api.register_data_callback(self.callback)
|
||||||
|
self.ws_connected = True
|
||||||
|
return await self.api.get_status()
|
||||||
|
|
||||||
|
async def shutdown(self, *_: Any) -> None:
|
||||||
|
"""Close resources."""
|
||||||
|
await self.api.close()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def callback(self, ws_data: MowerList) -> None:
|
||||||
|
"""Process websocket callbacks and write them to the DataUpdateCoordinator."""
|
||||||
|
self.async_set_updated_data(ws_data)
|
41
homeassistant/components/husqvarna_automower/entity.py
Normal file
41
homeassistant/components/husqvarna_automower/entity.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""Platform for Husqvarna Automower base entity."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioautomower.model import MowerAttributes
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from . import AutomowerDataUpdateCoordinator
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]):
|
||||||
|
"""Defining the Automower base Entity."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mower_id: str,
|
||||||
|
coordinator: AutomowerDataUpdateCoordinator,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize AutomowerEntity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.mower_id = mower_id
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, mower_id)},
|
||||||
|
name=self.mower_attributes.system.name,
|
||||||
|
manufacturer="Husqvarna",
|
||||||
|
model=self.mower_attributes.system.model,
|
||||||
|
suggested_area="Garden",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mower_attributes(self) -> MowerAttributes:
|
||||||
|
"""Get the mower attributes of the current mower."""
|
||||||
|
return self.coordinator.data[self.mower_id]
|
126
homeassistant/components/husqvarna_automower/lawn_mower.py
Normal file
126
homeassistant/components/husqvarna_automower/lawn_mower.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
"""Husqvarna Automower lawn mower entity."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioautomower.exceptions import ApiException
|
||||||
|
from aioautomower.model import MowerActivities, MowerStates
|
||||||
|
|
||||||
|
from homeassistant.components.lawn_mower import (
|
||||||
|
LawnMowerActivity,
|
||||||
|
LawnMowerEntity,
|
||||||
|
LawnMowerEntityFeature,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AutomowerDataUpdateCoordinator
|
||||||
|
from .entity import AutomowerBaseEntity
|
||||||
|
|
||||||
|
SUPPORT_STATE_SERVICES = (
|
||||||
|
LawnMowerEntityFeature.DOCK
|
||||||
|
| LawnMowerEntityFeature.PAUSE
|
||||||
|
| LawnMowerEntityFeature.START_MOWING
|
||||||
|
)
|
||||||
|
|
||||||
|
DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING)
|
||||||
|
ERROR_ACTIVITIES = (
|
||||||
|
MowerActivities.STOPPED_IN_GARDEN,
|
||||||
|
MowerActivities.UNKNOWN,
|
||||||
|
MowerActivities.NOT_APPLICABLE,
|
||||||
|
)
|
||||||
|
ERROR_STATES = [
|
||||||
|
MowerStates.FATAL_ERROR,
|
||||||
|
MowerStates.ERROR,
|
||||||
|
MowerStates.ERROR_AT_POWER_UP,
|
||||||
|
MowerStates.NOT_APPLICABLE,
|
||||||
|
MowerStates.UNKNOWN,
|
||||||
|
MowerStates.STOPPED,
|
||||||
|
MowerStates.OFF,
|
||||||
|
]
|
||||||
|
MOWING_ACTIVITIES = (
|
||||||
|
MowerActivities.MOWING,
|
||||||
|
MowerActivities.LEAVING,
|
||||||
|
MowerActivities.GOING_HOME,
|
||||||
|
)
|
||||||
|
PAUSED_STATES = [
|
||||||
|
MowerStates.PAUSED,
|
||||||
|
MowerStates.WAIT_UPDATING,
|
||||||
|
MowerStates.WAIT_POWER_UP,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up lawn mower platform."""
|
||||||
|
coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
async_add_entities(
|
||||||
|
AutomowerLawnMowerEntity(mower_id, coordinator) for mower_id in coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AutomowerLawnMowerEntity(LawnMowerEntity, AutomowerBaseEntity):
|
||||||
|
"""Defining each mower Entity."""
|
||||||
|
|
||||||
|
_attr_name = None
|
||||||
|
_attr_supported_features = SUPPORT_STATE_SERVICES
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mower_id: str,
|
||||||
|
coordinator: AutomowerDataUpdateCoordinator,
|
||||||
|
) -> None:
|
||||||
|
"""Set up HusqvarnaAutomowerEntity."""
|
||||||
|
super().__init__(mower_id, coordinator)
|
||||||
|
self._attr_unique_id = mower_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if the device is available."""
|
||||||
|
return super().available and self.mower_attributes.metadata.connected
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity(self) -> LawnMowerActivity:
|
||||||
|
"""Return the state of the mower."""
|
||||||
|
mower_attributes = self.mower_attributes
|
||||||
|
if mower_attributes.mower.state in PAUSED_STATES:
|
||||||
|
return LawnMowerActivity.PAUSED
|
||||||
|
if mower_attributes.mower.activity in MOWING_ACTIVITIES:
|
||||||
|
return LawnMowerActivity.MOWING
|
||||||
|
if (mower_attributes.mower.state == "RESTRICTED") or (
|
||||||
|
mower_attributes.mower.activity in DOCKED_ACTIVITIES
|
||||||
|
):
|
||||||
|
return LawnMowerActivity.DOCKED
|
||||||
|
return LawnMowerActivity.ERROR
|
||||||
|
|
||||||
|
async def async_start_mowing(self) -> None:
|
||||||
|
"""Resume schedule."""
|
||||||
|
try:
|
||||||
|
await self.coordinator.api.resume_schedule(self.mower_id)
|
||||||
|
except ApiException as exception:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"Command couldn't be sent to the command queue: {exception}"
|
||||||
|
) from exception
|
||||||
|
|
||||||
|
async def async_pause(self) -> None:
|
||||||
|
"""Pauses the mower."""
|
||||||
|
try:
|
||||||
|
await self.coordinator.api.pause_mowing(self.mower_id)
|
||||||
|
except ApiException as exception:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"Command couldn't be sent to the command queue: {exception}"
|
||||||
|
) from exception
|
||||||
|
|
||||||
|
async def async_dock(self) -> None:
|
||||||
|
"""Parks the mower until next schedule."""
|
||||||
|
try:
|
||||||
|
await self.coordinator.api.park_until_next_schedule(self.mower_id)
|
||||||
|
except ApiException as exception:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"Command couldn't be sent to the command queue: {exception}"
|
||||||
|
) from exception
|
10
homeassistant/components/husqvarna_automower/manifest.json
Normal file
10
homeassistant/components/husqvarna_automower/manifest.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"domain": "husqvarna_automower",
|
||||||
|
"name": "Husqvarna Automower",
|
||||||
|
"codeowners": ["@Thomas55555"],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": ["application_credentials"],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
|
||||||
|
"iot_class": "cloud_push",
|
||||||
|
"requirements": ["aioautomower==2024.1.5"]
|
||||||
|
}
|
21
homeassistant/components/husqvarna_automower/strings.json
Normal file
21
homeassistant/components/husqvarna_automower/strings.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"pick_implementation": {
|
||||||
|
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
|
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||||
|
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||||
|
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||||
|
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||||
|
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
|
||||||
|
},
|
||||||
|
"create_entry": {
|
||||||
|
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ APPLICATION_CREDENTIALS = [
|
|||||||
"google_sheets",
|
"google_sheets",
|
||||||
"google_tasks",
|
"google_tasks",
|
||||||
"home_connect",
|
"home_connect",
|
||||||
|
"husqvarna_automower",
|
||||||
"lametric",
|
"lametric",
|
||||||
"lyric",
|
"lyric",
|
||||||
"myuplink",
|
"myuplink",
|
||||||
|
@ -228,6 +228,7 @@ FLOWS = {
|
|||||||
"hue",
|
"hue",
|
||||||
"huisbaasje",
|
"huisbaasje",
|
||||||
"hunterdouglas_powerview",
|
"hunterdouglas_powerview",
|
||||||
|
"husqvarna_automower",
|
||||||
"huum",
|
"huum",
|
||||||
"hvv_departures",
|
"hvv_departures",
|
||||||
"hydrawise",
|
"hydrawise",
|
||||||
|
@ -2618,6 +2618,12 @@
|
|||||||
"integration_type": "virtual",
|
"integration_type": "virtual",
|
||||||
"supported_by": "motion_blinds"
|
"supported_by": "motion_blinds"
|
||||||
},
|
},
|
||||||
|
"husqvarna_automower": {
|
||||||
|
"name": "Husqvarna Automower",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_push"
|
||||||
|
},
|
||||||
"huum": {
|
"huum": {
|
||||||
"name": "Huum",
|
"name": "Huum",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
@ -205,6 +205,9 @@ aioaseko==0.0.2
|
|||||||
# homeassistant.components.asuswrt
|
# homeassistant.components.asuswrt
|
||||||
aioasuswrt==1.4.0
|
aioasuswrt==1.4.0
|
||||||
|
|
||||||
|
# homeassistant.components.husqvarna_automower
|
||||||
|
aioautomower==2024.1.5
|
||||||
|
|
||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==1.3.5
|
aioazuredevops==1.3.5
|
||||||
|
|
||||||
|
@ -184,6 +184,9 @@ aioaseko==0.0.2
|
|||||||
# homeassistant.components.asuswrt
|
# homeassistant.components.asuswrt
|
||||||
aioasuswrt==1.4.0
|
aioasuswrt==1.4.0
|
||||||
|
|
||||||
|
# homeassistant.components.husqvarna_automower
|
||||||
|
aioautomower==2024.1.5
|
||||||
|
|
||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==1.3.5
|
aioazuredevops==1.3.5
|
||||||
|
|
||||||
|
11
tests/components/husqvarna_automower/__init__.py
Normal file
11
tests/components/husqvarna_automower/__init__.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"""Tests for the Husqvarna Automower integration."""
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||||
|
"""Fixture for setting up the component."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
85
tests/components/husqvarna_automower/conftest.py
Normal file
85
tests/components/husqvarna_automower/conftest.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"""Test helpers for Husqvarna Automower."""
|
||||||
|
from collections.abc import Generator
|
||||||
|
import time
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from aioautomower.utils import mower_list_to_dictionary_dataclass
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.application_credentials import (
|
||||||
|
ClientCredential,
|
||||||
|
async_import_client_credential,
|
||||||
|
)
|
||||||
|
from homeassistant.components.husqvarna_automower.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .const import CLIENT_ID, CLIENT_SECRET, USER_ID
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture, load_json_value_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="jwt")
|
||||||
|
def load_jwt_fixture():
|
||||||
|
"""Load Fixture data."""
|
||||||
|
return load_fixture("jwt", DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="expires_at")
|
||||||
|
def mock_expires_at() -> float:
|
||||||
|
"""Fixture to set the oauth token expiration time."""
|
||||||
|
return time.time() + 3600
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry(jwt, expires_at: int) -> MockConfigEntry:
|
||||||
|
"""Return the default mocked config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
version=1,
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="Husqvarna Automower of Erika Mustermann",
|
||||||
|
data={
|
||||||
|
"auth_implementation": DOMAIN,
|
||||||
|
"token": {
|
||||||
|
"access_token": jwt,
|
||||||
|
"scope": "iam:read amc:api",
|
||||||
|
"expires_in": 86399,
|
||||||
|
"refresh_token": "3012bc9f-7a65-4240-b817-9154ffdcc30f",
|
||||||
|
"provider": "husqvarna",
|
||||||
|
"user_id": USER_ID,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_at": expires_at,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unique_id=USER_ID,
|
||||||
|
entry_id="automower_test",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def setup_credentials(hass: HomeAssistant) -> None:
|
||||||
|
"""Fixture to setup credentials."""
|
||||||
|
assert await async_setup_component(hass, "application_credentials", {})
|
||||||
|
await async_import_client_credential(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
ClientCredential(
|
||||||
|
CLIENT_ID,
|
||||||
|
CLIENT_SECRET,
|
||||||
|
),
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_automower_client() -> Generator[AsyncMock, None, None]:
|
||||||
|
"""Mock a Husqvarna Automower client."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.husqvarna_automower.AutomowerSession",
|
||||||
|
autospec=True,
|
||||||
|
) as mock_client:
|
||||||
|
client = mock_client.return_value
|
||||||
|
client.get_status.return_value = mower_list_to_dictionary_dataclass(
|
||||||
|
load_json_value_fixture("mower.json", DOMAIN)
|
||||||
|
)
|
||||||
|
yield client
|
4
tests/components/husqvarna_automower/const.py
Normal file
4
tests/components/husqvarna_automower/const.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""Constants for Husqvarna Automower tests."""
|
||||||
|
CLIENT_ID = "1234"
|
||||||
|
CLIENT_SECRET = "5678"
|
||||||
|
USER_ID = "123"
|
1
tests/components/husqvarna_automower/fixtures/jwt
Normal file
1
tests/components/husqvarna_automower/fixtures/jwt
Normal file
@ -0,0 +1 @@
|
|||||||
|
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVlZDU2ZDUzLTEyNWYtNDExZi04ZTFlLTNlNDRkMGVkOGJmOCJ9.eyJqdGkiOiI2MGYxNGQ1OS1iY2M4LTQwMzktYmMzOC0yNWRiMzc2MGQwNDciLCJpc3MiOiJodXNxdmFybmEiLCJyb2xlcyI6W10sImdyb3VwcyI6WyJhbWMiLCJkZXZlbG9wZXItcG9ydGFsIiwiZmQ3OGIzYTQtYTdmOS00Yzc2LWJlZjktYWE1YTUwNTgzMzgyIiwiZ2FyZGVuYS1teWFjY291bnQiLCJodXNxdmFybmEtY29ubmVjdCIsImh1c3F2YXJuYS1teXBhZ2VzIiwic21hcnRnYXJkZW4iXSwic2NvcGVzIjpbImlhbTpyZWFkIiwiYW1jOmFwaSJdLCJzY29wZSI6ImlhbTpyZWFkIGFtYzphcGkiLCJjbGllbnRfaWQiOiI0MzNlNWZkZi01MTI5LTQ1MmMteHh4eC1mYWRjZTMyMTMwNDIiLCJjdXN0b21lcl9pZCI6IjQ3NTU5OTc3MjA0NTh4eHh4IiwidXNlciI6eyJmaXJzdF9uYW1lIjoiSm9obiIsImxhc3RfbmFtZSI6IkRvZSIsImN1c3RvbV9hdHRyaWJ1dGVzIjp7ImhjX2NvdW50cnkiOiJERSJ9LCJjdXN0b21lcl9pZCI6IjQ3NTU5OTc3MjA0NTh4eHh4In0sImlhdCI6MTY5NzY2Njk0NywiZXhwIjoxNjk3NzUzMzQ3LCJzdWIiOiI1YTkzMTQxZS01NWE3LTQ3OWYtOTZlMi04YTYzMTg4YzA1NGYifQ.1O3FOoWHaWpo-PrW88097ai6nsUGlK2NWyqIDLkUl1BTatQoFhIA1nKmCthf6A9CAYeoPS4c8CBhqqLj-5VrJXfNc7pFZ1nAw69pT33Ku7_S9QqonPf_JRvWX8-A7sTCKXEkCTso6v_jbmiePK6C9_psClJx_PUgFFOoNaROZhSsAlq9Gftvzs9UTcd2UO9ohsku_Kpx480C0QCKRjm4LTrFTBpgijRPc3F0BnyfgW8rT3Trl290f3CyEzLk8k9bgGA0qDlAanKuNNKK1j7hwRsiq_28A7bWJzlLc6Wgrq8Pc2CnnMada_eXavkTu-VzB-q8_PGFkLyeG16CR-NXlox9mEB6NxTn5stYSMUkiTApAfgCwLuj4c_WCXnxUZn0VdnsswvaIZON3bTSOMATXLG8PFUyDOcDxHBV4LEDyTVspo-QblanTTBLFWMTfWIWApBmRO9OkiJrcq9g7T8hKNNImeN4skk2vIZVXkCq_cEOdVAG4099b1V8zXCBgtDc
|
139
tests/components/husqvarna_automower/fixtures/mower.json
Normal file
139
tests/components/husqvarna_automower/fixtures/mower.json
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"type": "mower",
|
||||||
|
"id": "c7233734-b219-4287-a173-08e3643f89f0",
|
||||||
|
"attributes": {
|
||||||
|
"system": {
|
||||||
|
"name": "Test Mower 1",
|
||||||
|
"model": "450XH-TEST",
|
||||||
|
"serialNumber": 123
|
||||||
|
},
|
||||||
|
"battery": {
|
||||||
|
"batteryPercent": 100
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"headlights": true,
|
||||||
|
"workAreas": false,
|
||||||
|
"position": true,
|
||||||
|
"stayOutZones": false
|
||||||
|
},
|
||||||
|
"mower": {
|
||||||
|
"mode": "MAIN_AREA",
|
||||||
|
"activity": "PARKED_IN_CS",
|
||||||
|
"state": "RESTRICTED",
|
||||||
|
"errorCode": 0,
|
||||||
|
"errorCodeTimestamp": 0
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"start": 1140,
|
||||||
|
"duration": 300,
|
||||||
|
"monday": true,
|
||||||
|
"tuesday": false,
|
||||||
|
"wednesday": true,
|
||||||
|
"thursday": false,
|
||||||
|
"friday": true,
|
||||||
|
"saturday": false,
|
||||||
|
"sunday": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": 0,
|
||||||
|
"duration": 480,
|
||||||
|
"monday": false,
|
||||||
|
"tuesday": true,
|
||||||
|
"wednesday": false,
|
||||||
|
"thursday": true,
|
||||||
|
"friday": false,
|
||||||
|
"saturday": true,
|
||||||
|
"sunday": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"planner": {
|
||||||
|
"nextStartTimestamp": 1685991600000,
|
||||||
|
"override": {
|
||||||
|
"action": "NOT_ACTIVE"
|
||||||
|
},
|
||||||
|
"restrictedReason": "WEEK_SCHEDULE"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"connected": true,
|
||||||
|
"statusTimestamp": 1697669932683
|
||||||
|
},
|
||||||
|
"positions": [
|
||||||
|
{
|
||||||
|
"latitude": 35.5402913,
|
||||||
|
"longitude": -82.5527055
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5407693,
|
||||||
|
"longitude": -82.5521503
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5403241,
|
||||||
|
"longitude": -82.5522924
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5406973,
|
||||||
|
"longitude": -82.5518579
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5404659,
|
||||||
|
"longitude": -82.5516567
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5406318,
|
||||||
|
"longitude": -82.5515709
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5402477,
|
||||||
|
"longitude": -82.5519437
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5403503,
|
||||||
|
"longitude": -82.5516889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5401429,
|
||||||
|
"longitude": -82.551536
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5405489,
|
||||||
|
"longitude": -82.5512195
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5404005,
|
||||||
|
"longitude": -82.5512115
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5405969,
|
||||||
|
"longitude": -82.551418
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5403437,
|
||||||
|
"longitude": -82.5523917
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"latitude": 35.5403481,
|
||||||
|
"longitude": -82.5520054
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cuttingHeight": 4,
|
||||||
|
"headlight": {
|
||||||
|
"mode": "EVENING_ONLY"
|
||||||
|
},
|
||||||
|
"statistics": {
|
||||||
|
"numberOfChargingCycles": 1380,
|
||||||
|
"numberOfCollisions": 11396,
|
||||||
|
"totalChargingTime": 4334400,
|
||||||
|
"totalCuttingTime": 4194000,
|
||||||
|
"totalDriveDistance": 1780272,
|
||||||
|
"totalRunningTime": 4564800,
|
||||||
|
"totalSearchingTime": 370800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
129
tests/components/husqvarna_automower/test_config_flow.py
Normal file
129
tests/components/husqvarna_automower/test_config_flow.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
"""Test the Husqvarna Automower config flow."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.husqvarna_automower.const import (
|
||||||
|
DOMAIN,
|
||||||
|
OAUTH2_AUTHORIZE,
|
||||||
|
OAUTH2_TOKEN,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
from .const import CLIENT_ID, USER_ID
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
current_request_with_host,
|
||||||
|
jwt,
|
||||||
|
) -> None:
|
||||||
|
"""Check full flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
"husqvarna_automower", context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"redirect_uri": "https://example.com/auth/external/callback",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["url"] == (
|
||||||
|
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||||
|
"&redirect_uri=https://example.com/auth/external/callback"
|
||||||
|
f"&state={state}"
|
||||||
|
)
|
||||||
|
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
assert resp.status == 200
|
||||||
|
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
|
aioclient_mock.post(
|
||||||
|
OAUTH2_TOKEN,
|
||||||
|
json={
|
||||||
|
"access_token": jwt,
|
||||||
|
"scope": "iam:read amc:api",
|
||||||
|
"expires_in": 86399,
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"provider": "husqvarna",
|
||||||
|
"user_id": "mock-user-id",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_at": 1697753347,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.husqvarna_automower.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup:
|
||||||
|
await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_non_unique_profile(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
current_request_with_host: None,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
mock_automower_client: AsyncMock,
|
||||||
|
jwt,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup a non-unique profile."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"redirect_uri": "https://example.com/auth/external/callback",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.EXTERNAL_STEP
|
||||||
|
assert result["url"] == (
|
||||||
|
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||||
|
"&redirect_uri=https://example.com/auth/external/callback"
|
||||||
|
f"&state={state}"
|
||||||
|
)
|
||||||
|
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
assert resp.status == 200
|
||||||
|
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
|
aioclient_mock.post(
|
||||||
|
OAUTH2_TOKEN,
|
||||||
|
json={
|
||||||
|
"access_token": jwt,
|
||||||
|
"scope": "iam:read amc:api",
|
||||||
|
"expires_in": 86399,
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"provider": "husqvarna",
|
||||||
|
"user_id": USER_ID,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_at": 1697753347,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
68
tests/components/husqvarna_automower/test_init.py
Normal file
68
tests/components/husqvarna_automower/test_init.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"""Tests for init module."""
|
||||||
|
import http
|
||||||
|
import time
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.husqvarna_automower.const import DOMAIN, OAUTH2_TOKEN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_unload_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_automower_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test load and unload entry."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
|
|
||||||
|
assert entry.state == ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state == ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("expires_at", "status", "expected_state"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
time.time() - 3600,
|
||||||
|
http.HTTPStatus.UNAUTHORIZED,
|
||||||
|
ConfigEntryState.SETUP_RETRY, # Will trigger reauth in the future
|
||||||
|
),
|
||||||
|
(
|
||||||
|
time.time() - 3600,
|
||||||
|
http.HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
ConfigEntryState.SETUP_RETRY,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=["unauthorized", "internal_server_error"],
|
||||||
|
)
|
||||||
|
async def test_expired_token_refresh_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
status: http.HTTPStatus,
|
||||||
|
expected_state: ConfigEntryState,
|
||||||
|
) -> None:
|
||||||
|
"""Test failure while refreshing token with a transient error."""
|
||||||
|
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
|
aioclient_mock.post(
|
||||||
|
OAUTH2_TOKEN,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert mock_config_entry.state is expected_state
|
88
tests/components/husqvarna_automower/test_lawn_mower.py
Normal file
88
tests/components/husqvarna_automower/test_lawn_mower.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"""Tests for lawn_mower module."""
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from aioautomower.exceptions import ApiException
|
||||||
|
from aioautomower.utils import mower_list_to_dictionary_dataclass
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.husqvarna_automower.const import DOMAIN
|
||||||
|
from homeassistant.components.lawn_mower import LawnMowerActivity
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
async_fire_time_changed,
|
||||||
|
load_json_value_fixture,
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_MOWER_ID = "c7233734-b219-4287-a173-08e3643f89f0"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_lawn_mower_states(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_automower_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test lawn_mower state."""
|
||||||
|
values = mower_list_to_dictionary_dataclass(
|
||||||
|
load_json_value_fixture("mower.json", DOMAIN)
|
||||||
|
)
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
state = hass.states.get("lawn_mower.test_mower_1")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == LawnMowerActivity.DOCKED
|
||||||
|
|
||||||
|
for activity, state, expected_state in [
|
||||||
|
("UNKNOWN", "PAUSED", LawnMowerActivity.PAUSED),
|
||||||
|
("MOWING", "NOT_APPLICABLE", LawnMowerActivity.MOWING),
|
||||||
|
("NOT_APPLICABLE", "ERROR", LawnMowerActivity.ERROR),
|
||||||
|
]:
|
||||||
|
values[TEST_MOWER_ID].mower.activity = activity
|
||||||
|
values[TEST_MOWER_ID].mower.state = state
|
||||||
|
mock_automower_client.get_status.return_value = values
|
||||||
|
freezer.tick(timedelta(minutes=5))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get("lawn_mower.test_mower_1")
|
||||||
|
assert state.state == expected_state
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("aioautomower_command", "service"),
|
||||||
|
[
|
||||||
|
("resume_schedule", "start_mowing"),
|
||||||
|
("pause_mowing", "pause"),
|
||||||
|
("park_until_next_schedule", "dock"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_lawn_mower_commands(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
aioautomower_command: str,
|
||||||
|
service: str,
|
||||||
|
mock_automower_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test lawn_mower commands."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
getattr(mock_automower_client, aioautomower_command).side_effect = ApiException(
|
||||||
|
"Test error"
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError) as exc_info:
|
||||||
|
await hass.services.async_call(
|
||||||
|
domain="lawn_mower",
|
||||||
|
service=service,
|
||||||
|
service_data={"entity_id": "lawn_mower.test_mower_1"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
str(exc_info.value)
|
||||||
|
== "Command couldn't be sent to the command queue: Test error"
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user