Skip to content
Draft
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
Empty file.
132 changes: 132 additions & 0 deletions custom_components/growstuff/api/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@

import logging
from typing import Optional, List, Dict, Any
from datetime import date

from aiohttp import ClientSession

_LOGGER = logging.getLogger(__name__)

_API_URL = "https://www.growstuff.org/api/v1"


class GrowstuffApiClient:
"""A client for the Growstuff API."""

def __init__(self, session: ClientSession, api_key: Optional[str] = None):
"""Initialize the client."""
self._session = session
self._api_key = api_key
self._headers = {
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
if self._api_key:
self._headers["Authorization"] = f"Bearer {self._api_key}"

async def _get(self, url: str, params: Optional[Dict[str, str]] = None) -> Optional[Dict[str, Any]]:
"""Make a GET request to the API."""
_LOGGER.debug(f"Fetching {url} with params {params}")
async with self._session.get(url, params=params, headers=self._headers) as response:
if response.status != 200:
_LOGGER.error(f"Failed to fetch {url}: {response.status}")
return None
return await response.json()

async def _patch(self, url: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Make a PATCH request to the API."""
_LOGGER.debug(f"Patching {url} with payload {payload}")
async with self._session.patch(url, json=payload, headers=self._headers) as response:
if response.status != 200:
_LOGGER.error(f"Failed to update {url}: {response.status}")
_LOGGER.debug(f"Response: {await response.text()}")
return None
return await response.json()

async def _delete(self, url: str) -> bool:
"""Make a DELETE request to the API."""
_LOGGER.debug(f"Deleting {url}")
async with self._session.delete(url, headers=self._headers) as response:
if response.status != 204:
_LOGGER.error(f"Failed to delete {url}: {response.status}")
_LOGGER.debug(f"Response: {await response.text()}")
return False
return True

async def get_url(self, url: str) -> Optional[Dict[str, Any]]:
"""Make a GET request to an arbitrary URL."""
return await self._get(url)

async def _get_all(self, url: str, params: Optional[Dict[str, str]] = None) -> List[Dict[str, Any]]:
"""Get all items from a paginated endpoint."""
items = []
while url:
data = await self._get(url, params)
if data:
items.extend(data.get("data", []))
links = data.get("links", {})
url = links.get("next")
params = None # Params are already included in the next url
else:
break
return items

async def get_member(self, login_name: str) -> Optional[Dict[str, Any]]:
"""Get a member by login name."""
url = f"{_API_URL}/members"
params = {"filter[login-name]": login_name}
data = await self._get(url, params)
if data and data.get("data"):
return data["data"][0]
return None

async def get_gardens(self, owner_id: str) -> List[Dict[str, Any]]:
"""Get all gardens for a member."""
url = f"{_API_URL}/gardens"
params = {"filter[owner-id]": owner_id}
return await self._get_all(url, params)

async def get_plantings(self, owner_id: str, finished: bool = False) -> List[Dict[str, Any]]:
"""Get all plantings for a member."""
url = f"{_API_URL}/plantings"
params = {"filter[owner-id]": owner_id, "filter[finished]": str(finished).lower()}
return await self._get_all(url, params)

async def get_harvests(self, owner_id: str) -> List[Dict[str, Any]]:
"""Get all harvests for a member."""
url = f"{_API_URL}/harvests"
params = {"filter[owner-id]": owner_id}
return await self._get_all(url, params)

async def get_seeds(self, owner_id: str, finished: bool = False) -> List[Dict[str, Any]]:
"""Get all seeds for a member."""
url = f"{_API_URL}/seeds"
params = {"filter[owner-id]": owner_id, "filter[finished]": str(finished).lower()}
return await self._get_all(url, params)

async def get_activities(self, owner_id: str, finished: bool = False) -> List[Dict[str, Any]]:
"""Get all activities for a member."""
url = f"{_API_URL}/activities"
params = {"filter[owner-id]": owner_id, "filter[finished]": str(finished).lower()}
return await self._get_all(url, params)

async def update_activity(self, activity_id: str, due: Optional[date], description: str, finished: bool) -> Optional[Dict[str, Any]]:
"""Update an activity."""
url = f"{_API_URL}/activities/{activity_id}"
payload = {
"data": {
"type": "activities",
"id": activity_id,
"attributes": {
"due-date": due.isoformat() if due else None,
"description": description,
"finished": finished,
},
}
}
return await self._patch(url, payload)

async def delete_activity(self, activity_id: str) -> bool:
"""Delete an activity."""
url = f"{_API_URL}/activities/{activity_id}"
return await self._delete(url)
1 change: 0 additions & 1 deletion custom_components/growstuff/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from homeassistant.const import Platform

DOMAIN = "growstuff"
_API_URL = "https://www.growstuff.org/api/v1"

PLATFORMS = [Platform.SENSOR, Platform.TODO]

Expand Down
19 changes: 10 additions & 9 deletions custom_components/growstuff/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@
import logging
from homeassistant.helpers.entity import Entity

from .api.client import GrowstuffApiClient

_LOGGER = logging.getLogger(__name__)


class GrowstuffEntity(Entity):
"""Base class for Growstuff entities."""

def __init__(self, data, session):
def __init__(self, data: dict, client: GrowstuffApiClient):
"""Initialize the sensor."""
self._links = data.get("links")
self._attributes = data.get("attributes")
self._relationships = data.get("relationships")
self._session = session
self._client = client
self._id = data.get("id")

@property
Expand All @@ -33,10 +35,9 @@ def entity_picture(self):
async def async_update(self):
"""Get the latest data from Growstuff and update the states."""
_LOGGER.debug("Fetching " + self._url())
async with self._session.get(self._url()) as response:
if response.status == 200:
data = await response.json()
item = data.get("data")
self._links = item.get("links")
self._attributes = item.get("attributes")
self._relationships = item.get("relationships")
data = await self._client.get_url(self._url())
if data:
item = data.get("data")
self._links = item.get("links")
self._attributes = item.get("attributes")
self._relationships = item.get("relationships")
105 changes: 35 additions & 70 deletions custom_components/growstuff/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, _API_URL, SENSOR_TYPES
from .const import DOMAIN, SENSOR_TYPES
from .entity import GrowstuffEntity
from .api.client import GrowstuffApiClient

_LOGGER = logging.getLogger(__name__)

Expand All @@ -27,96 +28,60 @@ async def async_setup_entry(
) -> None:
"""Set up all entities."""
session = async_get_clientsession(hass)
member_name = config_entry.data.get("member")
member_url = f"{_API_URL}/members?filter[login-name]={member_name}"

_LOGGER.debug("Fetching " + member_url)
async with session.get(member_url) as response:
if response.status != 200:
raise homeassistant.exceptions.ConfigEntryNotReady(
f"Member not found, check configuration: {response.status}"
)
member_result = (await response.json()).get("data")
api_client = GrowstuffApiClient(session)

if len(member_result) == 0:
member_name = config_entry.data.get("member")
member = await api_client.get_member(member_name)
if not member:
raise homeassistant.exceptions.ConfigEntryNotReady(
"Member not found, check configuration"
)

member = member_result[0]
member_id = member.get("id")

device_registry = dr.async_get(hass)
gardens_url = f"{_API_URL}/gardens?filter[owner-id]={member_id}"
gardens_result = await api_client.get_gardens(member_id)
gardens = {}
async with session.get(gardens_url) as response:
if response.status == 200:
data = await response.json()
for garden in data.get("data"):
device = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, garden.get("id"))},
name=garden.get("attributes").get("name"),
manufacturer="Growstuff",
suggested_area=garden.get("attributes").get("name"),
)
gardens[garden.get("id")] = device

for entity_type in SENSOR_TYPES:
url = f"{_API_URL}/{entity_type}?filter[owner-id]={member_id}"
# TODO: Activities may want to include all activities
if entity_type == "plantings" or entity_type == "activities" or entity_type == "seeds":
url += "&filter[finished]=false"
await add_entities_for_type(
url, entity_type, async_add_entities, session, gardens
for garden in gardens_result:
device = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, garden.get("id"))},
name=garden.get("attributes").get("name"),
manufacturer="Growstuff",
suggested_area=garden.get("attributes").get("name"),
)


async def add_entities_for_type(
url, entity_type, async_add_entities, session, gardens
):
"""Add entities for a given type until we added them all."""
_LOGGER.debug(f"Fetching {entity_type} from {url}")
async with session.get(url) as response:
if response.status != 200:
_LOGGER.error(f"Failed to fetch {entity_type}: {response.status}")
return
data = await response.json()
gardens[garden.get("id")] = device

entities = []
for item in data.get("data"):
for entity_type in SENSOR_TYPES:
if entity_type == "plantings":
garden = item.get("relationships").get("garden")
# As of https://github.com/Growstuff/growstuff/pull/4272 this will start populating.
if garden.get("data"):
garden_id = garden.get("data").get("id")
entities.append(GrowstuffPlantingSensor(item, session, gardens.get(garden_id)))
for item in await api_client.get_plantings(member_id):
garden = item.get("relationships").get("garden")
if garden and garden.get("data"):
garden_id = garden.get("data").get("id")
entities.append(GrowstuffPlantingSensor(item, api_client, gardens.get(garden_id)))
elif entity_type == "harvests":
entities.append(GrowstuffHarvestSensor(item, session))
for item in await api_client.get_harvests(member_id):
entities.append(GrowstuffHarvestSensor(item, api_client))
elif entity_type == "seeds":
entities.append(GrowstuffSeedSensor(item, session))
for item in await api_client.get_seeds(member_id):
entities.append(GrowstuffSeedSensor(item, api_client))
async_add_entities(entities)
links = data.get("links")
if "next" in links and links.get("next"):
await add_entities_for_type(
links.get("next"), entity_type, async_add_entities, session, gardens
)


class GrowstuffSensorEntity(GrowstuffEntity, SensorEntity):
"""Base class for Growstuff sensors."""

def __init__(self, data, session):
def __init__(self, data, client):
"""Initialize the sensor."""
super().__init__(data, session)
super().__init__(data, client)
self.entity_id = "sensor.growstuff_" + data.get("id")


# Device
class GrowstuffPlantingEntity(GrowstuffSensorEntity):
def __init__(self, data, session, garden_device=None):
def __init__(self, data, client, garden_device=None):
"""Initialize the sensor."""
super().__init__(data, session)
super().__init__(data, client)
self._garden_device = garden_device

@property
Expand All @@ -141,9 +106,9 @@ class GrowstuffPlantingSensor(GrowstuffPlantingEntity):
_attr_has_entity_name = True
_attr_icon = "mdi:sprout"

def __init__(self, data, session, garden_device=None):
def __init__(self, data, client, garden_device=None):
"""Initialize the sensor."""
super().__init__(data, session, garden_device)
super().__init__(data, client, garden_device)
self.entity_id = "sensor.planting_" + data.get("id")

@property
Expand Down Expand Up @@ -176,9 +141,9 @@ class GrowstuffHarvestSensor(GrowstuffSensorEntity):
_attr_has_entity_name = True
_attr_icon = "mdi:food-apple"

def __init__(self, harvest, session):
def __init__(self, harvest, client):
"""Initialize the sensor."""
super().__init__(harvest, session)
super().__init__(harvest, client)
self.entity_id = "sensor.harvest_" + harvest.get("id")

@property
Expand Down Expand Up @@ -208,9 +173,9 @@ class GrowstuffSeedSensor(GrowstuffSensorEntity):
_attr_has_entity_name = True
_attr_icon = "mdi:seed"

def __init__(self, seed, session):
def __init__(self, seed, client):
"""Initialize the sensor."""
super().__init__(seed, session)
super().__init__(seed, client)
self.entity_id = "sensor.seed_" + seed.get("id")

@property
Expand Down
Loading