Add acaia integration (#130059)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
a949d18c30
commit
eea782bbfe
22 changed files with 1142 additions and 0 deletions
|
@ -40,6 +40,8 @@ build.json @home-assistant/supervisor
|
||||||
# Integrations
|
# Integrations
|
||||||
/homeassistant/components/abode/ @shred86
|
/homeassistant/components/abode/ @shred86
|
||||||
/tests/components/abode/ @shred86
|
/tests/components/abode/ @shred86
|
||||||
|
/homeassistant/components/acaia/ @zweckj
|
||||||
|
/tests/components/acaia/ @zweckj
|
||||||
/homeassistant/components/accuweather/ @bieniu
|
/homeassistant/components/accuweather/ @bieniu
|
||||||
/tests/components/accuweather/ @bieniu
|
/tests/components/accuweather/ @bieniu
|
||||||
/homeassistant/components/acmeda/ @atmurray
|
/homeassistant/components/acmeda/ @atmurray
|
||||||
|
|
29
homeassistant/components/acaia/__init__.py
Normal file
29
homeassistant/components/acaia/__init__.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
"""Initialize the Acaia component."""
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import AcaiaConfigEntry, AcaiaCoordinator
|
||||||
|
|
||||||
|
PLATFORMS = [
|
||||||
|
Platform.BUTTON,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool:
|
||||||
|
"""Set up acaia as config entry."""
|
||||||
|
|
||||||
|
coordinator = AcaiaCoordinator(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: AcaiaConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
61
homeassistant/components/acaia/button.py
Normal file
61
homeassistant/components/acaia/button.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
"""Button entities for Acaia scales."""
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aioacaia.acaiascale import AcaiaScale
|
||||||
|
|
||||||
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import AcaiaConfigEntry
|
||||||
|
from .entity import AcaiaEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class AcaiaButtonEntityDescription(ButtonEntityDescription):
|
||||||
|
"""Description for acaia button entities."""
|
||||||
|
|
||||||
|
press_fn: Callable[[AcaiaScale], Coroutine[Any, Any, None]]
|
||||||
|
|
||||||
|
|
||||||
|
BUTTONS: tuple[AcaiaButtonEntityDescription, ...] = (
|
||||||
|
AcaiaButtonEntityDescription(
|
||||||
|
key="tare",
|
||||||
|
translation_key="tare",
|
||||||
|
press_fn=lambda scale: scale.tare(),
|
||||||
|
),
|
||||||
|
AcaiaButtonEntityDescription(
|
||||||
|
key="reset_timer",
|
||||||
|
translation_key="reset_timer",
|
||||||
|
press_fn=lambda scale: scale.reset_timer(),
|
||||||
|
),
|
||||||
|
AcaiaButtonEntityDescription(
|
||||||
|
key="start_stop",
|
||||||
|
translation_key="start_stop",
|
||||||
|
press_fn=lambda scale: scale.start_stop_timer(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AcaiaConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up button entities and services."""
|
||||||
|
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
async_add_entities(AcaiaButton(coordinator, description) for description in BUTTONS)
|
||||||
|
|
||||||
|
|
||||||
|
class AcaiaButton(AcaiaEntity, ButtonEntity):
|
||||||
|
"""Representation of an Acaia button."""
|
||||||
|
|
||||||
|
entity_description: AcaiaButtonEntityDescription
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Handle the button press."""
|
||||||
|
await self.entity_description.press_fn(self._scale)
|
149
homeassistant/components/acaia/config_flow.py
Normal file
149
homeassistant/components/acaia/config_flow.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
"""Config flow for Acaia integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice
|
||||||
|
from aioacaia.helpers import is_new_scale
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.bluetooth import (
|
||||||
|
BluetoothServiceInfoBleak,
|
||||||
|
async_discovered_service_info,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
||||||
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
SelectOptionDict,
|
||||||
|
SelectSelector,
|
||||||
|
SelectSelectorConfig,
|
||||||
|
SelectSelectorMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import CONF_IS_NEW_STYLE_SCALE, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for acaia."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
self._discovered: dict[str, Any] = {}
|
||||||
|
self._discovered_devices: dict[str, str] = {}
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
mac = format_mac(user_input[CONF_ADDRESS])
|
||||||
|
try:
|
||||||
|
is_new_style_scale = await is_new_scale(mac)
|
||||||
|
except AcaiaDeviceNotFound:
|
||||||
|
errors["base"] = "device_not_found"
|
||||||
|
except AcaiaError:
|
||||||
|
_LOGGER.exception("Error occurred while connecting to the scale")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
except AcaiaUnknownDevice:
|
||||||
|
return self.async_abort(reason="unsupported_device")
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(mac)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self._discovered_devices[user_input[CONF_ADDRESS]],
|
||||||
|
data={
|
||||||
|
CONF_ADDRESS: mac,
|
||||||
|
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for device in async_discovered_service_info(self.hass):
|
||||||
|
self._discovered_devices[device.address] = device.name
|
||||||
|
|
||||||
|
if not self._discovered_devices:
|
||||||
|
return self.async_abort(reason="no_devices_found")
|
||||||
|
|
||||||
|
options = [
|
||||||
|
SelectOptionDict(
|
||||||
|
value=device_mac,
|
||||||
|
label=f"{device_name} ({device_mac})",
|
||||||
|
)
|
||||||
|
for device_mac, device_name in self._discovered_devices.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_ADDRESS): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=options,
|
||||||
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_bluetooth(
|
||||||
|
self, discovery_info: BluetoothServiceInfoBleak
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a discovered Bluetooth device."""
|
||||||
|
|
||||||
|
self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address)
|
||||||
|
self._discovered[CONF_NAME] = discovery_info.name
|
||||||
|
|
||||||
|
await self.async_set_unique_id(mac)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._discovered[CONF_IS_NEW_STYLE_SCALE] = await is_new_scale(
|
||||||
|
discovery_info.address
|
||||||
|
)
|
||||||
|
except AcaiaDeviceNotFound:
|
||||||
|
_LOGGER.debug("Device not found during discovery")
|
||||||
|
return self.async_abort(reason="device_not_found")
|
||||||
|
except AcaiaError:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Error occurred while connecting to the scale during discovery",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return self.async_abort(reason="unknown")
|
||||||
|
except AcaiaUnknownDevice:
|
||||||
|
_LOGGER.debug("Unsupported device during discovery")
|
||||||
|
return self.async_abort(reason="unsupported_device")
|
||||||
|
|
||||||
|
return await self.async_step_bluetooth_confirm()
|
||||||
|
|
||||||
|
async def async_step_bluetooth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle confirmation of Bluetooth discovery."""
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self._discovered[CONF_NAME],
|
||||||
|
data={
|
||||||
|
CONF_ADDRESS: self._discovered[CONF_ADDRESS],
|
||||||
|
CONF_IS_NEW_STYLE_SCALE: self._discovered[CONF_IS_NEW_STYLE_SCALE],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.context["title_placeholders"] = placeholders = {
|
||||||
|
CONF_NAME: self._discovered[CONF_NAME]
|
||||||
|
}
|
||||||
|
|
||||||
|
self._set_confirm_only()
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="bluetooth_confirm",
|
||||||
|
description_placeholders=placeholders,
|
||||||
|
)
|
4
homeassistant/components/acaia/const.py
Normal file
4
homeassistant/components/acaia/const.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"""Constants for component."""
|
||||||
|
|
||||||
|
DOMAIN = "acaia"
|
||||||
|
CONF_IS_NEW_STYLE_SCALE = "is_new_style_scale"
|
86
homeassistant/components/acaia/coordinator.py
Normal file
86
homeassistant/components/acaia/coordinator.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
"""Coordinator for Acaia integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioacaia.acaiascale import AcaiaScale
|
||||||
|
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_ADDRESS
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .const import CONF_IS_NEW_STYLE_SCALE
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AcaiaConfigEntry = ConfigEntry[AcaiaCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AcaiaCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
"""Class to handle fetching data from the scale."""
|
||||||
|
|
||||||
|
config_entry: AcaiaConfigEntry
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: AcaiaConfigEntry) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name="acaia coordinator",
|
||||||
|
update_interval=SCAN_INTERVAL,
|
||||||
|
config_entry=entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._scale = AcaiaScale(
|
||||||
|
address_or_ble_device=entry.data[CONF_ADDRESS],
|
||||||
|
name=entry.title,
|
||||||
|
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
|
||||||
|
notify_callback=self.async_update_listeners,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scale(self) -> AcaiaScale:
|
||||||
|
"""Return the scale object."""
|
||||||
|
return self._scale
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
"""Fetch data."""
|
||||||
|
|
||||||
|
# scale is already connected, return
|
||||||
|
if self._scale.connected:
|
||||||
|
return
|
||||||
|
|
||||||
|
# scale is not connected, try to connect
|
||||||
|
try:
|
||||||
|
await self._scale.connect(setup_tasks=False)
|
||||||
|
except (AcaiaDeviceNotFound, AcaiaError, TimeoutError) as ex:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Could not connect to scale: %s, Error: %s",
|
||||||
|
self.config_entry.data[CONF_ADDRESS],
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
self._scale.device_disconnected_handler(notify=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
# connected, set up background tasks
|
||||||
|
if not self._scale.heartbeat_task or self._scale.heartbeat_task.done():
|
||||||
|
self._scale.heartbeat_task = self.config_entry.async_create_background_task(
|
||||||
|
hass=self.hass,
|
||||||
|
target=self._scale.send_heartbeats(),
|
||||||
|
name="acaia_heartbeat_task",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self._scale.process_queue_task or self._scale.process_queue_task.done():
|
||||||
|
self._scale.process_queue_task = (
|
||||||
|
self.config_entry.async_create_background_task(
|
||||||
|
hass=self.hass,
|
||||||
|
target=self._scale.process_queue(),
|
||||||
|
name="acaia_process_queue_task",
|
||||||
|
)
|
||||||
|
)
|
40
homeassistant/components/acaia/entity.py
Normal file
40
homeassistant/components/acaia/entity.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"""Base class for Acaia entities."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
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 AcaiaCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
|
||||||
|
"""Common elements for all entities."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AcaiaCoordinator,
|
||||||
|
entity_description: EntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
self._scale = coordinator.scale
|
||||||
|
self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}"
|
||||||
|
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self._scale.mac)},
|
||||||
|
manufacturer="Acaia",
|
||||||
|
model=self._scale.model,
|
||||||
|
suggested_area="Kitchen",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Returns whether entity is available."""
|
||||||
|
return super().available and self._scale.connected
|
15
homeassistant/components/acaia/icons.json
Normal file
15
homeassistant/components/acaia/icons.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"button": {
|
||||||
|
"tare": {
|
||||||
|
"default": "mdi:scale-balance"
|
||||||
|
},
|
||||||
|
"reset_timer": {
|
||||||
|
"default": "mdi:timer-refresh"
|
||||||
|
},
|
||||||
|
"start_stop": {
|
||||||
|
"default": "mdi:timer-play"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
homeassistant/components/acaia/manifest.json
Normal file
29
homeassistant/components/acaia/manifest.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"domain": "acaia",
|
||||||
|
"name": "Acaia",
|
||||||
|
"bluetooth": [
|
||||||
|
{
|
||||||
|
"manufacturer_id": 16962
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"local_name": "ACAIA*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"local_name": "PYXIS-*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"local_name": "LUNAR-*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"local_name": "PROCHBT001"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codeowners": ["@zweckj"],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": ["bluetooth_adapters"],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/acaia",
|
||||||
|
"integration_type": "device",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
"loggers": ["aioacaia"],
|
||||||
|
"requirements": ["aioacaia==0.1.6"]
|
||||||
|
}
|
38
homeassistant/components/acaia/strings.json
Normal file
38
homeassistant/components/acaia/strings.json
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "{name}",
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||||
|
"unsupported_device": "This device is not supported."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"device_not_found": "Device could not be found.",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"bluetooth_confirm": {
|
||||||
|
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
||||||
|
"data": {
|
||||||
|
"address": "[%key:common::config_flow::data::device%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"button": {
|
||||||
|
"tare": {
|
||||||
|
"name": "Tare"
|
||||||
|
},
|
||||||
|
"reset_timer": {
|
||||||
|
"name": "Reset timer"
|
||||||
|
},
|
||||||
|
"start_stop": {
|
||||||
|
"name": "Start/stop timer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,26 @@ from __future__ import annotations
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
|
BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
|
||||||
|
{
|
||||||
|
"domain": "acaia",
|
||||||
|
"manufacturer_id": 16962,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "acaia",
|
||||||
|
"local_name": "ACAIA*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "acaia",
|
||||||
|
"local_name": "PYXIS-*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "acaia",
|
||||||
|
"local_name": "LUNAR-*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "acaia",
|
||||||
|
"local_name": "PROCHBT001",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"domain": "airthings_ble",
|
"domain": "airthings_ble",
|
||||||
"manufacturer_id": 820,
|
"manufacturer_id": 820,
|
||||||
|
|
|
@ -24,6 +24,7 @@ FLOWS = {
|
||||||
],
|
],
|
||||||
"integration": [
|
"integration": [
|
||||||
"abode",
|
"abode",
|
||||||
|
"acaia",
|
||||||
"accuweather",
|
"accuweather",
|
||||||
"acmeda",
|
"acmeda",
|
||||||
"adax",
|
"adax",
|
||||||
|
|
|
@ -11,6 +11,12 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_push"
|
"iot_class": "cloud_push"
|
||||||
},
|
},
|
||||||
|
"acaia": {
|
||||||
|
"name": "Acaia",
|
||||||
|
"integration_type": "device",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "local_push"
|
||||||
|
},
|
||||||
"accuweather": {
|
"accuweather": {
|
||||||
"name": "AccuWeather",
|
"name": "AccuWeather",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
|
|
|
@ -172,6 +172,9 @@ aio-geojson-usgs-earthquakes==0.3
|
||||||
# homeassistant.components.gdacs
|
# homeassistant.components.gdacs
|
||||||
aio-georss-gdacs==0.10
|
aio-georss-gdacs==0.10
|
||||||
|
|
||||||
|
# homeassistant.components.acaia
|
||||||
|
aioacaia==0.1.6
|
||||||
|
|
||||||
# homeassistant.components.airq
|
# homeassistant.components.airq
|
||||||
aioairq==0.3.2
|
aioairq==0.3.2
|
||||||
|
|
||||||
|
|
|
@ -160,6 +160,9 @@ aio-geojson-usgs-earthquakes==0.3
|
||||||
# homeassistant.components.gdacs
|
# homeassistant.components.gdacs
|
||||||
aio-georss-gdacs==0.10
|
aio-georss-gdacs==0.10
|
||||||
|
|
||||||
|
# homeassistant.components.acaia
|
||||||
|
aioacaia==0.1.6
|
||||||
|
|
||||||
# homeassistant.components.airq
|
# homeassistant.components.airq
|
||||||
aioairq==0.3.2
|
aioairq==0.3.2
|
||||||
|
|
||||||
|
|
14
tests/components/acaia/__init__.py
Normal file
14
tests/components/acaia/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
"""Common test tools for the acaia integration."""
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_integration(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Set up the acaia integration for testing."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
80
tests/components/acaia/conftest.py
Normal file
80
tests/components/acaia/conftest.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
"""Common fixtures for the acaia tests."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from aioacaia.acaiascale import AcaiaDeviceState
|
||||||
|
from aioacaia.const import UnitMass as AcaiaUnitOfMass
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.acaia.const import CONF_IS_NEW_STYLE_SCALE, DOMAIN
|
||||||
|
from homeassistant.const import CONF_ADDRESS
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.acaia.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_verify() -> Generator[AsyncMock]:
|
||||||
|
"""Override is_new_scale check."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.acaia.config_flow.is_new_scale", return_value=True
|
||||||
|
) as mock_verify:
|
||||||
|
yield mock_verify
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||||
|
"""Return the default mocked config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
title="LUNAR-DDEEFF",
|
||||||
|
domain=DOMAIN,
|
||||||
|
version=1,
|
||||||
|
data={
|
||||||
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
|
CONF_IS_NEW_STYLE_SCALE: True,
|
||||||
|
},
|
||||||
|
unique_id="aa:bb:cc:dd:ee:ff",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def init_integration(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_scale: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Set up the acaia integration for testing."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_scale() -> Generator[MagicMock]:
|
||||||
|
"""Return a mocked acaia scale client."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.acaia.coordinator.AcaiaScale",
|
||||||
|
autospec=True,
|
||||||
|
) as scale_mock,
|
||||||
|
):
|
||||||
|
scale = scale_mock.return_value
|
||||||
|
scale.connected = True
|
||||||
|
scale.mac = "aa:bb:cc:dd:ee:ff"
|
||||||
|
scale.model = "Lunar"
|
||||||
|
scale.timer_running = True
|
||||||
|
scale.heartbeat_task = None
|
||||||
|
scale.process_queue_task = None
|
||||||
|
scale.device_state = AcaiaDeviceState(
|
||||||
|
battery_level=42, units=AcaiaUnitOfMass.GRAMS
|
||||||
|
)
|
||||||
|
scale.weight = 123.45
|
||||||
|
yield scale
|
139
tests/components/acaia/snapshots/test_button.ambr
Normal file
139
tests/components/acaia/snapshots/test_button.ambr
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
# serializer version: 1
|
||||||
|
# name: test_buttons[entry_button_reset_timer]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'button',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'button.lunar_ddeeff_reset_timer',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Reset timer',
|
||||||
|
'platform': 'acaia',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'reset_timer',
|
||||||
|
'unique_id': 'aa:bb:cc:dd:ee:ff_reset_timer',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_buttons[entry_button_start_stop_timer]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'button',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'button.lunar_ddeeff_start_stop_timer',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Start/stop timer',
|
||||||
|
'platform': 'acaia',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'start_stop',
|
||||||
|
'unique_id': 'aa:bb:cc:dd:ee:ff_start_stop',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_buttons[entry_button_tare]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'button',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'button.lunar_ddeeff_tare',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Tare',
|
||||||
|
'platform': 'acaia',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'tare',
|
||||||
|
'unique_id': 'aa:bb:cc:dd:ee:ff_tare',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_buttons[state_button_reset_timer]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'LUNAR-DDEEFF Reset timer',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'button.lunar_ddeeff_reset_timer',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_buttons[state_button_start_stop_timer]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'LUNAR-DDEEFF Start/stop timer',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'button.lunar_ddeeff_start_stop_timer',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_buttons[state_button_tare]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'LUNAR-DDEEFF Tare',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'button.lunar_ddeeff_tare',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
33
tests/components/acaia/snapshots/test_init.ambr
Normal file
33
tests/components/acaia/snapshots/test_init.ambr
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# serializer version: 1
|
||||||
|
# name: test_device
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': 'kitchen',
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'acaia',
|
||||||
|
'aa:bb:cc:dd:ee:ff',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Acaia',
|
||||||
|
'model': 'Lunar',
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'LUNAR-DDEEFF',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': None,
|
||||||
|
'suggested_area': 'Kitchen',
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': None,
|
||||||
|
})
|
||||||
|
# ---
|
83
tests/components/acaia/test_button.py
Normal file
83
tests/components/acaia/test_button.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
"""Tests for the acaia buttons."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures("init_integration")
|
||||||
|
|
||||||
|
|
||||||
|
BUTTONS = (
|
||||||
|
"tare",
|
||||||
|
"reset_timer",
|
||||||
|
"start_stop_timer",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_buttons(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test the acaia buttons."""
|
||||||
|
for button in BUTTONS:
|
||||||
|
state = hass.states.get(f"button.lunar_ddeeff_{button}")
|
||||||
|
assert state
|
||||||
|
assert state == snapshot(name=f"state_button_{button}")
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(state.entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry == snapshot(name=f"entry_button_{button}")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_button_presses(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_scale: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test the acaia button presses."""
|
||||||
|
|
||||||
|
for button in BUTTONS:
|
||||||
|
await hass.services.async_call(
|
||||||
|
BUTTON_DOMAIN,
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: f"button.lunar_ddeeff_{button}",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
function = getattr(mock_scale, button)
|
||||||
|
function.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_buttons_unavailable_on_disconnected_scale(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_scale: MagicMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test the acaia buttons are unavailable when the scale is disconnected."""
|
||||||
|
|
||||||
|
for button in BUTTONS:
|
||||||
|
state = hass.states.get(f"button.lunar_ddeeff_{button}")
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
mock_scale.connected = False
|
||||||
|
freezer.tick(timedelta(minutes=10))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
for button in BUTTONS:
|
||||||
|
state = hass.states.get(f"button.lunar_ddeeff_{button}")
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
242
tests/components/acaia/test_config_flow.py
Normal file
242
tests/components/acaia/test_config_flow.py
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
"""Test the acaia config flow."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.acaia.const import CONF_IS_NEW_STYLE_SCALE, DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_ADDRESS
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
service_info = BluetoothServiceInfo(
|
||||||
|
name="LUNAR-DDEEFF",
|
||||||
|
address="aa:bb:cc:dd:ee:ff",
|
||||||
|
rssi=-63,
|
||||||
|
manufacturer_data={},
|
||||||
|
service_data={},
|
||||||
|
service_uuids=[],
|
||||||
|
source="local",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_discovered_service_info() -> Generator[AsyncMock]:
|
||||||
|
"""Override getting Bluetooth service info."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.acaia.config_flow.async_discovered_service_info",
|
||||||
|
return_value=[service_info],
|
||||||
|
) as mock_discovered_service_info:
|
||||||
|
yield mock_discovered_service_info
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_verify: AsyncMock,
|
||||||
|
mock_discovered_service_info: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test we get the form."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
user_input = {
|
||||||
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
|
}
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2["title"] == "LUNAR-DDEEFF"
|
||||||
|
assert result2["data"] == {
|
||||||
|
**user_input,
|
||||||
|
CONF_IS_NEW_STYLE_SCALE: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_bluetooth_discovery(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_verify: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test we can discover a device."""
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "bluetooth_confirm"
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2["title"] == service_info.name
|
||||||
|
assert result2["data"] == {
|
||||||
|
CONF_ADDRESS: service_info.address,
|
||||||
|
CONF_IS_NEW_STYLE_SCALE: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "error"),
|
||||||
|
[
|
||||||
|
(AcaiaDeviceNotFound("Error"), "device_not_found"),
|
||||||
|
(AcaiaError, "unknown"),
|
||||||
|
(AcaiaUnknownDevice, "unsupported_device"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_bluetooth_discovery_errors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_verify: AsyncMock,
|
||||||
|
exception: Exception,
|
||||||
|
error: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test abortions of Bluetooth discovery."""
|
||||||
|
mock_verify.side_effect = exception
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == error
|
||||||
|
|
||||||
|
|
||||||
|
async def test_already_configured(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_verify: AsyncMock,
|
||||||
|
mock_discovered_service_info: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Ensure we can't add the same device twice."""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.ABORT
|
||||||
|
assert result2["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_already_configured_bluetooth_discovery(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Ensure configure device is not discovered again."""
|
||||||
|
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "error"),
|
||||||
|
[
|
||||||
|
(AcaiaDeviceNotFound("Error"), "device_not_found"),
|
||||||
|
(AcaiaError, "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_recoverable_config_flow_errors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_verify: AsyncMock,
|
||||||
|
mock_discovered_service_info: AsyncMock,
|
||||||
|
exception: Exception,
|
||||||
|
error: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test recoverable errors."""
|
||||||
|
mock_verify.side_effect = exception
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.FORM
|
||||||
|
assert result2["errors"] == {"base": error}
|
||||||
|
|
||||||
|
# recover
|
||||||
|
mock_verify.side_effect = None
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unsupported_device(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_verify: AsyncMock,
|
||||||
|
mock_discovered_service_info: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test flow aborts on unsupported device."""
|
||||||
|
mock_verify.side_effect = AcaiaUnknownDevice
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.ABORT
|
||||||
|
assert result2["reason"] == "unsupported_device"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_bluetooth_devices(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_discovered_service_info: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test flow aborts on unsupported device."""
|
||||||
|
mock_discovered_service_info.return_value = []
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "no_devices_found"
|
65
tests/components/acaia/test_init.py
Normal file
65
tests/components/acaia/test_init.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
"""Test init of acaia integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.acaia.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures("init_integration")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_unload_config_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test loading and unloading the integration."""
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"exception", [AcaiaError, AcaiaDeviceNotFound("Boom"), TimeoutError]
|
||||||
|
)
|
||||||
|
async def test_update_exception_leads_to_active_disconnect(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_scale: MagicMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
exception: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Test scale gets disconnected on exception."""
|
||||||
|
|
||||||
|
mock_scale.connect.side_effect = exception
|
||||||
|
mock_scale.connected = False
|
||||||
|
|
||||||
|
freezer.tick(timedelta(minutes=10))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_scale.device_disconnected_handler.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device(
|
||||||
|
mock_scale: MagicMock,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Snapshot the device from registry."""
|
||||||
|
|
||||||
|
device = device_registry.async_get_device({(DOMAIN, mock_scale.mac)})
|
||||||
|
assert device
|
||||||
|
assert device == snapshot
|
Loading…
Add table
Reference in a new issue