New integration Amazon Devices (#144422)

* New integration Amazon Devices

* apply review comments

* bump aioamazondevices

* Add notify platform

* pylance

* full coverage for coordinator tests

* cleanup imports

* Add switch platform

* update quality scale: docs items

* update quality scale: brands

* apply review comments

* fix new ruff rule

* simplify EntityDescription code

* remove additional platforms for first PR

* apply review comments

* update IQS

* apply last review comments

* snapshot update

* apply review comments

* apply review comments
This commit is contained in:
Simone Chemelli
2025-05-25 18:42:07 +03:00
committed by GitHub
parent 46951bf223
commit d0b2331a5f
26 changed files with 918 additions and 0 deletions

View File

@ -66,6 +66,7 @@ homeassistant.components.alarm_control_panel.*
homeassistant.components.alert.*
homeassistant.components.alexa.*
homeassistant.components.alpha_vantage.*
homeassistant.components.amazon_devices.*
homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.*
homeassistant.components.ambient_network.*

2
CODEOWNERS generated
View File

@ -89,6 +89,8 @@ build.json @home-assistant/supervisor
/tests/components/alert/ @home-assistant/core @frenck
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/homeassistant/components/amazon_devices/ @chemelli74
/tests/components/amazon_devices/ @chemelli74
/homeassistant/components/amazon_polly/ @jschlyter
/homeassistant/components/amberelectric/ @madpilot
/tests/components/amberelectric/ @madpilot

View File

@ -3,6 +3,7 @@
"name": "Amazon",
"integrations": [
"alexa",
"amazon_devices",
"amazon_polly",
"aws",
"aws_s3",

View File

@ -0,0 +1,28 @@
"""Amazon Devices integration."""
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
PLATFORMS = [Platform.BINARY_SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Set up Amazon Devices platform."""
coordinator = AmazonDevicesCoordinator(hass, entry)
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: AmazonConfigEntry) -> bool:
"""Unload a config entry."""
await entry.runtime_data.api.close()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,72 @@
"""Support for binary sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Final
from aioamazondevices.api import AmazonDevice
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Amazon Devices binary sensor entity description."""
is_on_fn: Callable[[AmazonDevice], bool]
BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription(
key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on_fn=lambda _device: _device.online,
),
AmazonBinarySensorEntityDescription(
key="bluetooth",
translation_key="bluetooth",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on_fn=lambda _device: _device.bluetooth_state,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Amazon Devices binary sensors based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in BINARY_SENSORS
for serial_num in coordinator.data
)
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
"""Binary sensor device."""
entity_description: AmazonBinarySensorEntityDescription
@property
def is_on(self) -> bool:
"""Return True if the binary sensor is on."""
return self.entity_description.is_on_fn(self.device)

View File

@ -0,0 +1,63 @@
"""Config flow for Amazon Devices integration."""
from __future__ import annotations
from typing import Any
from aioamazondevices.api import AmazonEchoApi
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import CountrySelector
from .const import CONF_LOGIN_DATA, DOMAIN
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Amazon Devices."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors = {}
if user_input:
client = AmazonEchoApi(
user_input[CONF_COUNTRY],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
try:
data = await client.login_mode_interactive(user_input[CONF_CODE])
except CannotConnect:
errors["base"] = "cannot_connect"
except CannotAuthenticate:
errors["base"] = "invalid_auth"
else:
await self.async_set_unique_id(data["customer_info"]["user_id"])
self._abort_if_unique_id_configured()
user_input.pop(CONF_CODE)
return self.async_create_entry(
title=user_input[CONF_USERNAME],
data=user_input | {CONF_LOGIN_DATA: data},
)
finally:
await client.close()
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(
CONF_COUNTRY, default=self.hass.config.country
): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.positive_int,
}
),
)

View File

@ -0,0 +1,8 @@
"""Amazon Devices constants."""
import logging
_LOGGER = logging.getLogger(__package__)
DOMAIN = "amazon_devices"
CONF_LOGIN_DATA = "login_data"

View File

@ -0,0 +1,58 @@
"""Support for Amazon Devices."""
from datetime import timedelta
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
from aioamazondevices.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, CONF_LOGIN_DATA
SCAN_INTERVAL = 30
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
"""Base coordinator for Amazon Devices."""
config_entry: AmazonConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: AmazonConfigEntry,
) -> None:
"""Initialize the scanner."""
super().__init__(
hass,
_LOGGER,
name=entry.title,
config_entry=entry,
update_interval=timedelta(seconds=SCAN_INTERVAL),
)
self.api = AmazonEchoApi(
entry.data[CONF_COUNTRY],
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
entry.data[CONF_LOGIN_DATA],
)
async def _async_update_data(self) -> dict[str, AmazonDevice]:
"""Update device data."""
try:
await self.api.login_mode_stored_data()
return await self.api.get_devices_data()
except (CannotConnect, CannotRetrieveData) as err:
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
except CannotAuthenticate as err:
raise ConfigEntryError("Could not authenticate") from err

View File

@ -0,0 +1,57 @@
"""Defines a base Amazon Devices entity."""
from typing import cast
from aioamazondevices.api import AmazonDevice
from aioamazondevices.const import DEVICE_TYPE_TO_MODEL, SPEAKER_GROUP_MODEL
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AmazonDevicesCoordinator
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Defines a base Amazon Devices entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AmazonDevicesCoordinator,
serial_num: str,
description: EntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model_details: dict[str, str] = cast(
"dict", DEVICE_TYPE_TO_MODEL.get(self.device.device_type)
)
model = model_details["model"] if model_details else None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=model,
model_id=self.device.device_type,
manufacturer="Amazon",
hw_version=model_details["hw_version"] if model_details else None,
sw_version=(
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
),
serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None,
)
self.entity_description = description
self._attr_unique_id = f"{serial_num}-{description.key}"
@property
def device(self) -> AmazonDevice:
"""Return the device."""
return self.coordinator.data[self._serial_num]
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._serial_num in self.coordinator.data

View File

@ -0,0 +1,12 @@
{
"entity": {
"binary_sensor": {
"bluetooth": {
"default": "mdi:bluetooth",
"state": {
"off": "mdi:bluetooth-off"
}
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"domain": "amazon_devices",
"name": "Amazon Devices",
"codeowners": ["@chemelli74"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==2.0.1"]
}

View File

@ -0,0 +1,72 @@
rules:
# Bronze
action-setup:
status: exempt
comment: no actions
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: no actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: entities do not explicitly subscribe to events
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage:
status: todo
comment: all tests missing
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
stale-devices:
status: todo
comment: automate the cleanup process
# Platinum
async-dependency: done
inject-websession: todo
strict-typing: done

View File

@ -0,0 +1,47 @@
{
"common": {
"data_country": "Country code",
"data_code": "One-time password (OTP code)",
"data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password sent to your email address."
},
"config": {
"flow_title": "{username}",
"step": {
"user": {
"data": {
"country": "[%key:component::amazon_devices::common::data_country%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::amazon_devices::common::data_description_code%]"
},
"data_description": {
"country": "[%key:component::amazon_devices::common::data_description_country%]",
"username": "[%key:component::amazon_devices::common::data_description_username%]",
"password": "[%key:component::amazon_devices::common::data_description_password%]",
"code": "[%key:component::amazon_devices::common::data_description_code%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"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%]"
},
"error": {
"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%]"
}
},
"entity": {
"binary_sensor": {
"bluetooth": {
"name": "Bluetooth"
}
}
}
}

View File

@ -47,6 +47,7 @@ FLOWS = {
"airzone",
"airzone_cloud",
"alarmdecoder",
"amazon_devices",
"amberelectric",
"ambient_network",
"ambient_station",

View File

@ -207,6 +207,12 @@
"amazon": {
"name": "Amazon",
"integrations": {
"amazon_devices": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling",
"name": "Amazon Devices"
},
"amazon_polly": {
"integration_type": "hub",
"config_flow": false,

10
mypy.ini generated
View File

@ -415,6 +415,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.amazon_devices.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.amazon_polly.*]
check_untyped_defs = true
disallow_incomplete_defs = true

3
requirements_all.txt generated
View File

@ -181,6 +181,9 @@ aioairzone-cloud==0.6.12
# homeassistant.components.airzone
aioairzone==1.0.0
# homeassistant.components.amazon_devices
aioamazondevices==2.0.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
aioambient==2024.08.0

View File

@ -169,6 +169,9 @@ aioairzone-cloud==0.6.12
# homeassistant.components.airzone
aioairzone==1.0.0
# homeassistant.components.amazon_devices
aioamazondevices==2.0.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
aioambient==2024.08.0

View File

@ -0,0 +1,13 @@
"""Tests for the Amazon Devices 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)
await hass.async_block_till_done()

View File

@ -0,0 +1,76 @@
"""Amazon Devices tests configuration."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from aioamazondevices.api import AmazonDevice
import pytest
from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from tests.common import MockConfigEntry
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.amazon_devices.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_amazon_devices_client() -> Generator[AsyncMock]:
"""Mock an Amazon Devices client."""
with (
patch(
"homeassistant.components.amazon_devices.coordinator.AmazonEchoApi",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.amazon_devices.config_flow.AmazonEchoApi",
new=mock_client,
),
):
client = mock_client.return_value
client.login_mode_interactive.return_value = {
"customer_info": {"user_id": TEST_USERNAME},
}
client.get_devices_data.return_value = {
TEST_SERIAL_NUMBER: AmazonDevice(
account_name="Echo Test",
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
device_family="mine",
device_type="echo",
device_owner_customer_id="amazon_ower_id",
device_cluster_members=[TEST_SERIAL_NUMBER],
online=True,
serial_number=TEST_SERIAL_NUMBER,
software_version="echo_test_software_version",
do_not_disturb=False,
response_style=None,
bluetooth_state=True,
)
}
yield client
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock a config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="Amazon Test Account",
data={
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_LOGIN_DATA: {"session": "test-session"},
},
unique_id=TEST_USERNAME,
)

View File

@ -0,0 +1,7 @@
"""Amazon Devices tests const."""
TEST_CODE = 123123
TEST_COUNTRY = "IT"
TEST_PASSWORD = "fake_password"
TEST_SERIAL_NUMBER = "echo_test_serial_number"
TEST_USERNAME = "fake_email@gmail.com"

View File

@ -0,0 +1,97 @@
# serializer version: 1
# name: test_all_entities[binary_sensor.echo_test_bluetooth-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.echo_test_bluetooth',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Bluetooth',
'platform': 'amazon_devices',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'bluetooth',
'unique_id': 'echo_test_serial_number-bluetooth',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.echo_test_bluetooth-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'Echo Test Bluetooth',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.echo_test_bluetooth',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[binary_sensor.echo_test_connectivity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.echo_test_connectivity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Connectivity',
'platform': 'amazon_devices',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'echo_test_serial_number-online',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.echo_test_connectivity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'Echo Test Connectivity',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.echo_test_connectivity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -0,0 +1,34 @@
# serializer version: 1
# name: test_device_info
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'amazon_devices',
'echo_test_serial_number',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Amazon',
'model': None,
'model_id': 'echo',
'name': 'Echo Test',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': 'echo_test_serial_number',
'suggested_area': None,
'sw_version': 'echo_test_software_version',
'via_device_id': None,
})
# ---

View File

@ -0,0 +1,71 @@
"""Tests for the Amazon Devices binary sensor platform."""
from unittest.mock import AsyncMock, patch
from aioamazondevices.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
)
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch(
"homeassistant.components.amazon_devices.PLATFORMS", [Platform.BINARY_SENSOR]
):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
"side_effect",
[
CannotConnect,
CannotRetrieveData,
CannotAuthenticate,
],
)
async def test_coordinator_data_update_fails(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
side_effect: Exception,
) -> None:
"""Test coordinator data update exceptions."""
entity_id = "binary_sensor.echo_test_connectivity"
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id))
assert state.state == STATE_ON
mock_amazon_devices_client.get_devices_data.side_effect = side_effect
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE

View File

@ -0,0 +1,134 @@
"""Tests for the Amazon Devices config flow."""
from unittest.mock import AsyncMock
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
import pytest
from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .const import TEST_CODE, TEST_COUNTRY, TEST_PASSWORD, TEST_USERNAME
from tests.common import MockConfigEntry
async def test_full_flow(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test full flow."""
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"],
{
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_CODE: TEST_CODE,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_USERNAME
assert result["data"] == {
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_LOGIN_DATA: {
"customer_info": {"user_id": TEST_USERNAME},
},
}
assert result["result"].unique_id == TEST_USERNAME
@pytest.mark.parametrize(
("exception", "error"),
[
(CannotConnect, "cannot_connect"),
(CannotAuthenticate, "invalid_auth"),
],
)
async def test_flow_errors(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_setup_entry: AsyncMock,
exception: Exception,
error: str,
) -> None:
"""Test flow errors."""
mock_amazon_devices_client.login_mode_interactive.side_effect = exception
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"],
{
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_CODE: TEST_CODE,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error}
mock_amazon_devices_client.login_mode_interactive.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_CODE: TEST_CODE,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_already_configured(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test duplicate flow."""
mock_config_entry.add_to_hass(hass)
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"],
{
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_CODE: TEST_CODE,
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -0,0 +1,30 @@
"""Tests for the Amazon Devices integration."""
from unittest.mock import AsyncMock
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.amazon_devices.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry
async def test_device_info(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test device registry integration."""
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
assert device_entry is not None
assert device_entry == snapshot