Add image entity for fyta (#135105)

pull/136162/head
dontinelli 2025-01-29 12:14:39 +01:00 committed by GitHub
parent 95c632e283
commit c486cc8cbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 337 additions and 6 deletions

View File

@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.IMAGE,
Platform.SENSOR,
]
type FytaConfigEntry = ConfigEntry[FytaCoordinator]

View File

@ -0,0 +1,64 @@
"""Entity for Fyta plant image."""
from __future__ import annotations
from datetime import datetime
from homeassistant.components.image import ImageEntity, ImageEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FytaConfigEntry
from .coordinator import FytaCoordinator
from .entity import FytaPlantEntity
async def async_setup_entry(
hass: HomeAssistant, entry: FytaConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the FYTA plant images."""
coordinator = entry.runtime_data
description = ImageEntityDescription(key="plant_image")
async_add_entities(
FytaPlantImageEntity(coordinator, entry, description, plant_id)
for plant_id in coordinator.fyta.plant_list
if plant_id in coordinator.data
)
def _async_add_new_device(plant_id: int) -> None:
async_add_entities(
[FytaPlantImageEntity(coordinator, entry, description, plant_id)]
)
coordinator.new_device_callbacks.append(_async_add_new_device)
class FytaPlantImageEntity(FytaPlantEntity, ImageEntity):
"""Represents a Fyta image."""
entity_description: ImageEntityDescription
def __init__(
self,
coordinator: FytaCoordinator,
entry: ConfigEntry,
description: ImageEntityDescription,
plant_id: int,
) -> None:
"""Initiatlize Fyta Image entity."""
super().__init__(coordinator, entry, description, plant_id)
ImageEntity.__init__(self, coordinator.hass)
self._attr_name = None
@property
def image_url(self) -> str:
"""Return the image_url for this sensor."""
image = self.plant.plant_origin_path
if image != self._attr_image_url:
self._attr_image_last_updated = datetime.now()
return image

View File

@ -81,3 +81,13 @@ def mock_setup_entry() -> Generator[AsyncMock]:
"homeassistant.components.fyta.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(autouse=True)
def mock_getrandbits():
"""Mock image access token which normally is randomized."""
with patch(
"homeassistant.components.image.SystemRandom.getrandbits",
return_value=1,
):
yield

View File

@ -19,8 +19,8 @@
"online": true,
"ph": null,
"plant_id": 0,
"plant_origin_path": "",
"plant_thumb_path": "",
"plant_origin_path": "http://www.plant_picture.com/picture",
"plant_thumb_path": "http://www.plant_picture.com/picture_thumb",
"is_productive_plant": false,
"salinity": 1,
"salinity_status": 4,

View File

@ -0,0 +1,30 @@
{
"battery_level": 80,
"fertilisation": {
"was_repotted": true
},
"low_battery": false,
"last_updated": "2023-01-10 10:10:00",
"light": 2,
"light_status": 3,
"nickname": "Gummibaum",
"nutrients_status": 3,
"moisture": 61,
"moisture_status": 3,
"sensor_available": true,
"sensor_id": "FD:1D:B7:E3:D0:E2",
"sensor_update_available": true,
"sw_version": "1.0",
"status": 1,
"online": true,
"ph": null,
"plant_id": 0,
"plant_origin_path": "http://www.plant_picture.com/picture1",
"plant_thumb_path": "http://www.plant_picture.com/picture_thumb",
"is_productive_plant": false,
"salinity": 1,
"salinity_status": 4,
"scientific_name": "Ficus elastica",
"temperature": 25.2,
"temperature_status": 3
}

View File

@ -19,8 +19,8 @@
"online": true,
"ph": 7,
"plant_id": 0,
"plant_origin_path": "",
"plant_thumb_path": "",
"plant_origin_path": "http://www.plant_picture.com/picture",
"plant_thumb_path": "http://www.plant_picture.com/picture_thumb",
"is_productive_plant": true,
"salinity": 1,
"salinity_status": 4,

View File

@ -43,8 +43,8 @@
'online': True,
'ph': None,
'plant_id': 0,
'plant_origin_path': '',
'plant_thumb_path': '',
'plant_origin_path': 'http://www.plant_picture.com/picture',
'plant_thumb_path': 'http://www.plant_picture.com/picture_thumb',
'productive_plant': False,
'repotted': True,
'salinity': 1.0,

View File

@ -0,0 +1,97 @@
# serializer version: 1
# name: test_all_entities[image.gummibaum-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'image',
'entity_category': None,
'entity_id': 'image.gummibaum',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'fyta',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'ce5f5431554d101905d31797e1232da8-0-plant_image',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[image.gummibaum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'access_token': '1',
'entity_picture': '/api/image_proxy/image.gummibaum?token=1',
'friendly_name': 'Gummibaum',
}),
'context': <ANY>,
'entity_id': 'image.gummibaum',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_entities[image.kakaobaum-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'image',
'entity_category': None,
'entity_id': 'image.kakaobaum',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'fyta',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'ce5f5431554d101905d31797e1232da8-1-plant_image',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[image.kakaobaum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'access_token': '1',
'entity_picture': '/api/image_proxy/image.kakaobaum?token=1',
'friendly_name': 'Kakaobaum',
}),
'context': <ANY>,
'entity_id': 'image.kakaobaum',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@ -0,0 +1,129 @@
"""Test the Home Assistant fyta sensor module."""
from datetime import timedelta
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from fyta_cli.fyta_exceptions import FytaConnectionError, FytaPlantError
from fyta_cli.fyta_models import Plant
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.fyta.const import DOMAIN as FYTA_DOMAIN
from homeassistant.components.image import ImageEntity
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_platform
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_json_object_fixture,
snapshot_platform,
)
async def test_all_entities(
hass: HomeAssistant,
mock_fyta_connector: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test all entities."""
await setup_platform(hass, mock_config_entry, [Platform.IMAGE])
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
assert len(hass.states.async_all("image")) == 2
@pytest.mark.parametrize(
"exception",
[
FytaConnectionError,
FytaPlantError,
],
)
async def test_connection_error(
hass: HomeAssistant,
exception: Exception,
mock_fyta_connector: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test connection error."""
await setup_platform(hass, mock_config_entry, [Platform.IMAGE])
mock_fyta_connector.update_all_plants.side_effect = exception
freezer.tick(delta=timedelta(hours=12))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("image.gummibaum").state == STATE_UNAVAILABLE
async def test_add_remove_entities(
hass: HomeAssistant,
mock_fyta_connector: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test if entities are added and old are removed."""
await setup_platform(hass, mock_config_entry, [Platform.IMAGE])
assert hass.states.get("image.gummibaum") is not None
plants: dict[int, Plant] = {
0: Plant.from_dict(load_json_object_fixture("plant_status1.json", FYTA_DOMAIN)),
2: Plant.from_dict(load_json_object_fixture("plant_status3.json", FYTA_DOMAIN)),
}
mock_fyta_connector.update_all_plants.return_value = plants
mock_fyta_connector.plant_list = {
0: "Kautschukbaum",
2: "Tomatenpflanze",
}
freezer.tick(delta=timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("image.kakaobaum") is None
assert hass.states.get("image.tomatenpflanze") is not None
async def test_update_image(
hass: HomeAssistant,
mock_fyta_connector: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test if entity picture is updated."""
await setup_platform(hass, mock_config_entry, [Platform.IMAGE])
image_entity: ImageEntity = hass.data["domain_entities"]["image"]["image.gummibaum"]
assert image_entity.image_url == "http://www.plant_picture.com/picture"
plants: dict[int, Plant] = {
0: Plant.from_dict(
load_json_object_fixture("plant_status1_update.json", FYTA_DOMAIN)
),
2: Plant.from_dict(load_json_object_fixture("plant_status3.json", FYTA_DOMAIN)),
}
mock_fyta_connector.update_all_plants.return_value = plants
mock_fyta_connector.plant_list = {
0: "Kautschukbaum",
2: "Tomatenpflanze",
}
freezer.tick(delta=timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert image_entity.image_url == "http://www.plant_picture.com/picture1"