Repository: home-assistant/core
Pull request: Convert Anova to cloud push
<!--
  You are amazing! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
-->
## Breaking change
<!--
  If your PR contains a breaking change for existing users, it is important
  to tell them what breaks, how to make it work again and why we did this.
  This piece of text is published with the release notes, so it helps if you
  write it towards our users, not us.
  Note: Remove this section if this PR is NOT a breaking change.
-->
The `State` and `Mode` entities for Anova devices have been changed to match the new protocol messaging. Any automations based on these entities will need to be updated.

## Proposed change
<!--
  Describe the big picture of your changes here to communicate to the
  maintainers why we should accept this pull request. If it fixes a bug
  or resolves a feature request, be sure to link to that issue in the
  additional information section.
-->
This changes Anova to use websockets instead of the old REST api. It should allow support for a bunch of different devices including the Anova Oven. 

Package update has to be included with this PR and not done separately as this is a major internal refactor.
https://github.com/Lash-L/anova_wifi/compare/v0.10.0...v0.12.0

## Type of change
<!--
  What type of change does your PR introduce to Home Assistant?
  NOTE: Please, check only 1! box!
  If your PR requires multiple boxes to be checked, you'll most likely need to
  split it into multiple PRs. This makes things easier and faster to code review.
-->

- [x] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New integration (thank you!)
- [x] New feature (which adds functionality to an existing integration)
- [ ] Deprecation (breaking change to happen in the future)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #92457
- This PR is related to issue: 
- Link to documentation pull request: https://github.com/home-assistant/home-assistant.io/pull/31223

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [x] The code change is tested and works locally.
- [x] Local tests pass. **Your PR cannot be merged unless tests pass**
- [x] There is no commented out code in this PR.
- [x] I have followed the [development checklist][dev-checklist]
- [x] I have followed the [perfect PR recommendations][perfect-pr]
- [x] The code has been formatted using Ruff (`ruff format homeassistant tests`)
- [x] Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

- [x] Documentation added/updated for [www.home-assistant.io][docs-repository]

If the code communicates with devices, web services, or third-party tools:

- [x] The [manifest file][manifest-docs] has all fields filled out correctly.  
      Updated and included derived files by running: `python3 -m script.hassfest`.
- [x] New or updated dependencies have been added to `requirements_all.txt`.  
      Updated by running `python3 -m script.gen_requirements_all`.
- [x] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
- [ ] Untested files have been added to `.coveragerc`.

<!--
  This project is very active and we have a high turnover of pull requests.

  Unfortunately, the number of incoming pull requests is higher than what our
  reviewers can review and merge so there is a long backlog of pull requests
  waiting for review. You can help here!
  
  By reviewing another pull request, you will help raise the code quality of
  that pull request and the final review will be faster. This way the general
  pace of pull request reviews will go up and your wait time will go down.
  
  When picking a pull request to review, try to choose one that hasn't yet
  been reviewed.

  Thanks for helping out!
-->

To help with the load of incoming pull requests:

- [ ] I have reviewed two other [open pull requests][prs] in this repository.

