-
-
Notifications
You must be signed in to change notification settings - Fork 29.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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>
- Loading branch information
Showing
22 changed files
with
884 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
"""The Anova integration.""" | ||
from __future__ import annotations | ||
|
||
import logging | ||
|
||
from anova_wifi import ( | ||
AnovaApi, | ||
AnovaPrecisionCooker, | ||
AnovaPrecisionCookerSensor, | ||
InvalidLogin, | ||
NoDevicesFound, | ||
) | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
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] | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up Anova from a config entry.""" | ||
api = AnovaApi( | ||
aiohttp_client.async_get_clientsession(hass), | ||
entry.data[CONF_USERNAME], | ||
entry.data[CONF_PASSWORD], | ||
) | ||
try: | ||
await api.authenticate() | ||
except InvalidLogin as err: | ||
_LOGGER.error( | ||
"Login was incorrect - please log back in through the config flow. %s", err | ||
) | ||
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["devices"] | ||
] | ||
try: | ||
new_devices = await api.get_devices() | ||
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, | ||
**{"devices": serialize_device_list(devices)}, | ||
}, | ||
) | ||
coordinators = [AnovaCoordinator(hass, device) for device in devices] | ||
for coordinator in coordinators: | ||
await coordinator.async_config_entry_first_refresh() | ||
firmware_version = coordinator.data["sensors"][ | ||
AnovaPrecisionCookerSensor.FIRMWARE_VERSION | ||
] | ||
coordinator.async_setup(str(firmware_version)) | ||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AnovaData( | ||
api_jwt=api.jwt, precision_cookers=devices, coordinators=coordinators | ||
) | ||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
return True | ||
|
||
|
||
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) | ||
|
||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
"""Config flow for Anova.""" | ||
from __future__ import annotations | ||
|
||
from anova_wifi import AnovaApi, InvalidLogin, NoDevicesFound | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME | ||
from homeassistant.data_entry_flow import FlowResult | ||
from homeassistant.helpers import aiohttp_client | ||
|
||
from .const import DOMAIN | ||
from .util import serialize_device_list | ||
|
||
|
||
class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Sets up a config flow for Anova.""" | ||
|
||
VERSION = 1 | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, str] | None = None | ||
) -> FlowResult: | ||
"""Handle a flow initiated by the user.""" | ||
errors: dict[str, str] = {} | ||
if user_input is not None: | ||
api = AnovaApi( | ||
aiohttp_client.async_get_clientsession(self.hass), | ||
user_input[CONF_USERNAME], | ||
user_input[CONF_PASSWORD], | ||
) | ||
await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) | ||
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, | ||
"devices": device_list, | ||
}, | ||
) | ||
|
||
return self.async_show_form( | ||
step_id="user", | ||
data_schema=vol.Schema( | ||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} | ||
), | ||
errors=errors, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
"""Constants for the Anova integration.""" | ||
|
||
DOMAIN = "anova" | ||
|
||
ANOVA_CLIENT = "anova_api_client" | ||
ANOVA_FIRMWARE_VERSION = "anova_firmware_version" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
"""Support for Anova Coordinators.""" | ||
from datetime import timedelta | ||
import logging | ||
|
||
from anova_wifi import AnovaOffline, AnovaPrecisionCooker | ||
import async_timeout | ||
|
||
from homeassistant.core import HomeAssistant, callback | ||
from homeassistant.helpers.entity import DeviceInfo | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class AnovaCoordinator(DataUpdateCoordinator): | ||
"""Anova custom coordinator.""" | ||
|
||
data: dict[str, dict[str, str | int | float]] | ||
|
||
def __init__( | ||
self, | ||
hass: HomeAssistant, | ||
anova_device: AnovaPrecisionCooker, | ||
) -> 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.anova_device = anova_device | ||
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) -> dict[str, dict[str, str | int | float]]: | ||
try: | ||
async with async_timeout.timeout(5): | ||
return await self.anova_device.update() | ||
except AnovaOffline as err: | ||
raise UpdateFailed(err) from err |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
"""Base entity for the Anova integration.""" | ||
from __future__ import annotations | ||
|
||
from homeassistant.helpers.entity import Entity, EntityDescription | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from .coordinator import AnovaCoordinator | ||
|
||
|
||
class AnovaEntity(CoordinatorEntity[AnovaCoordinator], Entity): | ||
"""Defines a Anova entity.""" | ||
|
||
def __init__(self, coordinator: AnovaCoordinator) -> None: | ||
"""Initialize the Anova entity.""" | ||
super().__init__(coordinator) | ||
self.device = coordinator.anova_device | ||
self._attr_device_info = coordinator.device_info | ||
self._attr_has_entity_name = True | ||
|
||
|
||
class AnovaDescriptionEntity(AnovaEntity, Entity): | ||
"""Defines a Anova entity that uses a description.""" | ||
|
||
def __init__( | ||
self, coordinator: AnovaCoordinator, description: EntityDescription | ||
) -> None: | ||
"""Initialize the entity and declare unique id based on description key.""" | ||
super().__init__(coordinator) | ||
self.entity_description = description | ||
self._attr_unique_id = f"{coordinator._device_unique_id}_{description.key}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"domain": "anova", | ||
"name": "Anova", | ||
"codeowners": ["@Lash-L"], | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/anova", | ||
"iot_class": "cloud_polling", | ||
"loggers": ["anova_wifi"], | ||
"requirements": ["anova-wifi==0.8.0"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
"""Dataclass models for the Anova integration.""" | ||
from dataclasses import dataclass | ||
|
||
from anova_wifi import AnovaPrecisionCooker | ||
|
||
from .coordinator import AnovaCoordinator | ||
|
||
|
||
@dataclass | ||
class AnovaData: | ||
"""Data for the Anova integration.""" | ||
|
||
api_jwt: str | ||
precision_cookers: list[AnovaPrecisionCooker] | ||
coordinators: list[AnovaCoordinator] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
"""Support for Anova Sensors.""" | ||
from __future__ import annotations | ||
|
||
from anova_wifi import AnovaPrecisionCookerSensor | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.components.sensor import ( | ||
SensorDeviceClass, | ||
SensorEntity, | ||
SensorEntityDescription, | ||
SensorStateClass, | ||
) | ||
from homeassistant.const import UnitOfTemperature, UnitOfTime | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
from homeassistant.helpers.typing import StateType | ||
|
||
from .const import DOMAIN | ||
from .entity import AnovaDescriptionEntity | ||
from .models import AnovaData | ||
|
||
SENSOR_DESCRIPTIONS: list[SensorEntityDescription] = [ | ||
SensorEntityDescription( | ||
key=AnovaPrecisionCookerSensor.COOK_TIME, | ||
state_class=SensorStateClass.TOTAL_INCREASING, | ||
native_unit_of_measurement=UnitOfTime.SECONDS, | ||
icon="mdi:clock-outline", | ||
translation_key="cook_time", | ||
), | ||
SensorEntityDescription( | ||
key=AnovaPrecisionCookerSensor.STATE, translation_key="state" | ||
), | ||
SensorEntityDescription( | ||
key=AnovaPrecisionCookerSensor.MODE, translation_key="mode" | ||
), | ||
SensorEntityDescription( | ||
key=AnovaPrecisionCookerSensor.TARGET_TEMPERATURE, | ||
native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
device_class=SensorDeviceClass.TEMPERATURE, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
icon="mdi:thermometer", | ||
translation_key="target_temperature", | ||
), | ||
SensorEntityDescription( | ||
key=AnovaPrecisionCookerSensor.COOK_TIME_REMAINING, | ||
native_unit_of_measurement=UnitOfTime.SECONDS, | ||
icon="mdi:clock-outline", | ||
translation_key="cook_time_remaining", | ||
), | ||
SensorEntityDescription( | ||
key=AnovaPrecisionCookerSensor.HEATER_TEMPERATURE, | ||
native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
device_class=SensorDeviceClass.TEMPERATURE, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
icon="mdi:thermometer", | ||
translation_key="heater_temperature", | ||
), | ||
SensorEntityDescription( | ||
key=AnovaPrecisionCookerSensor.TRIAC_TEMPERATURE, | ||
native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
device_class=SensorDeviceClass.TEMPERATURE, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
icon="mdi:thermometer", | ||
translation_key="triac_temperature", | ||
), | ||
SensorEntityDescription( | ||
key=AnovaPrecisionCookerSensor.WATER_TEMPERATURE, | ||
native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
device_class=SensorDeviceClass.TEMPERATURE, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
icon="mdi:thermometer", | ||
translation_key="water_temperature", | ||
), | ||
] | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, | ||
entry: config_entries.ConfigEntry, | ||
async_add_entities: AddEntitiesCallback, | ||
) -> 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 | ||
) | ||
|
||
|
||
class AnovaSensor(AnovaDescriptionEntity, SensorEntity): | ||
"""A sensor using Anova coordinator.""" | ||
|
||
@property | ||
def native_value(self) -> StateType: | ||
"""Return the state.""" | ||
return self.coordinator.data["sensors"][self.entity_description.key] |
Oops, something went wrong.