Add sensor platform to Mealie (#122280)

* Bump aiomealie to 0.7.0

* Add sensor platform to Mealie

* Fix
This commit is contained in:
Joost Lekkerkerker 2024-07-21 14:59:22 +02:00 committed by GitHub
parent 7f82fb8cb8
commit 874b1ae873
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 447 additions and 2 deletions

View File

@ -22,11 +22,12 @@ from .coordinator import (
MealieData,
MealieMealplanCoordinator,
MealieShoppingListCoordinator,
MealieStatisticsCoordinator,
)
from .services import setup_services
from .utils import create_version
PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.TODO]
PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.SENSOR, Platform.TODO]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@ -75,12 +76,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo
mealplan_coordinator = MealieMealplanCoordinator(hass, client)
shoppinglist_coordinator = MealieShoppingListCoordinator(hass, client)
statistics_coordinator = MealieStatisticsCoordinator(hass, client)
await mealplan_coordinator.async_config_entry_first_refresh()
await shoppinglist_coordinator.async_config_entry_first_refresh()
await statistics_coordinator.async_config_entry_first_refresh()
entry.runtime_data = MealieData(
client, mealplan_coordinator, shoppinglist_coordinator
client, mealplan_coordinator, shoppinglist_coordinator, statistics_coordinator
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@ -13,6 +13,7 @@ from aiomealie import (
MealplanEntryType,
ShoppingItem,
ShoppingList,
Statistics,
)
from homeassistant.config_entries import ConfigEntry
@ -33,6 +34,7 @@ class MealieData:
client: MealieClient
mealplan_coordinator: MealieMealplanCoordinator
shoppinglist_coordinator: MealieShoppingListCoordinator
statistics_coordinator: MealieStatisticsCoordinator
type MealieConfigEntry = ConfigEntry[MealieData]
@ -139,3 +141,26 @@ class MealieShoppingListCoordinator(
except MealieConnectionError as error:
raise UpdateFailed(error) from error
return shopping_list_items
class MealieStatisticsCoordinator(MealieDataUpdateCoordinator[Statistics]):
"""Class to manage fetching Mealie Statistics data."""
def __init__(self, hass: HomeAssistant, client: MealieClient) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
name="MealieStatistics",
client=client,
update_interval=timedelta(minutes=15),
)
async def _async_update_data(
self,
) -> Statistics:
try:
return await self.client.get_statistics()
except MealieAuthenticationError as error:
raise ConfigEntryAuthFailed from error
except MealieConnectionError as error:
raise UpdateFailed(error) from error

View File

@ -4,6 +4,23 @@
"shopping_list": {
"default": "mdi:basket"
}
},
"sensor": {
"recipes": {
"default": "mdi:food"
},
"users": {
"default": "mdi:account-multiple"
},
"categories": {
"default": "mdi:shape"
},
"tags": {
"default": "mdi:tag-multiple"
},
"tools": {
"default": "mdi:tools"
}
}
},
"services": {

View File

@ -0,0 +1,94 @@
"""Support for Mealie sensors."""
from collections.abc import Callable
from dataclasses import dataclass
from aiomealie import Statistics
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .coordinator import MealieConfigEntry, MealieStatisticsCoordinator
from .entity import MealieEntity
@dataclass(frozen=True, kw_only=True)
class MealieStatisticsSensorEntityDescription(SensorEntityDescription):
"""Describes Mealie Statistics sensor entity."""
value_fn: Callable[[Statistics], StateType]
SENSOR_TYPES: tuple[MealieStatisticsSensorEntityDescription, ...] = (
MealieStatisticsSensorEntityDescription(
key="recipes",
native_unit_of_measurement="recipes",
state_class=SensorStateClass.TOTAL,
value_fn=lambda statistics: statistics.total_recipes,
),
MealieStatisticsSensorEntityDescription(
key="users",
native_unit_of_measurement="users",
state_class=SensorStateClass.TOTAL,
value_fn=lambda statistics: statistics.total_users,
),
MealieStatisticsSensorEntityDescription(
key="categories",
native_unit_of_measurement="categories",
state_class=SensorStateClass.TOTAL,
value_fn=lambda statistics: statistics.total_categories,
),
MealieStatisticsSensorEntityDescription(
key="tags",
native_unit_of_measurement="tags",
state_class=SensorStateClass.TOTAL,
value_fn=lambda statistics: statistics.total_tags,
),
MealieStatisticsSensorEntityDescription(
key="tools",
native_unit_of_measurement="tools",
state_class=SensorStateClass.TOTAL,
value_fn=lambda statistics: statistics.total_tools,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: MealieConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Mealie sensors based on a config entry."""
coordinator = entry.runtime_data.statistics_coordinator
async_add_entities(
MealieStatisticSensors(coordinator, description) for description in SENSOR_TYPES
)
class MealieStatisticSensors(MealieEntity, SensorEntity):
"""Defines a Mealie sensor."""
entity_description: MealieStatisticsSensorEntityDescription
coordinator: MealieStatisticsCoordinator
def __init__(
self,
coordinator: MealieStatisticsCoordinator,
description: MealieStatisticsSensorEntityDescription,
) -> None:
"""Initialize Mealie sensor."""
super().__init__(coordinator, description.key)
self.entity_description = description
self._attr_translation_key = description.key
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@ -53,6 +53,23 @@
"side": {
"name": "Side"
}
},
"sensor": {
"recipes": {
"name": "Recipes"
},
"users": {
"name": "Users"
},
"categories": {
"name": "Categories"
},
"tags": {
"name": "Tags"
},
"tools": {
"name": "Tools"
}
}
},
"exceptions": {

View File

@ -10,6 +10,7 @@ from aiomealie import (
Recipe,
ShoppingItemsResponse,
ShoppingListsResponse,
Statistics,
UserInfo,
)
from mashumaro.codecs.orjson import ORJSONDecoder
@ -70,6 +71,9 @@ def mock_mealie_client() -> Generator[AsyncMock]:
client.get_shopping_items.return_value = ShoppingItemsResponse.from_json(
load_fixture("get_shopping_items.json", DOMAIN)
)
client.get_statistics.return_value = Statistics.from_json(
load_fixture("statistics.json", DOMAIN)
)
yield client

View File

@ -0,0 +1,7 @@
{
"totalRecipes": 765,
"totalUsers": 3,
"totalCategories": 24,
"totalTags": 454,
"totalTools": 11
}

View File

@ -0,0 +1,251 @@
# serializer version: 1
# name: test_entities[sensor.mealie_categories-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mealie_categories',
'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': 'Categories',
'platform': 'mealie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'categories',
'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_categories',
'unit_of_measurement': 'categories',
})
# ---
# name: test_entities[sensor.mealie_categories-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mealie Categories',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': 'categories',
}),
'context': <ANY>,
'entity_id': 'sensor.mealie_categories',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '24',
})
# ---
# name: test_entities[sensor.mealie_recipes-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mealie_recipes',
'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': 'Recipes',
'platform': 'mealie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'recipes',
'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_recipes',
'unit_of_measurement': 'recipes',
})
# ---
# name: test_entities[sensor.mealie_recipes-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mealie Recipes',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': 'recipes',
}),
'context': <ANY>,
'entity_id': 'sensor.mealie_recipes',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '765',
})
# ---
# name: test_entities[sensor.mealie_tags-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mealie_tags',
'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': 'Tags',
'platform': 'mealie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'tags',
'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_tags',
'unit_of_measurement': 'tags',
})
# ---
# name: test_entities[sensor.mealie_tags-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mealie Tags',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': 'tags',
}),
'context': <ANY>,
'entity_id': 'sensor.mealie_tags',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '454',
})
# ---
# name: test_entities[sensor.mealie_tools-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mealie_tools',
'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': 'Tools',
'platform': 'mealie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'tools',
'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_tools',
'unit_of_measurement': 'tools',
})
# ---
# name: test_entities[sensor.mealie_tools-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mealie Tools',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': 'tools',
}),
'context': <ANY>,
'entity_id': 'sensor.mealie_tools',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '11',
})
# ---
# name: test_entities[sensor.mealie_users-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mealie_users',
'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': 'Users',
'platform': 'mealie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'users',
'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_users',
'unit_of_measurement': 'users',
})
# ---
# name: test_entities[sensor.mealie_users-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mealie Users',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': 'users',
}),
'context': <ANY>,
'entity_id': 'sensor.mealie_users',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---

View File

@ -0,0 +1,27 @@
"""Tests for the Mealie sensors."""
from unittest.mock import AsyncMock, patch
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_mealie_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the sensor entities."""
with patch("homeassistant.components.mealie.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)