Skip to content

Commit

Permalink
Merge pull request #247 from marcelvriend/feature/polling-individual-…
Browse files Browse the repository at this point in the history
…entities

Polling individual entities
  • Loading branch information
isabellaalstrom authored Sep 14, 2022
2 parents 054b713 + 10ccf63 commit 5ab6182
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 451 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.
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.

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.
You get a binary sensor each for overdue, expired, expiring and missing products and for overdue tasks, overdue chores and overdue batteries.
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.


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

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

from .const import (
ATTR_BATTERIES,
Expand All @@ -26,12 +27,19 @@
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 @@ -41,13 +49,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Set up this integration using UI."""
_LOGGER.info(STARTUP_MESSAGE)

coordinator: GrocyDataUpdateCoordinator = GrocyDataUpdateCoordinator(hass)
coordinator.available_entities = await _async_get_available_entities(
coordinator.grocy_data
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: [],
},
)
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 @@ -56,6 +68,19 @@ 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 @@ -67,10 +92,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return unloaded


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

from homeassistant.components.binary_sensor import (
BinarySensorEntity,
Expand All @@ -23,26 +24,31 @@
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."""
coordinator: GrocyDataUpdateCoordinator = hass.data[DOMAIN]
available_entities = hass.data[DOMAIN][GROCY_AVAILABLE_ENTITIES]
registered_entities = hass.data[DOMAIN][REGISTERED_ENTITIES]
entities = []
for description in BINARY_SENSORS:
if description.exists_fn(coordinator.available_entities):
entity = GrocyBinarySensorEntity(coordinator, description, config_entry)
coordinator.entities.append(entity)
if description.exists_fn(available_entities):
entity = GrocyBinarySensorEntity(hass, description, config_entry)
entities.append(entity)
registered_entities.append(entity)
else:
_LOGGER.debug(
"Entity description '%s' is not available.",
Expand All @@ -53,7 +59,16 @@ async def async_setup_entry(


@dataclass
class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
class GrocyEntityDescriptionMixin:
"""Mixin for required keys."""

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


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

attributes_fn: Callable[[List[Any]], Mapping[str, Any] | None] = lambda _: None
Expand All @@ -67,6 +82,7 @@ class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
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 @@ -77,6 +93,7 @@ class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
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 @@ -87,6 +104,7 @@ class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
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 @@ -97,6 +115,7 @@ class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
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 @@ -107,6 +126,7 @@ class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
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 @@ -117,6 +137,7 @@ class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
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 @@ -127,6 +148,7 @@ class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
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 @@ -141,6 +163,4 @@ class GrocyBinarySensorEntity(GrocyEntity, BinarySensorEntity):
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
entity_data = self.coordinator.data.get(self.entity_description.key, None)

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

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

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

SCAN_INTERVAL = timedelta(seconds=30)

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

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 @@ -47,3 +48,27 @@
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: 0 additions & 74 deletions custom_components/grocy/coordinator.py

This file was deleted.

Loading

0 comments on commit 5ab6182

Please sign in to comment.