---
title: "Greenhouse Operations"
description: "Greenhouse operations API: bays, climate readings and plantings, sized for Pixeltable Cloud in pixeltable.toml"
url: "https://www.pixeltable.com/recipes/greenhouse-bays"
source: "https://github.com/pierrebrunelle/pixeltable-greenhouse-bays"
---

# Greenhouse Operations

Greenhouse operations API: bays, climate readings and plantings, sized for Pixeltable Cloud in pixeltable.toml

One `app.py` with 3 tables, 3 computed columns, 7 HTTP routes. Needs `pixeltable[serve]>=0.7.14`.

## Tables

### bays (table)

| Column | Kind | Type or expression |
| --- | --- | --- |
| bay_id | primary key | `` |
| zone | stored | String |
| crop | stored | String (optional) |
| status | stored | String |
| label | computed on insert | `bay_label(zone, bay_id, crop)` |

- B-tree index on `zone`
- B-tree index on `status`
- B-tree index on `crop`

### readings (table)

| Column | Kind | Type or expression |
| --- | --- | --- |
| id | primary key | `pxtf.uuid.uuid7()` |
| bay_id | stored | String |
| taken_at | stored | String |
| temp_c | stored | Float |
| humidity_pct | stored | Float |
| band | computed on insert | `climate_band(temp_c, humidity_pct)` |
| vpd | computed on insert | `vpd_kpa(temp_c, humidity_pct)` |

- B-tree index on `bay_id`

### plantings (table)

| Column | Kind | Type or expression |
| --- | --- | --- |
| id | primary key | `pxtf.uuid.uuid7()` |
| bay_id | stored | String |
| crop | stored | String |
| planted_on | stored | String |
| status | stored | String |

- B-tree index on `bay_id`

## Routes (service `greenhouse_api`)

- `POST /bays`: inserts into `bays` (takes bay_id, zone, crop, status; returns bay_id, label)
- `POST /bays/status`: updates `bays` (takes status, crop; returns bay_id, status, label)
- `POST /readings`: inserts into `readings` (takes bay_id, taken_at, temp_c, humidity_pct; returns id, band, vpd)
- `POST /plantings`: inserts into `plantings` (takes bay_id, crop, planted_on, status; returns id)
- `POST /climate-band`: computes `readings` columns without storing (takes temp_c, humidity_pct; returns band, vpd)
- `GET /bays/open`: runs the query `open_bays` (takes zone)
- `GET /bays/climate`: runs the query `bay_climate` (takes bay_id)

## Run it locally

```sh
git clone https://github.com/pierrebrunelle/pixeltable-greenhouse-bays
cd pixeltable-greenhouse-bays
uv sync
uv run pxt schema update app.py greenhouse
uv run python seed.py greenhouse
uv run pxt service run app.py greenhouse --port 8000
```

## Run it on Pixeltable Cloud

```sh
uv run pxt new    # a free database, no account
# put the pxt:// address it prints in pixeltable.toml
uv run pxt db update pxt://<org>:main -f
uv run pxt schema update app.py pxt://<org>:main -f
uv run pxt service update app.py pxt://<org>:main -f
```

## app.py

At commit `ad7b4525` (https://github.com/pierrebrunelle/pixeltable-greenhouse-bays/blob/ad7b4525b322b4a51d669fdb9ebeb7cf5b55de05/app.py):

```python
"""Greenhouse Operations API built with Pixeltable.

    pxt schema update app.py greenhouse
    pxt service run app.py greenhouse
"""
import pixeltable as pxt
import pixeltable.functions as pxtf
from pixeltable.serving import FastAPIRouter

from udfs import bay_label, climate_band, vpd_kpa

# ---- tables ----
TableModel = pxt.model_base()


class Bays(TableModel, name='bays', has_default_idxs=False):
    bay_id = pxt.Column(type=pxt.String, primary_key=True)
    zone: pxt.String
    crop: pxt.String | None
    status: pxt.String              # open / planted / resting

    label = bay_label(zone, bay_id, crop)

    __indexes__ = [pxt.BtreeIndex(zone), pxt.BtreeIndex(status), pxt.BtreeIndex(crop)]


class Readings(TableModel, name='readings', has_default_idxs=False):
    id = pxt.Column(value=pxtf.uuid.uuid7(), primary_key=True)
    bay_id: pxt.String
    taken_at: pxt.String
    temp_c: pxt.Float
    humidity_pct: pxt.Float

    band = climate_band(temp_c, humidity_pct)
    vpd = vpd_kpa(temp_c, humidity_pct)

    __indexes__ = [pxt.BtreeIndex(bay_id)]


class Plantings(TableModel, name='plantings', has_default_idxs=False):
    id = pxt.Column(value=pxtf.uuid.uuid7(), primary_key=True)
    bay_id: pxt.String
    crop: pxt.String
    planted_on: pxt.String
    status: pxt.String

    __indexes__ = [pxt.BtreeIndex(bay_id)]


# ---- queries ----
@pxt.query
def open_bays(zone: str):
    """Open bays in a zone (zone + status indexes)."""
    return Bays.where((Bays.zone == zone) & (Bays.status == 'open')).select(Bays.bay_id, Bays.label).order_by(Bays.bay_id)


@pxt.query
def bay_climate(bay_id: str):
    """Readings for one bay, newest first."""
    return Readings.where(Readings.bay_id == bay_id).select(
        Readings.taken_at, Readings.temp_c, Readings.humidity_pct, Readings.band, Readings.vpd
    ).order_by(Readings.taken_at, asc=False)


# ---- routes ----
greenhouse_api = FastAPIRouter(name='greenhouse_api')
greenhouse_api.add_insert_route(Bays, path='/bays', inputs=[Bays.bay_id, Bays.zone, Bays.crop, Bays.status],
                                outputs=[Bays.bay_id, Bays.label])
greenhouse_api.add_update_route(Bays, path='/bays/status', inputs=[Bays.status, Bays.crop],
                                outputs=[Bays.bay_id, Bays.status, Bays.label])
greenhouse_api.add_insert_route(Readings, path='/readings',
                                inputs=[Readings.bay_id, Readings.taken_at, Readings.temp_c, Readings.humidity_pct],
                                outputs=[Readings.id, Readings.band, Readings.vpd])
greenhouse_api.add_insert_route(Plantings, path='/plantings',
                                inputs=[Plantings.bay_id, Plantings.crop, Plantings.planted_on, Plantings.status],
                                outputs=[Plantings.id])
greenhouse_api.add_compute_route(Readings, path='/climate-band', inputs=[Readings.temp_c, Readings.humidity_pct],
                                 outputs=[Readings.band, Readings.vpd])
greenhouse_api.add_query_route(path='/bays/open', query=open_bays, method='get')
greenhouse_api.add_query_route(path='/bays/climate', query=bay_climate, method='get')
```
