Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Future updates will appear automatically within Home Assistant via HACS.

#### Manual Installation

1. Update Home Assistant to version 2026.02 or newer
1. Update Home Assistant to version 2026.07.4 or newer
2. Download this repository
3. Copy the `custom_components/grocy` folder into your Home Assistant's `custom_components` folder
4. Restart Home Assistant
Expand Down
2 changes: 1 addition & 1 deletion custom_components/grocy/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ async def async_setup_entry(
for description in BINARY_SENSORS:
if description.exists_fn(coordinator.available_entities):
entity = GrocyBinarySensorEntity(coordinator, description, config_entry)
coordinator.entities.append(entity)
entities.append(entity)
else:
_LOGGER.debug(
Expand All @@ -52,6 +51,7 @@ async def async_setup_entry(
)

async_add_entities(entities, True)
coordinator.entities.extend(entities)


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion custom_components/grocy/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ async def async_setup_entry(
coordinator: GrocyDataUpdateCoordinator = hass.data[DOMAIN]

entity = GrocyCalendarEntity(coordinator, config_entry)
coordinator.entities.append(entity)
async_add_entities([entity], True)
coordinator.entities.append(entity)


class GrocyCalendarEntity(CalendarEntity):
Expand Down
2 changes: 1 addition & 1 deletion custom_components/grocy/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ async def async_setup_entry(
for description in SENSORS:
if description.exists_fn(coordinator.available_entities):
entity = GrocySensorEntity(coordinator, description, config_entry)
coordinator.entities.append(entity)
entities.append(entity)
else:
_LOGGER.debug(
Expand All @@ -57,6 +56,7 @@ async def async_setup_entry(
)

async_add_entities(entities, True)
coordinator.entities.extend(entities)


@dataclass
Expand Down
8 changes: 6 additions & 2 deletions custom_components/grocy/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,12 @@ async def _async_force_update_entity(
),
None,
)
if entity:
await entity.async_update_ha_state(force_refresh=True)
if entity is None:
return
if hasattr(entity, "hass") and entity.hass is None:
return

await entity.async_update_ha_state(force_refresh=True)


async def async_sync_calendar_service(
Expand Down
35 changes: 29 additions & 6 deletions custom_components/grocy/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ async def async_setup_entry(
for description in TODOS:
if description.exists_fn(coordinator.available_entities):
entity = GrocyTodoListEntity(coordinator, description, config_entry)
coordinator.entities.append(entity)
entities.append(entity)
else:
_LOGGER.debug(
Expand All @@ -82,6 +81,7 @@ async def async_setup_entry(
)

async_add_entities(entities, True)
coordinator.entities.extend(entities)


@dataclass
Expand Down Expand Up @@ -188,22 +188,45 @@ def __init__(
elif isinstance(item, MealPlanItem):
due = item.day
days_until = _calculate_days_until(due, True)
recipe = getattr(item, "recipe", None)
summary = (
recipe.name
if (recipe and getattr(recipe, "name", None))
else "Unknown recipe"
)
description = (
recipe.description
if (recipe and getattr(recipe, "description", None))
else None
)
super().__init__(
uid=item.id.__str__(),
summary=item.recipe.name,
summary=summary,
due=due,
status=_calculate_item_status(days_until),
description=item.recipe.description or None,
description=description,
)
elif isinstance(item, MealPlanItemWrapper):
due = item.meal_plan.day
days_until = _calculate_days_until(due, True)
mp = item.meal_plan
recipe = getattr(mp, "recipe", None)
summary = (
recipe.name
if (recipe and getattr(recipe, "name", None))
else "Unknown recipe"
)
description = (
recipe.description
if (recipe and getattr(recipe, "description", None))
else None
)
super().__init__(
uid=item.meal_plan.id.__str__(),
summary=item.meal_plan.recipe.name,
uid=mp.id.__str__(),
summary=summary,
due=due,
status=_calculate_item_status(days_until),
description=item.meal_plan.recipe.description or None,
description=description,
)
elif isinstance(item, Product):
super().__init__(
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
homeassistant:
image: ghcr.io/home-assistant/home-assistant:2026.1.3
image: ghcr.io/home-assistant/home-assistant:2026.7.4
container_name: ha-dev
depends_on:
- grocy
Expand Down
2 changes: 2 additions & 0 deletions docs/test-feature-map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ features:
functions:
- test_todo_item_from_meal_plan_item
- test_todo_item_from_meal_plan_item_wrapper
- test_todo_item_from_meal_plan_item_missing_recipe
- test_async_update_todo_item_complete_meal_plan
- test_async_update_todo_item_meal_plan_needs_action_raises
- file: tests/test_grocy_data.py
Expand Down Expand Up @@ -328,6 +329,7 @@ cross_cutting:
- test_async_unload_services_noop_if_not_registered
- test_async_force_update_entity_updates_matching_entity
- test_async_force_update_entity_ignores_missing
- test_async_force_update_entity_skips_entity_without_hass
- file: tests/test_grocy_data.py
functions:
- test_async_update_data_dispatches_to_correct_method
Expand Down
2 changes: 1 addition & 1 deletion hacs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"render_readme": true,
"zip_release": true,
"hide_default_branch": true,
"homeassistant": "2026.2.1",
"homeassistant": "2026.7.4",
"filename": "grocy.zip"
}
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ name = "grocy-custom-component"
version = "1.15.0"
description = "Grocy custom component for Home Assistant"
readme = "README.md"
requires-python = ">=3.13.2"
requires-python = ">=3.14.2"
dependencies = [
"homeassistant==2026.2.1",
"homeassistant==2026.7.4",
"grocy-py==0.1.0",
"icalendar>=6.0.0",
]
Expand Down
16 changes: 16 additions & 0 deletions tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,22 @@ async def test_async_force_update_entity_updates_matching_entity() -> None:
entity.async_update_ha_state.assert_awaited_once_with(force_refresh=True)


@pytest.mark.feature("cross_cutting")
@pytest.mark.asyncio
async def test_async_force_update_entity_skips_entity_without_hass() -> None:
"""Verify force update skips entities that are not attached to hass."""
entity = SimpleNamespace(
entity_description=SimpleNamespace(key=services.ATTR_TASKS),
hass=None,
async_update_ha_state=AsyncMock(),
)
coordinator = SimpleNamespace(entities=[entity])

await services._async_force_update_entity(coordinator, services.ATTR_TASKS)

entity.async_update_ha_state.assert_not_awaited()


@pytest.mark.feature("cross_cutting")
@pytest.mark.asyncio
async def test_async_force_update_entity_ignores_missing() -> None:
Expand Down
16 changes: 16 additions & 0 deletions tests/test_todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,22 @@ def test_todo_item_from_meal_plan_item() -> None:
assert item.status == TodoItemStatus.COMPLETED


@pytest.mark.feature("meal_planning")
def test_todo_item_from_meal_plan_item_missing_recipe() -> None:
"""Verify meal plan item with missing recipe does not raise and uses fallback."""
mpi = MealPlanItem(
id=51,
day=dt.date.today() + dt.timedelta(days=1),
recipe=None,
type=MealPlanItemType.RECIPE,
)
item = GrocyTodoItem(mpi, ATTR_MEAL_PLAN)

assert item.uid == "51"
assert item.summary == "Unknown recipe"
assert item.description is None


# ─── GrocyTodoItem from MealPlanItemWrapper ───────────────────────────────────


Expand Down
Loading