Skip to content

Commit

Permalink
Merge pull request #250 from marcelvriend/revert/polling-individual-e…
Browse files Browse the repository at this point in the history
…ntities

Revert "Polling individual entities"
  • Loading branch information
isabellaalstrom authored Sep 19, 2022
2 parents 5ab6182 + 0a95c22 commit 88ba300
Show file tree
Hide file tree
Showing 9 changed files with 451 additions and 495 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ The configuration is slightly different for those who use the [official Grocy ad
4. Restart Home Assistant as instructed by HACS.
5. Install the [Grocy integration](https://my.home-assistant.io/redirect/config_flow_start/?domain=grocy). Fill out the information according to [this instruction](#integration-configuration).
6. Before integration version v4.3.3, now restart Home Assistant again (with later versions you can skip this step).
7. You will now have a new integration for Grocy. All entities are disabled from start, manually enable the entities you want to use. It can take up to 30 seconds before all entities are visible.
7. You will now have a new integration for Grocy. All entities are disabled from start, manually enable the entities you want to use.

Future integration updates will appear automatically within Home Assistant via HACS.


# Entities

**All entities are disabled from the start. You have to manually enable the entities you want to use in Home Assistant.**
You get a sensor each for chores, meal plan, shopping list, stock, tasks and batteries. The sensors refresh every 60 seconds.
You get a binary sensor each for overdue, expired, expiring and missing products and for overdue tasks, overdue chores and overdue batteries. The binary sensors refresh every 5 minutes.
You get a sensor each for chores, meal plan, shopping list, stock, tasks and batteries.
You get a binary sensor each for overdue, expired, expiring and missing products and for overdue tasks, overdue chores and overdue batteries.


# Services
Expand Down
43 changes: 9 additions & 34 deletions custom_components/grocy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from pygrocy import Grocy

from .const import (
ATTR_BATTERIES,
Expand All @@ -27,19 +26,12 @@
ATTR_SHOPPING_LIST,
ATTR_STOCK,
ATTR_TASKS,
CONF_API_KEY,
CONF_PORT,
CONF_URL,
CONF_VERIFY_SSL,
DOMAIN,
GROCY_AVAILABLE_ENTITIES,
GROCY_CLIENT,
PLATFORMS,
REGISTERED_ENTITIES,
STARTUP_MESSAGE,
)
from .coordinator import GrocyDataUpdateCoordinator
from .grocy_data import GrocyData, async_setup_endpoint_for_image_proxy
from .helpers import extract_base_url_and_path
from .services import async_setup_services, async_unload_services

_LOGGER = logging.getLogger(__name__)
Expand All @@ -49,17 +41,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Set up this integration using UI."""
_LOGGER.info(STARTUP_MESSAGE)

grocy_client = setup_grocy_client(hass, config_entry)
available_entities = await _async_get_available_entities(grocy_client)

hass.data.setdefault(
DOMAIN,
{
GROCY_CLIENT: grocy_client,
GROCY_AVAILABLE_ENTITIES: available_entities,
REGISTERED_ENTITIES: [],
},
coordinator: GrocyDataUpdateCoordinator = GrocyDataUpdateCoordinator(hass)
coordinator.available_entities = await _async_get_available_entities(
coordinator.grocy_data
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN] = coordinator

hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
await async_setup_services(hass, config_entry)
Expand All @@ -68,19 +56,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
return True


def setup_grocy_client(hass: HomeAssistant, config_entry: ConfigEntry) -> GrocyData:
"""Initialize Grocy"""
url = config_entry.data[CONF_URL]
api_key = config_entry.data[CONF_API_KEY]
port = config_entry.data[CONF_PORT]
verify_ssl = config_entry.data[CONF_VERIFY_SSL]

(base_url, path) = extract_base_url_and_path(url)

grocy_api = Grocy(base_url, api_key, path=path, port=port, verify_ssl=verify_ssl)
return GrocyData(hass, grocy_api)


async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
await async_unload_services(hass)
Expand All @@ -92,10 +67,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return unloaded


async def _async_get_available_entities(grocy_client: GrocyData) -> List[str]:
async def _async_get_available_entities(grocy_data: GrocyData) -> List[str]:
"""Return a list of available entities based on enabled Grocy features."""
available_entities = []
grocy_config = await grocy_client.async_get_config()
grocy_config = await grocy_data.async_get_config()
if grocy_config:
if "FEATURE_FLAG_STOCK" in grocy_config.enabled_features:
available_entities.append(ATTR_STOCK)
Expand Down
40 changes: 10 additions & 30 deletions custom_components/grocy/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import logging
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import timedelta
from typing import Any, Coroutine, List
from typing import Any, List

from homeassistant.components.binary_sensor import (
BinarySensorEntity,
Expand All @@ -24,31 +23,26 @@
ATTR_OVERDUE_PRODUCTS,
ATTR_OVERDUE_TASKS,
DOMAIN,
GROCY_AVAILABLE_ENTITIES,
REGISTERED_ENTITIES,
)
from .coordinator import GrocyDataUpdateCoordinator
from .entity import GrocyEntity
from .grocy_data import GrocyData

_LOGGER = logging.getLogger(__name__)

SCAN_INTERVAL = timedelta(seconds=300)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
):
"""Setup binary sensor platform."""
available_entities = hass.data[DOMAIN][GROCY_AVAILABLE_ENTITIES]
registered_entities = hass.data[DOMAIN][REGISTERED_ENTITIES]
coordinator: GrocyDataUpdateCoordinator = hass.data[DOMAIN]
entities = []
for description in BINARY_SENSORS:
if description.exists_fn(available_entities):
entity = GrocyBinarySensorEntity(hass, description, config_entry)
if description.exists_fn(coordinator.available_entities):
entity = GrocyBinarySensorEntity(coordinator, description, config_entry)
coordinator.entities.append(entity)
entities.append(entity)
registered_entities.append(entity)
else:
_LOGGER.debug(
"Entity description '%s' is not available.",
Expand All @@ -59,16 +53,7 @@ async def async_setup_entry(


@dataclass
class GrocyEntityDescriptionMixin:
"""Mixin for required keys."""

value_fn: Callable[[GrocyData], Coroutine[Any, Any, Any]]


@dataclass
class GrocyBinarySensorEntityDescription(
BinarySensorEntityDescription, GrocyEntityDescriptionMixin
):
class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Grocy binary sensor entity description."""

attributes_fn: Callable[[List[Any]], Mapping[str, Any] | None] = lambda _: None
Expand All @@ -82,7 +67,6 @@ class GrocyBinarySensorEntityDescription(
name="Grocy expired products",
icon="mdi:delete-alert-outline",
exists_fn=lambda entities: ATTR_EXPIRED_PRODUCTS in entities,
value_fn=lambda grocy: grocy.async_update_expired_products(),
attributes_fn=lambda data: {
"expired_products": [x.as_dict() for x in data],
"count": len(data),
Expand All @@ -93,7 +77,6 @@ class GrocyBinarySensorEntityDescription(
name="Grocy expiring products",
icon="mdi:clock-fast",
exists_fn=lambda entities: ATTR_EXPIRING_PRODUCTS in entities,
value_fn=lambda grocy: grocy.async_update_expiring_products(),
attributes_fn=lambda data: {
"expiring_products": [x.as_dict() for x in data],
"count": len(data),
Expand All @@ -104,7 +87,6 @@ class GrocyBinarySensorEntityDescription(
name="Grocy overdue products",
icon="mdi:alert-circle-check-outline",
exists_fn=lambda entities: ATTR_OVERDUE_PRODUCTS in entities,
value_fn=lambda grocy: grocy.async_update_overdue_products(),
attributes_fn=lambda data: {
"overdue_products": [x.as_dict() for x in data],
"count": len(data),
Expand All @@ -115,7 +97,6 @@ class GrocyBinarySensorEntityDescription(
name="Grocy missing products",
icon="mdi:flask-round-bottom-empty-outline",
exists_fn=lambda entities: ATTR_MISSING_PRODUCTS in entities,
value_fn=lambda grocy: grocy.async_update_missing_products(),
attributes_fn=lambda data: {
"missing_products": [x.as_dict() for x in data],
"count": len(data),
Expand All @@ -126,7 +107,6 @@ class GrocyBinarySensorEntityDescription(
name="Grocy overdue chores",
icon="mdi:alert-circle-check-outline",
exists_fn=lambda entities: ATTR_OVERDUE_CHORES in entities,
value_fn=lambda grocy: grocy.async_update_overdue_chores(),
attributes_fn=lambda data: {
"overdue_chores": [x.as_dict() for x in data],
"count": len(data),
Expand All @@ -137,7 +117,6 @@ class GrocyBinarySensorEntityDescription(
name="Grocy overdue tasks",
icon="mdi:alert-circle-check-outline",
exists_fn=lambda entities: ATTR_OVERDUE_TASKS in entities,
value_fn=lambda grocy: grocy.async_update_overdue_tasks(),
attributes_fn=lambda data: {
"overdue_tasks": [x.as_dict() for x in data],
"count": len(data),
Expand All @@ -148,7 +127,6 @@ class GrocyBinarySensorEntityDescription(
name="Grocy overdue batteries",
icon="mdi:battery-charging-10",
exists_fn=lambda entities: ATTR_OVERDUE_BATTERIES in entities,
value_fn=lambda grocy: grocy.async_update_batteries(),
attributes_fn=lambda data: {
"overdue_batteries": [x.as_dict() for x in data],
"count": len(data),
Expand All @@ -163,4 +141,6 @@ class GrocyBinarySensorEntity(GrocyEntity, BinarySensorEntity):
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return len(self.data) > 0 if self.data else False
entity_data = self.coordinator.data.get(self.entity_description.key, None)

return len(entity_data) > 0 if entity_data else False
31 changes: 3 additions & 28 deletions custom_components/grocy/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Constants for Grocy."""
from datetime import timedelta
from typing import Final

NAME: Final = "Grocy"
Expand All @@ -9,6 +10,8 @@

PLATFORMS: Final = ["binary_sensor", "sensor"]

SCAN_INTERVAL = timedelta(seconds=30)

DEFAULT_PORT: Final = 9192
CONF_URL: Final = "url"
CONF_PORT: Final = "port"
Expand All @@ -25,10 +28,6 @@
-------------------------------------------------------------------
"""

GROCY_CLIENT = "grocy_client"
GROCY_AVAILABLE_ENTITIES = "available_entities"
REGISTERED_ENTITIES = "registered_entities"

CHORES: Final = "Chore(s)"
MEAL_PLANS: Final = "Meal(s)"
PRODUCTS: Final = "Product(s)"
Expand All @@ -48,27 +47,3 @@
ATTR_SHOPPING_LIST: Final = "shopping_list"
ATTR_STOCK: Final = "stock"
ATTR_TASKS: Final = "tasks"

SERVICE_PRODUCT_ID = "product_id"
SERVICE_AMOUNT = "amount"
SERVICE_PRICE = "price"
SERVICE_SPOILED = "spoiled"
SERVICE_SUBPRODUCT_SUBSTITUTION = "allow_subproduct_substitution"
SERVICE_TRANSACTION_TYPE = "transaction_type"
SERVICE_CHORE_ID = "chore_id"
SERVICE_DONE_BY = "done_by"
SERVICE_SKIPPED = "skipped"
SERVICE_TASK_ID = "task_id"
SERVICE_ENTITY_TYPE = "entity_type"
SERVICE_DATA = "data"
SERVICE_RECIPE_ID = "recipe_id"
SERVICE_BATTERY_ID = "battery_id"

SERVICE_ADD_PRODUCT = "add_product_to_stock"
SERVICE_CONSUME_PRODUCT = "consume_product_from_stock"
SERVICE_OPEN_PRODUCT = "open_product"
SERVICE_EXECUTE_CHORE = "execute_chore"
SERVICE_COMPLETE_TASK = "complete_task"
SERVICE_ADD_GENERIC = "add_generic"
SERVICE_CONSUME_RECIPE = "consume_recipe"
SERVICE_TRACK_BATTERY = "track_battery"
74 changes: 74 additions & 0 deletions custom_components/grocy/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Data update coordinator for Grocy."""
from __future__ import annotations

import logging
from typing import Any, Dict, List

from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from pygrocy import Grocy

from .const import (
CONF_API_KEY,
CONF_PORT,
CONF_URL,
CONF_VERIFY_SSL,
DOMAIN,
SCAN_INTERVAL,
)
from .grocy_data import GrocyData
from .helpers import extract_base_url_and_path

_LOGGER = logging.getLogger(__name__)


class GrocyDataUpdateCoordinator(DataUpdateCoordinator[Dict[str, Any]]):
"""Grocy data update coordinator."""

def __init__(
self,
hass: HomeAssistant,
) -> None:
"""Initialize Grocy data update coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)

url = self.config_entry.data[CONF_URL]
api_key = self.config_entry.data[CONF_API_KEY]
port = self.config_entry.data[CONF_PORT]
verify_ssl = self.config_entry.data[CONF_VERIFY_SSL]

(base_url, path) = extract_base_url_and_path(url)

self.grocy_api = Grocy(
base_url, api_key, path=path, port=port, verify_ssl=verify_ssl
)
self.grocy_data = GrocyData(hass, self.grocy_api)

self.available_entities: List[str] = []
self.entities: List[Entity] = []

async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data."""
data: dict[str, Any] = {}

for entity in self.entities:
if not entity.enabled:
_LOGGER.debug("Entity %s is disabled.", entity.entity_id)
continue

try:
data[
entity.entity_description.key
] = await self.grocy_data.async_update_data(
entity.entity_description.key
)
except Exception as error: # pylint: disable=broad-except
raise UpdateFailed(f"Update failed: {error}") from error

return data
Loading

0 comments on commit 88ba300

Please sign in to comment.