Bump fyta_cli to 0.6.0 (#123816)

* Bump fyta_cli to 0.5.1

* Code adjustments to enable strit typing

* Update homeassistant/components/fyta/__init__.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update diagnostics

* Update config_flow + init (ruff)

* Update sensor

* Update coordinator

* Update homeassistant/components/fyta/diagnostics.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/diagnostics.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Set one ph sensor to null/none

* Update sensor

* Clean-up (ruff)

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/124072/head
dontinelli 2024-08-16 17:46:37 +02:00 committed by GitHub
parent e07768412a
commit 8a110abc82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1279 additions and 76 deletions

View File

@ -197,6 +197,7 @@ homeassistant.components.fritzbox_callmonitor.*
homeassistant.components.fronius.*
homeassistant.components.frontend.*
homeassistant.components.fully_kiosk.*
homeassistant.components.fyta.*
homeassistant.components.generic_hygrostat.*
homeassistant.components.generic_thermostat.*
homeassistant.components.geo_location.*

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from datetime import datetime
import logging
from typing import Any
from fyta_cli.fyta_connector import FytaConnector
@ -73,11 +72,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
fyta = FytaConnector(
config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD]
)
credentials: dict[str, Any] = await fyta.login()
credentials = await fyta.login()
await fyta.client.close()
new[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN]
new[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat()
new[CONF_ACCESS_TOKEN] = credentials.access_token
new[CONF_EXPIRATION] = credentials.expiration.isoformat()
hass.config_entries.async_update_entry(
config_entry, data=new, minor_version=2, version=1

View File

@ -12,10 +12,11 @@ from fyta_cli.fyta_exceptions import (
FytaConnectionError,
FytaPasswordError,
)
from fyta_cli.fyta_models import Credentials
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
@ -49,14 +50,11 @@ DATA_SCHEMA = vol.Schema(
class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fyta."""
credentials: Credentials
_entry: FytaConfigEntry | None = None
VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""Initialize FytaConfigFlow."""
self.credentials: dict[str, Any] = {}
self._entry: FytaConfigEntry | None = None
async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]:
"""Reusable Auth Helper."""
fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
@ -75,10 +73,6 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
finally:
await fyta.client.close()
self.credentials[CONF_EXPIRATION] = self.credentials[
CONF_EXPIRATION
].isoformat()
return {}
async def async_step_user(
@ -90,7 +84,10 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]})
if not (errors := await self.async_auth(user_input)):
user_input |= self.credentials
user_input |= {
CONF_ACCESS_TOKEN: self.credentials.access_token,
CONF_EXPIRATION: self.credentials.expiration.isoformat(),
}
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
@ -114,7 +111,10 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
assert self._entry is not None
if user_input and not (errors := await self.async_auth(user_input)):
user_input |= self.credentials
user_input |= {
CONF_ACCESS_TOKEN: self.credentials.access_token,
CONF_EXPIRATION: self.credentials.expiration.isoformat(),
}
return self.async_update_reload_and_abort(
self._entry, data={**self._entry.data, **user_input}
)

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
from fyta_cli.fyta_connector import FytaConnector
from fyta_cli.fyta_exceptions import (
@ -13,6 +13,7 @@ from fyta_cli.fyta_exceptions import (
FytaPasswordError,
FytaPlantError,
)
from fyta_cli.fyta_models import Plant
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
@ -27,7 +28,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]):
class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
"""Fyta custom coordinator."""
config_entry: FytaConfigEntry
@ -44,7 +45,7 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]):
async def _async_update_data(
self,
) -> dict[int, dict[str, Any]]:
) -> dict[int, Plant]:
"""Fetch data from API endpoint."""
if (
@ -60,7 +61,6 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]):
async def renew_authentication(self) -> bool:
"""Renew access token for FYTA API."""
credentials: dict[str, Any] = {}
try:
credentials = await self.fyta.login()
@ -70,8 +70,8 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]):
raise ConfigEntryAuthFailed from ex
new_config_entry = {**self.config_entry.data}
new_config_entry[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN]
new_config_entry[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat()
new_config_entry[CONF_ACCESS_TOKEN] = credentials.access_token
new_config_entry[CONF_EXPIRATION] = credentials.expiration.isoformat()
self.hass.config_entries.async_update_entry(
self.config_entry, data=new_config_entry

View File

@ -25,5 +25,5 @@ async def async_get_config_entry_diagnostics(
return {
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
"plant_data": data,
"plant_data": {key: value.to_dict() for key, value in data.items()},
}

View File

@ -1,6 +1,6 @@
"""Entities for FYTA integration."""
from typing import Any
from fyta_cli.fyta_models import Plant
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
@ -32,13 +32,13 @@ class FytaPlantEntity(CoordinatorEntity[FytaCoordinator]):
manufacturer="Fyta",
model="Plant",
identifiers={(DOMAIN, f"{entry.entry_id}-{plant_id}")},
name=self.plant.get("name"),
sw_version=self.plant.get("sw_version"),
name=self.plant.name,
sw_version=self.plant.sw_version,
)
self.entity_description = description
@property
def plant(self) -> dict[str, Any]:
def plant(self) -> Plant:
"""Get plant data."""
return self.coordinator.data[self.plant_id]

View File

@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["fyta_cli==0.4.1"]
"requirements": ["fyta_cli==0.6.0"]
}

View File

@ -7,7 +7,7 @@ from dataclasses import dataclass
from datetime import datetime
from typing import Final
from fyta_cli.fyta_connector import PLANT_MEASUREMENT_STATUS, PLANT_STATUS
from fyta_cli.fyta_models import Plant
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -23,19 +23,18 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import FytaConfigEntry
from .coordinator import FytaCoordinator
from .entity import FytaPlantEntity
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class FytaSensorEntityDescription(SensorEntityDescription):
"""Describes Fyta sensor entity."""
value_fn: Callable[[str | int | float | datetime], str | int | float | datetime] = (
lambda value: value
)
value_fn: Callable[[Plant], StateType | datetime]
PLANT_STATUS_LIST: list[str] = ["deleted", "doing_great", "need_attention", "no_sensor"]
@ -48,63 +47,68 @@ PLANT_MEASUREMENT_STATUS_LIST: list[str] = [
"too_high",
]
SENSORS: Final[list[FytaSensorEntityDescription]] = [
FytaSensorEntityDescription(
key="scientific_name",
translation_key="scientific_name",
value_fn=lambda plant: plant.scientific_name,
),
FytaSensorEntityDescription(
key="status",
translation_key="plant_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=PLANT_STATUS.get,
value_fn=lambda plant: plant.status.name.lower(),
),
FytaSensorEntityDescription(
key="temperature_status",
translation_key="temperature_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_MEASUREMENT_STATUS_LIST,
value_fn=PLANT_MEASUREMENT_STATUS.get,
value_fn=lambda plant: plant.temperature_status.name.lower(),
),
FytaSensorEntityDescription(
key="light_status",
translation_key="light_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_MEASUREMENT_STATUS_LIST,
value_fn=PLANT_MEASUREMENT_STATUS.get,
value_fn=lambda plant: plant.light_status.name.lower(),
),
FytaSensorEntityDescription(
key="moisture_status",
translation_key="moisture_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_MEASUREMENT_STATUS_LIST,
value_fn=PLANT_MEASUREMENT_STATUS.get,
value_fn=lambda plant: plant.moisture_status.name.lower(),
),
FytaSensorEntityDescription(
key="salinity_status",
translation_key="salinity_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_MEASUREMENT_STATUS_LIST,
value_fn=PLANT_MEASUREMENT_STATUS.get,
value_fn=lambda plant: plant.salinity_status.name.lower(),
),
FytaSensorEntityDescription(
key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda plant: plant.temperature,
),
FytaSensorEntityDescription(
key="light",
translation_key="light",
native_unit_of_measurement="μmol/s⋅m²",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda plant: plant.light,
),
FytaSensorEntityDescription(
key="moisture",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.MOISTURE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda plant: plant.moisture,
),
FytaSensorEntityDescription(
key="salinity",
@ -112,11 +116,13 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [
native_unit_of_measurement=UnitOfConductivity.MILLISIEMENS,
device_class=SensorDeviceClass.CONDUCTIVITY,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda plant: plant.salinity,
),
FytaSensorEntityDescription(
key="ph",
device_class=SensorDeviceClass.PH,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda plant: plant.ph,
),
FytaSensorEntityDescription(
key="battery_level",
@ -124,6 +130,7 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda plant: plant.battery_level,
),
]
@ -138,7 +145,7 @@ async def async_setup_entry(
FytaPlantSensor(coordinator, entry, sensor, plant_id)
for plant_id in coordinator.fyta.plant_list
for sensor in SENSORS
if sensor.key in coordinator.data[plant_id]
if sensor.key in dir(coordinator.data[plant_id])
]
async_add_entities(plant_entities)
@ -150,8 +157,7 @@ class FytaPlantSensor(FytaPlantEntity, SensorEntity):
entity_description: FytaSensorEntityDescription
@property
def native_value(self) -> str | int | float | datetime:
def native_value(self) -> StateType | datetime:
"""Return the state for this sensor."""
val = self.plant[self.entity_description.key]
return self.entity_description.value_fn(val)
return self.entity_description.value_fn(self.plant)

View File

@ -1726,6 +1726,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.fyta.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.generic_hygrostat.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -921,7 +921,7 @@ freesms==0.2.0
fritzconnection[qr]==1.13.2
# homeassistant.components.fyta
fyta_cli==0.4.1
fyta_cli==0.6.0
# homeassistant.components.google_translate
gTTS==2.2.4

View File

@ -774,7 +774,7 @@ freebox-api==1.1.0
fritzconnection[qr]==1.13.2
# homeassistant.components.fyta
fyta_cli==0.4.1
fyta_cli==0.6.0
# homeassistant.components.google_translate
gTTS==2.2.4

View File

@ -4,6 +4,7 @@ from collections.abc import Generator
from datetime import UTC, datetime
from unittest.mock import AsyncMock, patch
from fyta_cli.fyta_models import Credentials, Plant
import pytest
from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN as FYTA_DOMAIN
@ -35,23 +36,27 @@ def mock_config_entry() -> MockConfigEntry:
def mock_fyta_connector():
"""Build a fixture for the Fyta API that connects successfully and returns one device."""
plants: dict[int, Plant] = {
0: Plant.from_dict(load_json_object_fixture("plant_status1.json", FYTA_DOMAIN)),
1: Plant.from_dict(load_json_object_fixture("plant_status2.json", FYTA_DOMAIN)),
}
mock_fyta_connector = AsyncMock()
mock_fyta_connector.expiration = datetime.fromisoformat(EXPIRATION).replace(
tzinfo=UTC
)
mock_fyta_connector.client = AsyncMock(autospec=True)
mock_fyta_connector.update_all_plants.return_value = load_json_object_fixture(
"plant_status.json", FYTA_DOMAIN
)
mock_fyta_connector.plant_list = load_json_object_fixture(
"plant_list.json", FYTA_DOMAIN
)
mock_fyta_connector.update_all_plants.return_value = plants
mock_fyta_connector.plant_list = {
0: "Gummibaum",
1: "Kakaobaum",
}
mock_fyta_connector.login = AsyncMock(
return_value={
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
CONF_EXPIRATION: datetime.fromisoformat(EXPIRATION).replace(tzinfo=UTC),
}
return_value=Credentials(
access_token=ACCESS_TOKEN,
expiration=datetime.fromisoformat(EXPIRATION).replace(tzinfo=UTC),
)
)
with (
patch(

View File

@ -1,4 +0,0 @@
{
"0": "Gummibaum",
"1": "Kakaobaum"
}

View File

@ -1,14 +0,0 @@
{
"0": {
"name": "Gummibaum",
"scientific_name": "Ficus elastica",
"status": 1,
"sw_version": "1.0"
},
"1": {
"name": "Kakaobaum",
"scientific_name": "Theobroma cacao",
"status": 2,
"sw_version": "1.0"
}
}

View File

@ -0,0 +1,23 @@
{
"battery_level": 80,
"battery_status": true,
"last_updated": "2023-01-10 10:10:00",
"light": 2,
"light_status": 3,
"nickname": "Gummibaum",
"moisture": 61,
"moisture_status": 3,
"sensor_available": true,
"sw_version": "1.0",
"status": 3,
"online": true,
"ph": null,
"plant_id": 0,
"plant_origin_path": "",
"plant_thumb_path": "",
"salinity": 1,
"salinity_status": 4,
"scientific_name": "Ficus elastica",
"temperature": 25.2,
"temperature_status": 3
}

View File

@ -0,0 +1,23 @@
{
"battery_level": 80,
"battery_status": true,
"last_updated": "2023-01-02 10:10:00",
"light": 2,
"light_status": 3,
"nickname": "Kakaobaum",
"moisture": 61,
"moisture_status": 3,
"sensor_available": true,
"sw_version": "1.0",
"status": 3,
"online": true,
"ph": 7,
"plant_id": 0,
"plant_origin_path": "",
"plant_thumb_path": "",
"salinity": 1,
"salinity_status": 4,
"scientific_name": "Theobroma cacao",
"temperature": 25.2,
"temperature_status": 3
}

View File

@ -23,16 +23,50 @@
}),
'plant_data': dict({
'0': dict({
'battery_level': 80.0,
'battery_status': True,
'last_updated': '2023-01-10T10:10:00',
'light': 2.0,
'light_status': 3,
'moisture': 61.0,
'moisture_status': 3,
'name': 'Gummibaum',
'online': True,
'ph': None,
'plant_id': 0,
'plant_origin_path': '',
'plant_thumb_path': '',
'salinity': 1.0,
'salinity_status': 4,
'scientific_name': 'Ficus elastica',
'status': 1,
'sensor_available': True,
'status': 3,
'sw_version': '1.0',
'temperature': 25.2,
'temperature_status': 3,
}),
'1': dict({
'battery_level': 80.0,
'battery_status': True,
'last_updated': '2023-01-02T10:10:00',
'light': 2.0,
'light_status': 3,
'moisture': 61.0,
'moisture_status': 3,
'name': 'Kakaobaum',
'online': True,
'ph': 7.0,
'plant_id': 0,
'plant_origin_path': '',
'plant_thumb_path': '',
'salinity': 1.0,
'salinity_status': 4,
'scientific_name': 'Theobroma cacao',
'status': 2,
'sensor_available': True,
'status': 3,
'sw_version': '1.0',
'temperature': 25.2,
'temperature_status': 3,
}),
}),
})

File diff suppressed because it is too large Load Diff