[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure

<!--
  Thank you for contributing <3

  Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
[manifest-docs]: https://developers.home-assistant.io/docs/creating_integration_manifest/
[quality-scale]: https://developers.home-assistant.io/docs/integration_quality_scale_index/
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr


Problem statement:
Anova precision cooker not found
### The problem

I have an anova precision cooker in my app and it is currently online and allows me to control it but I am unable to add the integration because it does not find any devices.

### What version of Home Assistant Core has the issue?

Core-2023.5.0

### What was the last working version of Home Assistant Core?

_No response_

### What type of installation are you running?

Home Assistant OS

### Integration causing the issue

Anova

### Link to integration documentation on our website

_No response_

### Diagnostics information

_No response_

### Example YAML snippet

_No response_

### Anything in the logs that might be useful for us?

_No response_

### Additional information

_No response_

Hints from issue discussion:
Hey there @lash-l, mind taking a look at this issue as it has been labeled with an integration (`anova`) you are listed as a [code owner](https://github.com/home-assistant/core/blob/dev/CODEOWNERS#L85) for? Thanks!

<details>
<summary>Code owner commands</summary>


Code owners of `anova` can trigger bot actions by commenting:

- `@home-assistant close` Closes the issue.
- `@home-assistant rename Awesome new title` Renames the issue.
- `@home-assistant reopen` Reopen the issue.
- `@home-assistant unassign anova` Removes the current integration label and assignees on the issue, add the integration domain after the command.

</details>


<sub><sup>(message by CodeOwnersMention)</sup></sub>

---

[anova documentation](https://www.home-assistant.io/integrations/anova)
[anova source](https://github.com/home-assistant/core/tree/dev/homeassistant/components/anova)
<sub><sup>(message by IssueLinks)</sup></sub>
What device do you have? Can you give me the model number located on the device?
i'm also having the same result. The model number is only: Anova PC.   I think one of the first models. it has a blue wheel where i can control the temperature.
I can ping the device (ESP-28C651) it's also online in the app.

Cooker details from the app: 
Model Number A3
Firmware 4.6.4
![20230504_161322](https://user-images.githubusercontent.com/52957720/236234933-9169b186-cfd2-471f-98c0-5f41a88ea3ae.jpg)
![20230504_161328](https://user-images.githubusercontent.com/52957720/236234954-1c6a8756-8ab5-4c92-a582-cdb5d6da2fd8.jpg)

Mine is also the Anova PC but it's the 110v version.
Interesting... 

Thank you both for this info. I'm going to try to get a version out with some logging on the custom component version. Once I do I would appreciate if you could try installing that version and sending me the logs.

I'm pretty confident I know what is wrong.
> Interesting...
> 
> Thank you both for this info. I'm going to try to get a version out with some logging on the custom component version. Once I do I would appreciate if you could try installing that version and sending me the logs.
> 
> I'm pretty confident I know what is wrong.

Anything you need, just let me know.
Okay download the latest version through HACS. https://github.com/Lash-L/anova-wifi-hacs

make sure you download 'master'

then go into your configuration.yaml and add 'custom_components.anova_sous_vide: debug' in loggers

Then try starting up and send me your debug logs
Actually - looks like I was too fast with trying to change things. I don't have the time right now but I need to fix a few things.
I have a similar issue.

App/Cooker Information:
-----------------------
Platform: Android (33)
App Version: 3.4.6
Model Number: A3
Device ID: anova f56-a01ffded44a
Connection Type: Wi-Fi
Firmware Version: ver 5.5.1
I do as well, "Retrying setup: Cannot connect to sous vide - perhaps it is not online?"

Platform: Android (33)
App Version: 3.4.6
Model Number: A3
Device ID: anova f56-a81ca1697c4
Connection Type: Wi-Fi
Firmware Version: ver 6.1.1

Commit message: Change to sensor setup to be listener based.

Patch to review:
diff --git a/homeassistant/components/anova/__init__.py b/homeassistant/components/anova/__init__.py
index 6181d02025dbfc..dc550bc4c028f4 100644
--- a/homeassistant/components/anova/__init__.py
+++ b/homeassistant/components/anova/__init__.py
@@ -3,17 +3,16 @@
 
 import logging
 
-from anova_wifi import AnovaApi, AnovaPrecisionCooker, InvalidLogin, NoDevicesFound
+from anova_wifi import AnovaApi, InvalidLogin, NoDevicesFound
 
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME, Platform
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import aiohttp_client
 
 from .const import DOMAIN
 from .coordinator import AnovaCoordinator
 from .models import AnovaData
-from .util import serialize_device_list
 
 PLATFORMS = [Platform.SENSOR]
 
@@ -35,36 +34,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         )
         return False
     assert api.jwt
-    api.existing_devices = [
-        AnovaPrecisionCooker(
-            aiohttp_client.async_get_clientsession(hass),
-            device[0],
-            device[1],
-            api.jwt,
-        )
-        for device in entry.data[CONF_DEVICES]
-    ]
     try:
-        new_devices = await api.get_devices()
+        await api.create_websocket()
     except NoDevicesFound:
-        # get_devices raises an exception if no devices are online
-        new_devices = []
-    devices = api.existing_devices
-    if new_devices:
-        hass.config_entries.async_update_entry(
-            entry,
-            data={
-                **entry.data,
-                **{CONF_DEVICES: serialize_device_list(devices)},
-            },
+        _LOGGER.warning(
+            "No devices were found on the websocket, perhaps you don't have any devices on this account?"
         )
-    coordinators = [AnovaCoordinator(hass, device) for device in devices]
-    for coordinator in coordinators:
-        await coordinator.async_config_entry_first_refresh()
-        firmware_version = coordinator.data.sensor.firmware_version
-        coordinator.async_setup(str(firmware_version))
+    assert api.websocket_handler
+    # Create a coordinator per device, if the device is offline, no data will be on the websocket,
+    # and the coordinator should auto mark as unavailable. But as long as the websocket successfully connected,
+    # config entry should setup.
+    coordinators = [
+        AnovaCoordinator(hass, device)
+        for device in api.websocket_handler.devices.values()
+    ]
     hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AnovaData(
-        api_jwt=api.jwt, precision_cookers=devices, coordinators=coordinators
+        api_jwt=api.jwt, coordinators=coordinators, api=api
     )
     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
     return True
@@ -73,6 +58,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Unload a config entry."""
     if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
-        hass.data[DOMAIN].pop(entry.entry_id)
-
+        anova_data: AnovaData = hass.data[DOMAIN].pop(entry.entry_id)
+        # Disconnect from WS
+        await anova_data.api.disconnect_websocket()
     return unload_ok
+
+
+async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+    """Migrate old entry."""
+
+    if config_entry.version == 1:
+        # It used to be needed to persist devices, however that is no longer the case as the
+        # websocket holds information for all devices on the account.
+        new = {
+            CONF_USERNAME: config_entry.data[CONF_USERNAME],
+            CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
+        }
+
+        config_entry.version = 2
+        hass.config_entries.async_update_entry(config_entry, data=new)
+
+    return True
diff --git a/homeassistant/components/anova/config_flow.py b/homeassistant/components/anova/config_flow.py
index d0846fbffc7fcc..d83ea55fb0ef4e 100644
--- a/homeassistant/components/anova/config_flow.py
+++ b/homeassistant/components/anova/config_flow.py
@@ -1,22 +1,21 @@
 """Config flow for Anova."""
 from __future__ import annotations
 
-from anova_wifi import AnovaApi, InvalidLogin, NoDevicesFound
+from anova_wifi import AnovaApi, InvalidLogin
 import voluptuous as vol
 
 from homeassistant.config_entries import ConfigFlow
-from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
 from homeassistant.data_entry_flow import FlowResult
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 
 from .const import DOMAIN
-from .util import serialize_device_list
 
 
 class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
     """Sets up a config flow for Anova."""
 
-    VERSION = 1
+    VERSION = 2
 
     async def async_step_user(
         self, user_input: dict[str, str] | None = None
@@ -33,22 +32,16 @@ async def async_step_user(
             self._abort_if_unique_id_configured()
             try:
                 await api.authenticate()
-                devices = await api.get_devices()
             except InvalidLogin:
                 errors["base"] = "invalid_auth"
-            except NoDevicesFound:
-                errors["base"] = "no_devices_found"
             except Exception:  # pylint: disable=broad-except
                 errors["base"] = "unknown"
             else:
-                # We store device list in config flow in order to persist found devices on restart, as the Anova api get_devices does not return any devices that are offline.
-                device_list = serialize_device_list(devices)
                 return self.async_create_entry(
                     title="Anova",
                     data={
-                        CONF_USERNAME: api.username,
-                        CONF_PASSWORD: api.password,
-                        CONF_DEVICES: device_list,
+                        CONF_USERNAME: user_input[CONF_USERNAME],
+                        CONF_PASSWORD: user_input[CONF_PASSWORD],
                     },
                 )
 
diff --git a/homeassistant/components/anova/coordinator.py b/homeassistant/components/anova/coordinator.py
index 83dc2c295c39ca..049e536ae65d82 100644
--- a/homeassistant/components/anova/coordinator.py
+++ b/homeassistant/components/anova/coordinator.py
@@ -1,13 +1,12 @@
 """Support for Anova Coordinators."""
-from asyncio import timeout
-from datetime import timedelta
 import logging
 
-from anova_wifi import AnovaOffline, AnovaPrecisionCooker, APCUpdate
+from anova_wifi import APCUpdate, APCWifiDevice
 
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
 from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
 
 from .const import DOMAIN
 
@@ -17,37 +16,24 @@
 class AnovaCoordinator(DataUpdateCoordinator[APCUpdate]):
     """Anova custom coordinator."""
 
-    def __init__(
-        self,
-        hass: HomeAssistant,
-        anova_device: AnovaPrecisionCooker,
-    ) -> None:
+    config_entry: ConfigEntry
+
+    def __init__(self, hass: HomeAssistant, anova_device: APCWifiDevice) -> None:
         """Set up Anova Coordinator."""
         super().__init__(
             hass,
             name="Anova Precision Cooker",
             logger=_LOGGER,
-            update_interval=timedelta(seconds=30),
         )
-        assert self.config_entry is not None
-        self.device_unique_id = anova_device.device_key
+        self.device_unique_id = anova_device.cooker_id
         self.anova_device = anova_device
+        self.anova_device.set_update_listener(self.async_set_updated_data)
         self.device_info: DeviceInfo | None = None
 
-    @callback
-    def async_setup(self, firmware_version: str) -> None:
-        """Set the firmware version info."""
         self.device_info = DeviceInfo(
             identifiers={(DOMAIN, self.device_unique_id)},
             name="Anova Precision Cooker",
             manufacturer="Anova",
             model="Precision Cooker",
-            sw_version=firmware_version,
         )
-
-    async def _async_update_data(self) -> APCUpdate:
-        try:
-            async with timeout(5):
-                return await self.anova_device.update()
-        except AnovaOffline as err:
-            raise UpdateFailed(err) from err
+        self.sensor_data_set: bool = False
diff --git a/homeassistant/components/anova/entity.py b/homeassistant/components/anova/entity.py
index d3ed2eb2667c0f..823d5e2c519012 100644
--- a/homeassistant/components/anova/entity.py
+++ b/homeassistant/components/anova/entity.py
@@ -18,6 +18,11 @@ def __init__(self, coordinator: AnovaCoordinator) -> None:
         self.device = coordinator.anova_device
         self._attr_device_info = coordinator.device_info
 
+    @property
+    def available(self) -> bool:
+        """Return if entity is available."""
+        return self.coordinator.data is not None and super().available
+
 
 class AnovaDescriptionEntity(AnovaEntity):
     """Defines an Anova entity that uses a description."""
diff --git a/homeassistant/components/anova/manifest.json b/homeassistant/components/anova/manifest.json
index 7c4509e2f2567c..40148a8666c874 100644
--- a/homeassistant/components/anova/manifest.json
+++ b/homeassistant/components/anova/manifest.json
@@ -4,7 +4,7 @@
   "codeowners": ["@Lash-L"],
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/anova",
-  "iot_class": "cloud_polling",
+  "iot_class": "cloud_push",
   "loggers": ["anova_wifi"],
-  "requirements": ["anova-wifi==0.10.0"]
+  "requirements": ["anova-wifi==0.11.6"]
 }
diff --git a/homeassistant/components/anova/models.py b/homeassistant/components/anova/models.py
index a63355b2bbd29b..8a3d11e828e007 100644
--- a/homeassistant/components/anova/models.py
+++ b/homeassistant/components/anova/models.py
@@ -1,7 +1,7 @@
 """Dataclass models for the Anova integration."""
 from dataclasses import dataclass
 
-from anova_wifi import AnovaPrecisionCooker
+from anova_wifi import AnovaApi
 
 from .coordinator import AnovaCoordinator
 
@@ -11,5 +11,5 @@ class AnovaData:
     """Data for the Anova integration."""
 
     api_jwt: str
-    precision_cookers: list[AnovaPrecisionCooker]
     coordinators: list[AnovaCoordinator]
+    api: AnovaApi
diff --git a/homeassistant/components/anova/sensor.py b/homeassistant/components/anova/sensor.py
index 24bda4dbed6774..2c149c1d48b403 100644
--- a/homeassistant/components/anova/sensor.py
+++ b/homeassistant/components/anova/sensor.py
@@ -4,7 +4,7 @@
 from collections.abc import Callable
 from dataclasses import dataclass
 
-from anova_wifi import APCUpdateSensor
+from anova_wifi import AnovaMode, AnovaState, APCUpdateSensor
 
 from homeassistant import config_entries
 from homeassistant.components.sensor import (
@@ -19,6 +19,7 @@
 from homeassistant.helpers.typing import StateType
 
 from .const import DOMAIN
+from .coordinator import AnovaCoordinator
 from .entity import AnovaDescriptionEntity
 from .models import AnovaData
 
@@ -27,7 +28,7 @@
 class AnovaSensorEntityDescriptionMixin:
     """Describes the mixin variables for anova sensors."""
 
-    value_fn: Callable[[APCUpdateSensor], float | int | str]
+    value_fn: Callable[[APCUpdateSensor], StateType]
 
 
 @dataclass(frozen=True)
@@ -37,7 +38,7 @@ class AnovaSensorEntityDescription(
     """Describes a Anova sensor."""
 
 
-SENSOR_DESCRIPTIONS: list[SensorEntityDescription] = [
+SENSOR_DESCRIPTIONS: list[AnovaSensorEntityDescription] = [
     AnovaSensorEntityDescription(
         key="cook_time",
         state_class=SensorStateClass.TOTAL_INCREASING,
@@ -49,11 +50,15 @@ class AnovaSensorEntityDescription(
     AnovaSensorEntityDescription(
         key="state",
         translation_key="state",
+        device_class=SensorDeviceClass.ENUM,
+        options=[state.name for state in AnovaState],
         value_fn=lambda data: data.state,
     ),
     AnovaSensorEntityDescription(
         key="mode",
         translation_key="mode",
+        device_class=SensorDeviceClass.ENUM,
+        options=[mode.name for mode in AnovaMode],
         value_fn=lambda data: data.mode,
     ),
     AnovaSensorEntityDescription(
@@ -105,11 +110,30 @@ async def async_setup_entry(
 ) -> None:
     """Set up Anova device."""
     anova_data: AnovaData = hass.data[DOMAIN][entry.entry_id]
-    async_add_entities(
-        AnovaSensor(coordinator, description)
-        for coordinator in anova_data.coordinators
-        for description in SENSOR_DESCRIPTIONS
-    )
+
+    for coordinator in anova_data.coordinators:
+        setup_coordinator(coordinator, async_add_entities)
+
+
+def setup_coordinator(
+    coordinator: AnovaCoordinator,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up an individual Anova Coordinator."""
+
+    def _async_sensor_listener() -> None:
+        """Listen for new sensor data and add sensors if they did not exist."""
+        if not coordinator.sensor_data_set:
+            valid_entities: set[AnovaSensor] = set()
+            for description in SENSOR_DESCRIPTIONS:
+                if description.value_fn(coordinator.data.sensor) is not None:
+                    valid_entities.add(AnovaSensor(coordinator, description))
+            async_add_entities(valid_entities)
+            coordinator.sensor_data_set = True
+
+    if coordinator.data is not None:
+        _async_sensor_listener()
+    coordinator.async_add_listener(_async_sensor_listener)
 
 
 class AnovaSensor(AnovaDescriptionEntity, SensorEntity):
diff --git a/homeassistant/components/anova/strings.json b/homeassistant/components/anova/strings.json
index b7762732303087..bfe3a61282ecca 100644
--- a/homeassistant/components/anova/strings.json
+++ b/homeassistant/components/anova/strings.json
@@ -11,13 +11,9 @@
         "description": "[%key:common::config_flow::description::confirm_setup%]"
       }
     },
-    "abort": {
-      "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
-    },
     "error": {
       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
-      "unknown": "[%key:common::config_flow::error::unknown%]",
-      "no_devices_found": "No devices were found. Make sure you have at least one Anova device online."
+      "unknown": "[%key:common::config_flow::error::unknown%]"
     }
   },
   "entity": {
@@ -26,10 +22,28 @@
         "name": "Cook time"
       },
       "state": {
-        "name": "State"
+        "name": "State",
+        "state": {
+          "preheating": "Preheating",
+          "cooking": "Cooking",
+          "maintaining": "Maintaining",
+          "timer_expired": "Timer expired",
+          "set_timer": "Set timer",
+          "no_state": "No state"
+        }
       },
       "mode": {
-        "name": "[%key:common::config_flow::data::mode%]"
+        "name": "[%key:common::config_flow::data::mode%]",
+        "state": {
+          "startup": "Startup",
+          "idle": "[%key:common::state::idle%]",
+          "cook": "Cooking",
+          "low_water": "Low water",
+          "ota": "Ota",
+          "provisioning": "Provisioning",
+          "high_temp": "High temperature",
+          "device_failure": "Device failure"
+        }
       },
       "target_temperature": {
         "name": "Target temperature"
diff --git a/homeassistant/components/anova/util.py b/homeassistant/components/anova/util.py
deleted file mode 100644
index 10e8fa0fef9bf3..00000000000000
--- a/homeassistant/components/anova/util.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Anova utilities."""
-
-from anova_wifi import AnovaPrecisionCooker
-
-
-def serialize_device_list(devices: list[AnovaPrecisionCooker]) -> list[tuple[str, str]]:
-    """Turn the device list into a serializable list that can be reconstructed."""
-    return [(device.device_key, device.type) for device in devices]
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index c49882f4394515..4e11e509507882 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -295,7 +295,7 @@
       "name": "Anova",
       "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "cloud_polling"
+      "iot_class": "cloud_push"
     },
     "anthemav": {
       "name": "Anthem A/V Receivers",
diff --git a/requirements_all.txt b/requirements_all.txt
index a501710a5184f6..257d891836345c 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -440,7 +440,7 @@ androidtvremote2==0.0.14
 anel-pwrctrl-homeassistant==0.0.1.dev2
 
 # homeassistant.components.anova
-anova-wifi==0.10.0
+anova-wifi==0.11.6
 
 # homeassistant.components.anthemav
 anthemav==1.4.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index b64fcb944eb181..d390b4e6661494 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -404,7 +404,7 @@ androidtv[async]==0.0.73
 androidtvremote2==0.0.14
 
 # homeassistant.components.anova
-anova-wifi==0.10.0
+anova-wifi==0.11.6
 
 # homeassistant.components.anthemav
 anthemav==1.4.1
diff --git a/tests/components/anova/__init__.py b/tests/components/anova/__init__.py
index aa58ee5bbb5b5b..c0fd036e61f144 100644
--- a/tests/components/anova/__init__.py
+++ b/tests/components/anova/__init__.py
@@ -3,7 +3,7 @@
 
 from unittest.mock import patch
 
-from anova_wifi import AnovaPrecisionCooker, APCUpdate, APCUpdateBinary, APCUpdateSensor
+from anova_wifi import APCUpdate, APCUpdateBinary, APCUpdateSensor
 
 from homeassistant.components.anova.const import DOMAIN
 from homeassistant.config_entries import ConfigEntry
@@ -20,7 +20,7 @@
     sensor=APCUpdateSensor(
         0, "Low water", "No state", 23.33, 0, "2.2.0", 20.87, 21.79, 21.33
     ),
-    binary_sensor=APCUpdateBinary(False, False, False, True, False, True, False),
+    binary_sensor=APCUpdateBinary(False, False, False, True, False, True, False, False),
 )
 
 
@@ -32,9 +32,9 @@ def create_entry(hass: HomeAssistant, device_id: str = DEVICE_UNIQUE_ID) -> Conf
         data={
             CONF_USERNAME: "sample@gmail.com",
             CONF_PASSWORD: "sample",
-            "devices": [(device_id, "type_sample")],
         },
         unique_id="sample@gmail.com",
+        version=2,
     )
     entry.add_to_hass(hass)
     return entry
@@ -43,21 +43,10 @@ def create_entry(hass: HomeAssistant, device_id: str = DEVICE_UNIQUE_ID) -> Conf
 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)
-        ]
 
+    with patch("homeassistant.components.anova.AnovaApi.authenticate"):
         entry = create_entry(hass)
 
         if not skip_setup:
diff --git a/tests/components/anova/conftest.py b/tests/components/anova/conftest.py
index 34f713502dda8f..de27efd166e6ec 100644
--- a/tests/components/anova/conftest.py
+++ b/tests/components/anova/conftest.py
@@ -1,36 +1,23 @@
 """Common fixtures for Anova."""
-from unittest.mock import AsyncMock, patch
+from unittest.mock import patch
 
-from anova_wifi import AnovaApi, AnovaPrecisionCooker, InvalidLogin, NoDevicesFound
+from anova_wifi import AnovaApi, InvalidLogin
+from anova_wifi.mocks.anova_api import anova_api_mock
 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
+    api_mock = anova_api_mock()
 
-    with patch("homeassistant.components.anova.AnovaApi", return_value=api_mock):
+    with patch("homeassistant.components.anova.AnovaApi", return_value=api_mock), patch(
+        "homeassistant.components.anova.config_flow.AnovaApi", return_value=api_mock
+    ):
         api = AnovaApi(
             None,
             "sample@gmail.com",
@@ -44,18 +31,11 @@ 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 = anova_api_mock(connect_messages=[], post_connect_messages=[])
 
-    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):
+    with patch("homeassistant.components.anova.AnovaApi", return_value=api_mock), patch(
+        "homeassistant.components.anova.config_flow.AnovaApi", return_value=api_mock
+    ):
         api = AnovaApi(
             None,
             "sample@gmail.com",
@@ -69,7 +49,7 @@ async def anova_api_wrong_login(
     hass: HomeAssistant,
 ) -> AnovaApi:
     """Mock the api for Anova with a wrong login."""
-    api_mock = AsyncMock()
+    api_mock = anova_api_mock()
 
     async def authenticate_side_effect():
         raise InvalidLogin()
@@ -83,3 +63,19 @@ async def authenticate_side_effect():
             "sample",
         )
         yield api
+
+
+@pytest.fixture
+async def anova_api_no_data(
+    hass: HomeAssistant,
+) -> AnovaApi:
+    """Mock the api for Anova with a wrong login."""
+    api_mock = anova_api_mock(post_connect_messages=[])
+
+    with patch("homeassistant.components.anova.AnovaApi", return_value=api_mock):
+        api = AnovaApi(
+            None,
+            "sample@gmail.com",
+            "sample",
+        )
+        yield api
diff --git a/tests/components/anova/test_config_flow.py b/tests/components/anova/test_config_flow.py
index d1255876137c05..105cebf9bf4955 100644
--- a/tests/components/anova/test_config_flow.py
+++ b/tests/components/anova/test_config_flow.py
@@ -2,79 +2,31 @@
 
 from unittest.mock import patch
 
-from anova_wifi import AnovaPrecisionCooker, InvalidLogin, NoDevicesFound
+from anova_wifi import AnovaApi, InvalidLogin
 
 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
+from . import CONF_INPUT
 
 
-async def test_flow_user(
-    hass: HomeAssistant,
-) -> None:
+async def test_flow_user(hass: HomeAssistant, anova_api: AnovaApi) -> 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"
+    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",
+    }
 
 
 async def test_flow_wrong_login(hass: HomeAssistant) -> None:
@@ -111,23 +63,3 @@ async def test_flow_unknown_error(hass: HomeAssistant) -> None:
         )
         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"}
diff --git a/tests/components/anova/test_init.py b/tests/components/anova/test_init.py
index 631a69e103b9c8..9ecde02aed3031 100644
--- a/tests/components/anova/test_init.py
+++ b/tests/components/anova/test_init.py
@@ -1,15 +1,17 @@
 """Test init for Anova."""
 
-from unittest.mock import patch
 
 from anova_wifi import AnovaApi
 
+from homeassistant import config_entries
 from homeassistant.components.anova import DOMAIN
 from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
 from homeassistant.core import HomeAssistant
 
-from . import ONLINE_UPDATE, async_init_integration, create_entry
+from . import async_init_integration, create_entry
+
+from tests.common import MockConfigEntry
 
 
 async def test_async_setup_entry(hass: HomeAssistant, anova_api: AnovaApi) -> None:
@@ -17,8 +19,7 @@ async def test_async_setup_entry(hass: HomeAssistant, anova_api: AnovaApi) -> No
     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"
+    assert state.state == "idle"
 
 
 async def test_wrong_login(
@@ -30,37 +31,6 @@ async def test_wrong_login(
     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)
@@ -72,3 +42,34 @@ async def test_unload_entry(hass: HomeAssistant, anova_api: AnovaApi) -> None:
     await hass.async_block_till_done()
 
     assert entry.state is ConfigEntryState.NOT_LOADED
+
+
+async def test_no_devices_found(
+    hass: HomeAssistant, anova_api_no_devices: AnovaApi
+) -> None:
+    """Test when there don't seem to be any devices on the account."""
+    entry = await async_init_integration(hass)
+    assert entry.state is ConfigEntryState.LOADED
+    # Config flow should be loaded - but we shouldn't have our entities.
+    assert hass.states.get("sensor.anova_precision_cooker_mode") is None
+
+
+async def test_migrate_entry(hass: HomeAssistant, anova_api: AnovaApi) -> None:
+    """Test the migration of the config flow."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        title="Anova",
+        data={
+            CONF_USERNAME: "sample@gmail.com",
+            CONF_PASSWORD: "sample",
+            "devices": [("random_id", "type_sample")],
+        },
+        unique_id="sample@gmail.com",
+        version=1,
+    )
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    assert entry.state == config_entries.ConfigEntryState.LOADED
+    assert entry.version == 2
diff --git a/tests/components/anova/test_sensor.py b/tests/components/anova/test_sensor.py
index 0ce5c7a4d0a546..a60f87c56a011f 100644
--- a/tests/components/anova/test_sensor.py
+++ b/tests/components/anova/test_sensor.py
@@ -1,19 +1,13 @@
 """Test the Anova sensors."""
 
-from datetime import timedelta
 import logging
-from unittest.mock import patch
 
-from anova_wifi import AnovaApi, AnovaOffline
+from anova_wifi import AnovaApi
 
-from homeassistant.const import STATE_UNAVAILABLE
 from homeassistant.core import HomeAssistant
-from homeassistant.util import dt as dt_util
 
 from . import async_init_integration
 
-from tests.common import async_fire_time_changed
-
 LOGGER = logging.getLogger(__name__)
 
 
@@ -28,34 +22,25 @@ async def test_sensors(hass: HomeAssistant, anova_api: AnovaApi) -> None:
     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"
+        == "22.37"
     )
-    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_mode").state == "idle"
+    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"
+        == "54.72"
     )
     assert (
         hass.states.get("sensor.anova_precision_cooker_water_temperature").state
-        == "21.33"
+        == "18.33"
     )
     assert (
         hass.states.get("sensor.anova_precision_cooker_triac_temperature").state
-        == "21.79"
+        == "36.04"
     )
 
 
-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."""
+async def test_no_data_sensors(hass: HomeAssistant, anova_api_no_data: AnovaApi):
+    """Test that if we have no data for the device, and we have not set it up previously, It is not immediately set up."""
     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_util.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
+    assert hass.states.get("sensor.anova_precision_cooker_triac_temperature") is None


Code review comments:
diff_hunk: @@ -35,36 +34,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         )
         return False
     assert api.jwt
-    api.existing_devices = [
-        AnovaPrecisionCooker(
-            aiohttp_client.async_get_clientsession(hass),
-            device[0],
-            device[1],
-            api.jwt,
-        )
-        for device in entry.data[CONF_DEVICES]
-    ]
     try:
-        new_devices = await api.get_devices()
+        await api.create_websocket()
     except NoDevicesFound:
-        # get_devices raises an exception if no devices are online
-        new_devices = []
-    devices = api.existing_devices
-    if new_devices:
-        hass.config_entries.async_update_entry(
-            entry,
-            data={
-                **entry.data,
-                **{CONF_DEVICES: serialize_device_list(devices)},
-            },
+        _LOGGER.warning(
+            "No devices were found on the websocket, perhaps you don't have any devices on this account?"
         )
-    coordinators = [AnovaCoordinator(hass, device) for device in devices]
-    for coordinator in coordinators:
-        await coordinator.async_config_entry_first_refresh()
-        firmware_version = coordinator.data.sensor.firmware_version
-        coordinator.async_setup(str(firmware_version))
+    assert api.websocket_handler
path: homeassistant/components/anova/__init__.py
text: @user1:
We should avoid `assert` in production code as they can be disabled by optimizations.
Is there a possibility that `websocket_handler` is None?

@author:
There is not a possibility it is None at this point, but I have changed it to be an if statement instead.