Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #9 from andrew-codechimp/images
Browse files Browse the repository at this point in the history
Images
  • Loading branch information
andrew-codechimp authored Jun 7, 2024
2 parents fe9e249 + 4d36900 commit ecb300b
Show file tree
Hide file tree
Showing 11 changed files with 361 additions and 13 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ Given that Home Assistant To-do items are more simplistic, if you edit a Mealie
Creates a Mealie calendar within Home Assistant, this is read only and creates appropriate time slots for breakfast, lunch and dinner (see below for how to change the time slots.)

**Sensors**
Sensors for today's breakfast, lunch, dinner and side are created for easy use on dashboards.
Sensors for today's breakfast, lunch, dinner and side are created for easy use on dashboards.
An attribute recipe_url provides a link to the recipe.

**Images**
Images for today's breakfast, lunch, dinner and side, if a meal is a note or the recipe does not have an image the Mealie logo is displayed and the state becomes Unknown, allowing you to conditionally show/hide the image.
An attribute recipe_url provides a link to the recipe.

## Installation

Expand Down
7 changes: 6 additions & 1 deletion custom_components/mealie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@
from .api import MealieApiClient
from .coordinator import MealieDataUpdateCoordinator

PLATFORMS: list[Platform] = [Platform.TODO, Platform.CALENDAR, Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.TODO,
Platform.CALENDAR,
Platform.SENSOR,
Platform.IMAGE,
]

