mirror of
https://github.com/elisspace/core.git
synced 2026-09-30 06:19:55 +00:00
Add Anova integration (#86254)
* init setup of Anova Sous Vide * bump anova-wifi to 0.2.4 * Removed yaml support * Bump to anova-wifi 0.2.5 * Added support for adding sous vide while offline * Added basic test for sensor * added better tests for sensors and init * expanded code coverage * Decreased timedelta to lowest functioning value. * Updating my username * migrate to async_forward_entry_setups * applying pr recommended changes * bump anova-wifi to 0.2.7 * Improvements to hopefully get this review ready * formatting changes * clean ups for pr review * remove unneeded unique id check. * bump ao anova_wifi 0.3.0 * rename device_id to device_unique_id * renamed to 'anova' * added unique_id to MockConfigEntry * removed leftover anova sous vides * added device id to strings * added error for incorrect device id * add has_entity_name * added attr name for tests * added authentication functionality * bump to 0.4.3 * split entity into its own class/object * pulling firmware version out of async_setup Co-authored-by: J. Nick Koston <nick@koston.org> * addressed pr changes * fixed pytest * added anova data model * removed unneeded time change * add logging in package * rework step_user * Update homeassistant/components/anova/sensor.py Co-authored-by: J. Nick Koston <nick@koston.org> * Removed lower from attr unique id Co-authored-by: J. Nick Koston <nick@koston.org> * Removed unneeded member variables in sensor Co-authored-by: J. Nick Koston <nick@koston.org> * removed repeated subclass attr Co-authored-by: J. Nick Koston <nick@koston.org> * simplify update_failed test * created descriptionentity * bump to 0.6.1 limit ws connect * add translation for sensor entities * version bump - support pro model * add anova to strict typing * fixed sensor not getting datas type * Apply suggestions from code review Co-authored-by: J. Nick Koston <nick@koston.org> * Check for new devices in init * style changes * return false instead of config entry not ready * move serialize_device_list to utils * move repeating device check into api * moved unneeded code out of try except * fixed tests to get 100% cov * Update homeassistant/components/anova/strings.json Co-authored-by: J. Nick Koston <nick@koston.org> --------- Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""Tests for the Anova integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from anova_wifi import (
|
||||
AnovaPrecisionCooker,
|
||||
AnovaPrecisionCookerBinarySensor,
|
||||
AnovaPrecisionCookerSensor,
|
||||
)
|
||||
|
||||
from homeassistant.components.anova.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DEVICE_UNIQUE_ID = "abc123def"
|
||||
|
||||
CONF_INPUT = {CONF_USERNAME: "sample@gmail.com", CONF_PASSWORD: "sample"}
|
||||
|
||||
ONLINE_UPDATE = {
|
||||
"sensors": {
|
||||
AnovaPrecisionCookerSensor.COOK_TIME: 0,
|
||||
AnovaPrecisionCookerSensor.MODE: "Low water",
|
||||
AnovaPrecisionCookerSensor.STATE: "No state",
|
||||
AnovaPrecisionCookerSensor.TARGET_TEMPERATURE: 23.33,
|
||||
AnovaPrecisionCookerSensor.COOK_TIME_REMAINING: 0,
|
||||
AnovaPrecisionCookerSensor.FIRMWARE_VERSION: "2.2.0",
|
||||
AnovaPrecisionCookerSensor.HEATER_TEMPERATURE: 20.87,
|
||||
AnovaPrecisionCookerSensor.TRIAC_TEMPERATURE: 21.79,
|
||||
AnovaPrecisionCookerSensor.WATER_TEMPERATURE: 21.33,
|
||||
},
|
||||
"binary_sensors": {
|
||||
AnovaPrecisionCookerBinarySensor.COOKING: False,
|
||||
AnovaPrecisionCookerBinarySensor.DEVICE_SAFE: True,
|
||||
AnovaPrecisionCookerBinarySensor.WATER_LEAK: False,
|
||||
AnovaPrecisionCookerBinarySensor.WATER_LEVEL_CRITICAL: True,
|
||||
AnovaPrecisionCookerBinarySensor.WATER_TEMP_TOO_HIGH: False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_entry(hass: HomeAssistant, device_id: str = DEVICE_UNIQUE_ID) -> ConfigEntry:
|
||||
"""Add config entry in Home Assistant."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Anova",
|
||||
data={
|
||||
CONF_USERNAME: "sample@gmail.com",
|
||||
CONF_PASSWORD: "sample",
|
||||
"devices": [(device_id, "type_sample")],
|
||||
},
|
||||
unique_id="sample@gmail.com",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
|
||||
|
||||
async def async_init_integration(
|
||||
hass: HomeAssistant,
|
||||
skip_setup: bool = False,
|
||||
error: str | None = None,
|
||||
) -> ConfigEntry:
|
||||
"""Set up the Anova integration in Home Assistant."""
|
||||
with patch(
|
||||
"homeassistant.components.anova.coordinator.AnovaPrecisionCooker.update"
|
||||
) as update_patch, patch(
|
||||
"homeassistant.components.anova.AnovaApi.authenticate"
|
||||
), patch(
|
||||
"homeassistant.components.anova.AnovaApi.get_devices"
|
||||
) as device_patch:
|
||||
update_patch.return_value = ONLINE_UPDATE
|
||||
device_patch.return_value = [
|
||||
AnovaPrecisionCooker(None, DEVICE_UNIQUE_ID, "type_sample", None)
|
||||
]
|
||||
|
||||
entry = create_entry(hass)
|
||||
|
||||
if not skip_setup:
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Common fixtures for Anova."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from anova_wifi import AnovaApi, AnovaPrecisionCooker, InvalidLogin, NoDevicesFound
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import DEVICE_UNIQUE_ID
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def anova_api(
|
||||
hass: HomeAssistant,
|
||||
) -> AnovaApi:
|
||||
"""Mock the api for Anova."""
|
||||
api_mock = AsyncMock()
|
||||
|
||||
new_device = AnovaPrecisionCooker(None, DEVICE_UNIQUE_ID, "type_sample", None)
|
||||
|
||||
async def authenticate_side_effect():
|
||||
api_mock.jwt = "my_test_jwt"
|
||||
|
||||
async def get_devices_side_effect():
|
||||
if not api_mock.existing_devices:
|
||||
api_mock.existing_devices = []
|
||||
api_mock.existing_devices = api_mock.existing_devices + [new_device]
|
||||
return [new_device]
|
||||
|
||||
api_mock.authenticate.side_effect = authenticate_side_effect
|
||||
api_mock.get_devices.side_effect = get_devices_side_effect
|
||||
|
||||
with patch("homeassistant.components.anova.AnovaApi", return_value=api_mock):
|
||||
api = AnovaApi(
|
||||
None,
|
||||
"sample@gmail.com",
|
||||
"sample",
|
||||
)
|
||||
yield api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def anova_api_no_devices(
|
||||
hass: HomeAssistant,
|
||||
) -> AnovaApi:
|
||||
"""Mock the api for Anova with no online devices."""
|
||||
api_mock = AsyncMock()
|
||||
|
||||
async def authenticate_side_effect():
|
||||
api_mock.jwt = "my_test_jwt"
|
||||
|
||||
async def get_devices_side_effect():
|
||||
raise NoDevicesFound()
|
||||
|
||||
api_mock.authenticate.side_effect = authenticate_side_effect
|
||||
api_mock.get_devices.side_effect = get_devices_side_effect
|
||||
|
||||
with patch("homeassistant.components.anova.AnovaApi", return_value=api_mock):
|
||||
api = AnovaApi(
|
||||
None,
|
||||
"sample@gmail.com",
|
||||
"sample",
|
||||
)
|
||||
yield api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def anova_api_wrong_login(
|
||||
hass: HomeAssistant,
|
||||
) -> AnovaApi:
|
||||
"""Mock the api for Anova with a wrong login."""
|
||||
api_mock = AsyncMock()
|
||||
|
||||
async def authenticate_side_effect():
|
||||
raise InvalidLogin()
|
||||
|
||||
api_mock.authenticate.side_effect = authenticate_side_effect
|
||||
|
||||
with patch("homeassistant.components.anova.AnovaApi", return_value=api_mock):
|
||||
api = AnovaApi(
|
||||
None,
|
||||
"sample@gmail.com",
|
||||
"sample",
|
||||
)
|
||||
yield api
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Test Anova config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from anova_wifi import AnovaPrecisionCooker, InvalidLogin, NoDevicesFound
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.anova.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import CONF_INPUT, DEVICE_UNIQUE_ID, create_entry
|
||||
|
||||
|
||||
async def test_flow_user(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test user initialized flow."""
|
||||
with patch(
|
||||
"homeassistant.components.anova.config_flow.AnovaApi.authenticate",
|
||||
) as auth_patch, patch(
|
||||
"homeassistant.components.anova.AnovaApi.get_devices"
|
||||
) as device_patch, patch(
|
||||
"homeassistant.components.anova.AnovaApi.authenticate"
|
||||
), patch(
|
||||
"homeassistant.components.anova.config_flow.AnovaApi.get_devices"
|
||||
) as config_flow_device_patch:
|
||||
auth_patch.return_value = True
|
||||
device_patch.return_value = [
|
||||
AnovaPrecisionCooker(None, DEVICE_UNIQUE_ID, "type_sample", None)
|
||||
]
|
||||
config_flow_device_patch.return_value = [
|
||||
AnovaPrecisionCooker(None, DEVICE_UNIQUE_ID, "type_sample", None)
|
||||
]
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_USERNAME: "sample@gmail.com",
|
||||
CONF_PASSWORD: "sample",
|
||||
"devices": [(DEVICE_UNIQUE_ID, "type_sample")],
|
||||
}
|
||||
|
||||
|
||||
async def test_flow_user_already_configured(hass: HomeAssistant) -> None:
|
||||
"""Test user initialized flow with duplicate device."""
|
||||
with patch(
|
||||
"homeassistant.components.anova.config_flow.AnovaApi.authenticate",
|
||||
) as auth_patch, patch(
|
||||
"homeassistant.components.anova.AnovaApi.get_devices"
|
||||
) as device_patch, patch(
|
||||
"homeassistant.components.anova.config_flow.AnovaApi.get_devices"
|
||||
) as config_flow_device_patch:
|
||||
auth_patch.return_value = True
|
||||
device_patch.return_value = [
|
||||
AnovaPrecisionCooker(None, DEVICE_UNIQUE_ID, "type_sample", None)
|
||||
]
|
||||
config_flow_device_patch.return_value = [
|
||||
AnovaPrecisionCooker(None, DEVICE_UNIQUE_ID, "type_sample", None)
|
||||
]
|
||||
create_entry(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_flow_wrong_login(hass: HomeAssistant) -> None:
|
||||
"""Test incorrect login throwing error."""
|
||||
with patch(
|
||||
"homeassistant.components.anova.config_flow.AnovaApi.authenticate",
|
||||
side_effect=InvalidLogin,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_flow_unknown_error(hass: HomeAssistant) -> None:
|
||||
"""Test unknown error throwing error."""
|
||||
with patch(
|
||||
"homeassistant.components.anova.config_flow.AnovaApi.authenticate",
|
||||
side_effect=Exception(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_flow_no_devices(hass: HomeAssistant) -> None:
|
||||
"""Test unknown error throwing error."""
|
||||
with patch(
|
||||
"homeassistant.components.anova.config_flow.AnovaApi.authenticate"
|
||||
), patch(
|
||||
"homeassistant.components.anova.config_flow.AnovaApi.get_devices",
|
||||
side_effect=NoDevicesFound(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_devices_found"}
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Test init for Anova."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from anova_wifi import AnovaApi
|
||||
|
||||
from homeassistant.components.anova import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import ONLINE_UPDATE, async_init_integration, create_entry
|
||||
|
||||
|
||||
async def test_async_setup_entry(hass: HomeAssistant, anova_api: AnovaApi) -> None:
|
||||
"""Test a successful setup entry."""
|
||||
await async_init_integration(hass)
|
||||
state = hass.states.get("sensor.anova_precision_cooker_mode")
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "Low water"
|
||||
|
||||
|
||||
async def test_wrong_login(
|
||||
hass: HomeAssistant, anova_api_wrong_login: AnovaApi
|
||||
) -> None:
|
||||
"""Test for setup failure if connection to Anova is missing."""
|
||||
entry = create_entry(hass)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_new_devices(hass: HomeAssistant, anova_api: AnovaApi) -> None:
|
||||
"""Test for if we find a new device on init."""
|
||||
entry = create_entry(hass, "test_device_2")
|
||||
with patch(
|
||||
"homeassistant.components.anova.coordinator.AnovaPrecisionCooker.update"
|
||||
) as update_patch:
|
||||
update_patch.return_value = ONLINE_UPDATE
|
||||
assert len(entry.data["devices"]) == 1
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(entry.data["devices"]) == 2
|
||||
|
||||
|
||||
async def test_device_cached_but_offline(
|
||||
hass: HomeAssistant, anova_api_no_devices: AnovaApi
|
||||
) -> None:
|
||||
"""Test if we have previously seen a device, but it was offline on startup."""
|
||||
entry = create_entry(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.anova.coordinator.AnovaPrecisionCooker.update"
|
||||
) as update_patch:
|
||||
update_patch.return_value = ONLINE_UPDATE
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(entry.data["devices"]) == 1
|
||||
state = hass.states.get("sensor.anova_precision_cooker_mode")
|
||||
assert state is not None
|
||||
assert state.state == "Low water"
|
||||
|
||||
|
||||
async def test_unload_entry(hass: HomeAssistant, anova_api: AnovaApi) -> None:
|
||||
"""Test successful unload of entry."""
|
||||
entry = await async_init_integration(hass)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Test the Anova sensors."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
from anova_wifi import AnovaApi, AnovaOffline
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt
|
||||
|
||||
from . import async_init_integration
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def test_sensors(hass: HomeAssistant, anova_api: AnovaApi) -> None:
|
||||
"""Test setting up creates the sensors."""
|
||||
await async_init_integration(hass)
|
||||
assert len(hass.states.async_all("sensor")) == 8
|
||||
assert (
|
||||
hass.states.get("sensor.anova_precision_cooker_cook_time_remaining").state
|
||||
== "0"
|
||||
)
|
||||
assert hass.states.get("sensor.anova_precision_cooker_cook_time").state == "0"
|
||||
assert (
|
||||
hass.states.get("sensor.anova_precision_cooker_heater_temperature").state
|
||||
== "20.87"
|
||||
)
|
||||
assert hass.states.get("sensor.anova_precision_cooker_mode").state == "Low water"
|
||||
assert hass.states.get("sensor.anova_precision_cooker_state").state == "No state"
|
||||
assert (
|
||||
hass.states.get("sensor.anova_precision_cooker_target_temperature").state
|
||||
== "23.33"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.anova_precision_cooker_water_temperature").state
|
||||
== "21.33"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.anova_precision_cooker_triac_temperature").state
|
||||
== "21.79"
|
||||
)
|
||||
|
||||
|
||||
async def test_update_failed(hass: HomeAssistant, anova_api: AnovaApi) -> None:
|
||||
"""Test updating data after the coordinator has been set up, but anova is offline."""
|
||||
await async_init_integration(hass)
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.components.anova.AnovaPrecisionCooker.update",
|
||||
side_effect=AnovaOffline(),
|
||||
):
|
||||
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=61))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.anova_precision_cooker_water_temperature")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
Reference in New Issue
Block a user