CONFIG_SCHEMA = vol.Schema(
{
Expand Down
8 changes: 8 additions & 0 deletions custom_components/mealie/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ async def async_get_meal_plans_today(self, group_id: str) -> dict:

return await self.api_wrapper("get", "/api/groups/mealplans/today", data=params)

def async_get_recipe_image_url(self, recipe_id: str) -> str:
"""Construct a url for the recipe image."""
return self.http_normalize_slashes(f"/api/media/recipes/{recipe_id}/images/min-original.webp")

def async_get_recipe_url(self, recipe_slug: str) -> str:
"""Construct a url for the recipe."""
return self.http_normalize_slashes(f"/g/home/r/{recipe_slug}")

async def api_wrapper(self, method: str, service: str, data: dict = {}) -> any:
"""Get information from the API."""

Expand Down
2 changes: 2 additions & 0 deletions custom_components/mealie/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

DOMAIN_CONFIG = "config"
COORDINATOR = "coordinator"
MEALIE_LOGO = "mealie.png"

SERVICE_ADD_SHOPPING_LIST_ITEM = "add_shopping_list_item"

Expand All @@ -33,3 +34,4 @@
CONF_DINNER_END = "dinner_end"

ATTR_SHOPPING_LIST_ID = "shopping_list_id"
ATTR_RECIPE_URL = "recipe_url"
115 changes: 114 additions & 1 deletion custom_components/mealie/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

from __future__ import annotations

from datetime import timedelta
from datetime import datetime, timedelta

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
UpdateFailed,
Expand Down Expand Up @@ -34,6 +35,14 @@ def __init__(
self._shopping_lists: dict | None = None
self.shopping_list_items: dict = {}
self.meal_plan: dict = {}
self.last_breakfast_image = None
self.last_breakfast_image_update = None
self.last_lunch_image = None
self.last_lunch_image_update = None
self.last_dinner_image: datetime | None = None
self.last_dinner_image_update: datetime | None = None
self.last_side_image: datetime | None = None
self.last_side_image_update: datetime | None = None

super().__init__(
hass=hass,
Expand Down Expand Up @@ -86,6 +95,110 @@ def todays_side(self) -> str | None:
return plan["title"]
return None

def todays_breakfast_image(self) -> str | None:
"""Return today's breakfast image."""
if self.meal_plan:
for plan in self.meal_plan:
if plan.get("entryType") == "breakfast":
if plan["recipeId"] and plan["recipe"]["image"]:
url = self.api.async_get_recipe_image_url(plan["recipeId"])
if url != self.last_breakfast_image:
self.last_breakfast_image = url
self.last_breakfast_image_update = dt_util.now()
return url
if self.last_breakfast_image is not None:
self.last_breakfast_image = None
self.last_breakfast_image_update = None
return None

def todays_lunch_image(self) -> str | None:
"""Return today's lunch."""
if self.meal_plan:
for plan in self.meal_plan:
if plan.get("entryType") == "lunch":
if plan["recipeId"] and plan["recipe"]["image"]:
url = self.api.async_get_recipe_image_url(plan["recipeId"])
if url != self.last_lunch_image:
self.last_lunch_image = url
self.last_lunch_image_update = dt_util.now()
return url
if self.last_lunch_image is not None:
self.last_lunch_image = None
self.last_lunch_image_update = None
return None

def todays_dinner_image(self) -> str | None:
"""Return today's dinner image."""
if self.meal_plan:
for plan in self.meal_plan:
if plan.get("entryType") == "dinner":
if plan["recipeId"] and plan["recipe"]["image"]:
url = self.api.async_get_recipe_image_url(plan["recipeId"])
if url != self.last_dinner_image:
self.last_dinner_image = url
self.last_dinner_image_update = dt_util.now()
return url
if self.last_dinner_image is not None:
self.last_dinner_image = None
self.last_dinner_image_update = None
return None

def todays_side_image(self) -> str | None:
"""Return today's side image."""
if self.meal_plan:
for plan in self.meal_plan:
if plan.get("entryType") == "side":
if plan["recipeId"]:
url = self.api.async_get_recipe_image_url(plan["recipeId"])
if url != self.last_side_image:
self.last_side_image = url
self.last_side_image_update = dt_util.now()
return url
if self.last_side_image is not None:
self.last_side_image = None
self.last_side_image_update = None
return None

def todays_breakfast_recipe_url(self) -> str | None:
"""Return today's breakfast recipe url."""
if self.meal_plan:
for plan in self.meal_plan:
if plan.get("entryType") == "breakfast":
if plan["recipeId"]:
url = self.api.async_get_recipe_url(plan["recipe"]["slug"])
return url
return None

def todays_lunch_recipe_url(self) -> str | None:
"""Return today's lunch recipe url."""
if self.meal_plan:
for plan in self.meal_plan:
if plan.get("entryType") == "lunch":
if plan["recipeId"]:
url = self.api.async_get_recipe_url(plan["recipe"]["slug"])
return url
return None

def todays_dinner_recipe_url(self) -> str | None:
"""Return today's dinner recipe url."""
if self.meal_plan:
for plan in self.meal_plan:
if plan.get("entryType") == "dinner":
if plan["recipeId"]:
url = self.api.async_get_recipe_url(plan["recipe"]["slug"])
return url
return None

def todays_side_recipe_url(self) -> str | None:
"""Return today's side recipe url."""
if self.meal_plan:
for plan in self.meal_plan:
if plan.get("entryType") == "side":
if plan["recipeId"]:
url = self.api.async_get_recipe_url(plan["recipe"]["slug"])
return url
return None

async def async_get_shopping_lists(self) -> dict:
"""Return shopping lists fetched at most once."""
if self._shopping_lists is None:
Expand Down
173 changes: 173 additions & 0 deletions custom_components/mealie/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""Image platform for Mealie."""

from __future__ import annotations

from pathlib import Path
from dataclasses import dataclass
from homeassistant.core import HomeAssistant, callback
from homeassistant.config_entries import ConfigEntry
from homeassistant.components.image import (
ImageEntity,
ImageEntityDescription,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED

from .const import DOMAIN, COORDINATOR, MEALIE_LOGO, ATTR_RECIPE_URL
from .entity import MealieEntity
from .coordinator import MealieDataUpdateCoordinator


ENTITY_DESCRIPTIONS = (
ImageEntityDescription(
key="todays_breakfast",
translation_key="todays_breakfast",
),
ImageEntityDescription(
key="todays_lunch",
translation_key="todays_lunch",
),
ImageEntityDescription(
key="todays_dinner",
translation_key="todays_dinner",
),
ImageEntityDescription(
key="todays_side",
translation_key="todays_side",
),
)


@dataclass
class Image:
"""Represent an image."""

content_type: str
content: bytes


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
coordinator: MealieDataUpdateCoordinator = hass.data[DOMAIN][COORDINATOR]

async_add_entities(
MealieImage(
entity_description=entity_description,
coordinator=coordinator,
)
for entity_description in ENTITY_DESCRIPTIONS
)


class MealieImage(MealieEntity, ImageEntity):
"""Mealie Image class."""

entity_description: ImageEntityDescription
current_image: bytes | None = None

def __init__(
self,
entity_description: ImageEntityDescription,
coordinator: MealieDataUpdateCoordinator,
) -> None:
"""Initialize the sensor class."""
super().__init__(entity_description, coordinator)
ImageEntity.__init__(self, coordinator.hass)

self._attr_should_poll = False
self.entity_id = f"image.mealie_{entity_description.key}"
self._attr_unique_id = f"mealie_{entity_description.key}_image".lower()
self.entity_description = entity_description
self.coordinator = coordinator
self._attr_has_entity_name = True
self.current_image = None

async def async_added_to_hass(self) -> None:
"""Handle added to Hass."""
await super().async_added_to_hass()

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""

self._cached_image = None

if self.entity_description.key == "todays_breakfast":
self._attr_image_url = self.coordinator.todays_breakfast_image()
self._attr_image_last_updated = self.coordinator.last_breakfast_image_update
if self.entity_description.key == "todays_lunch":
self._attr_image_url = self.coordinator.todays_lunch_image()
self._attr_image_last_updated = self.coordinator.last_lunch_image_update
if self.entity_description.key == "todays_dinner":
self._attr_image_url = self.coordinator.todays_dinner_image()
self._attr_image_last_updated = self.coordinator.last_dinner_image_update
if self.entity_description.key == "todays_side":
self._attr_image_url = self.coordinator.todays_side_image()
self._attr_image_last_updated = self.coordinator.last_side_image_update

super()._handle_coordinator_update()

async def _async_load_image_from_url(self, url: str) -> Image | None:
"""Load an image by url."""
if response := await self._fetch_url(url):
return Image(
content_type="image/webp",
content=response.content,
)
return None

def image(self) -> bytes | None:
"""Return bytes of image."""

return self.current_image

async def async_image(self) -> bytes | None:
"""Return bytes of image."""

mealie_logo_path = Path(__file__).parent / MEALIE_LOGO

if self._cached_image:
return self._cached_image.content

if (url := self.image_url) is not UNDEFINED:
if not url or (image := await self._async_load_image_from_url(url)) is None:
self.current_image = await self.hass.async_add_executor_job(
mealie_logo_path.read_bytes
)
self._attr_content_type = "image/png"
return self.current_image
self._cached_image = image
self.current_image = image
self._attr_content_type = "image/webp"
return image.content
self.current_image = await self.hass.async_add_executor_job(
mealie_logo_path.read_bytes
)
self._attr_content_type = "image/png"
return self.current_image

@property
def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the state attributes."""

if self.entity_description.key == "todays_breakfast":
url = self.coordinator.todays_breakfast_recipe_url()
if self.entity_description.key == "todays_lunch":
url = self.coordinator.todays_lunch_recipe_url()
if self.entity_description.key == "todays_dinner":
url = self.coordinator.todays_dinner_recipe_url()
if self.entity_description.key == "todays_side":
url = self.coordinator.todays_side_recipe_url()

attrs = {
ATTR_RECIPE_URL: url,
}

super_attrs = super().extra_state_attributes
if super_attrs:
attrs.update(super_attrs)
return attrs
Binary file added custom_components/mealie/mealie.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit ecb300b

Please sign in to comment.