Skip to content

Python Packages

GreenThumb's Python codebase is split into two focused packages and one thin compatibility shim, all located inside the rasp5/ monorepo directory.

Package Map

rasp5/
├── greenthumb-models/   ← shared models (Pi + Cloud)
├── greenthumb-rpi5/     ← Pi hardware drivers
└── greenthumb-core/     ← compatibility re-export shim

greenthumb-models

Location: rasp5/greenthumb-models/
Imported by: microcontroller-api (Pi) and greenthumb-api (Cloud)

Contains the single source of truth for all database table definitions and data-transfer schemas used across both tiers.

Package Structure

greenthumb_models/
├── models.py        # SQLModel table definitions (all entities)
└── sync_schemas.py  # Pydantic DTOs for Pi ↔ Cloud sync

Database Models (models.py)

All tables are declared as SQLModel classes (acts as both ORM model and Pydantic schema).
Cloud-only fields (last_seen_at, tailscale_ip on Device) are conditionally added when IS_CLOUD=1.
Pi-only fields (is_dirty, is_synced) are conditionally added when IS_CLOUD is unset.

from greenthumb_models.models import (
    # Identity
    AppUser, AppUserRead,
    Device, DeviceCreate, DeviceRead, DeviceUpdate,
    DeviceAdminRead,       # includes device_token, last_seen_at, tailscale_ip
    DeviceAdminUpdate,

    # Hardware catalog
    SensorModel, ActuatorModel,
    DeviceSensor, DeviceActuator,
    Variable, Unit,
    SensorCapability,

    # Cultivation tree
    PlantSpecies,
    GrowthPhase,
    Cultivation, CultivationPhase,
    Threshold,

    # Operational
    Measurement,
    Photo,
    ActuatorLog,
    SyncMetadata,
)

Key relationships

erDiagram
    APP_USER ||--o{ DEVICE : owns
    DEVICE ||--o{ DEVICE_SENSOR : has
    DEVICE ||--o{ DEVICE_ACTUATOR : has
    DEVICE ||--o{ CULTIVATION : hosts
    SENSOR_MODEL ||--o{ DEVICE_SENSOR : defines
    ACTUATOR_MODEL ||--o{ DEVICE_ACTUATOR : defines
    SENSOR_MODEL ||--o{ SENSOR_CAPABILITY : has
    VARIABLE ||--o{ SENSOR_CAPABILITY : measured_by
    VARIABLE ||--o{ MEASUREMENT : records
    DEVICE_SENSOR ||--o{ MEASUREMENT : produces
    PLANT_SPECIES ||--o{ CULTIVATION : grows
    PLANT_SPECIES ||--o{ GROWTH_PHASE : has
    CULTIVATION ||--o{ CULTIVATION_PHASE : tracks
    GROWTH_PHASE ||--o{ CULTIVATION_PHASE : instantiates
    CULTIVATION ||--o{ THRESHOLD : scopes
    GROWTH_PHASE ||--o{ THRESHOLD : scopes
    CULTIVATION ||--o{ PHOTO : documents
    DEVICE_ACTUATOR ||--o{ ACTUATOR_LOG : records

Conditional fields by tier

Field Table Present on
is_dirty device, threshold Pi only (IS_CLOUD unset)
is_synced measurement, photo Pi only
last_seen_at device Cloud only (IS_CLOUD=1)
tailscale_ip device Cloud only

Sync Schemas (sync_schemas.py)

Pure Pydantic DTOs (not SQLModel tables) used by config_builder.py, SyncClient, and /sync/ routes:

from greenthumb_models.sync_schemas import (
    DeviceConfig,       # top-level DTO passed to DeviceManager.init_from_config()
    ActivePhaseInfo,    # currently active cultivation phase (ended_at IS NULL)
    ThresholdConfig,    # one threshold with is_default_phase flag
    SensorConfig,       # one device sensor with capabilities list
    ActuatorConfig,     # one device actuator with instance + model config
    CapabilityConfig,   # one variable a sensor can measure
)

DeviceConfig is the authoritative configuration object — built by config_builder.py on both the Pi (from local DB) and the cloud (from cloud DB), and validated with DeviceConfig.model_validate(raw) on the receiving end.


greenthumb-rpi5

Location: rasp5/greenthumb-rpi5/
Imported by: microcontroller-api (Pi only — hardware-specific)

Contains all Raspberry Pi 5 hardware interfaces and the DeviceManager.

Package Structure

greenthumb_rpi5/
├── device.py     # DeviceManager — top-level device orchestration
├── sensor.py     # Sensor base class + AHT10, BMP280, TSL2561 drivers
├── actuator.py   # Actuator base class + RGBLED, WaterPump, Camera drivers
├── schemas/      # Pi-specific Pydantic schemas (SystemState, ActuatorCommand, etc.)
└── __init__.py

DeviceManager

The central object that holds runtime state for one greenhouse device:

from greenthumb_rpi5.device import DeviceManager

dm = DeviceManager(id=device_id)

# Option A — initialize from a DeviceConfig (preferred, used in lifespan)
from greenthumb_models.sync_schemas import DeviceConfig
dm.init_from_config(config: DeviceConfig)

# Option B — initialize from local DB (fallback)
with Session(engine) as session:
    dm.init(session)

# Runtime usage
data      = dm.read_data()                     # dict[id_sensor, dict[var_name, value]]
values    = dm.get_sensor_values_by_variable() # dict[id_variable, float] — flat map
saved     = dm.save_data(session)              # persist current readings → Measurement rows
await dm.command_actuator(id, command)         # thread-safe actuator command
dm.heartbeat()                                 # reset safety-mode timer
dm.reload_cultivation(session)                 # reload thresholds + active phase after begin/end

# Read-only state
thresholds = dm.get_thresholds()               # list[dict] — active thresholds
phase      = dm.get_active_phase()             # ActivePhaseInfo | None
safety     = dm.get_safety_mode()             # bool

# Key attributes
dm.id                     # int — device ID
dm.id_active_cultivation  # int | None — current cultivation
dm.actuators              # dict[int, Actuator]
dm.sensors                # dict[int, Sensor]

reload_cultivation(session)

Re-queries the local DB for the active Cultivation, its Threshold rows, and the open CultivationPhase. Updates dm.thresholds, dm.id_active_cultivation, and dm.active_phase in-place. Called by POST /settings/cultivation/begin and POST /settings/cultivation/end so the controller picks up the change on its next cycle without a restart.

Camera

from greenthumb_rpi5.actuator import Camera

camera = Camera(src='/dev/video0', width=1280, height=720)

# Capture a JPEG and write to disk
path = camera.capture_photo(
    photos_dir='/data/photos',
    device_id=1,
    cultivation_id=3,   # optional
)
# Returns: absolute path str, or None if no frame available

# MJPEG frame for streaming
frame_bytes = camera.get_latest_frame()

Sensors

from greenthumb_rpi5.sensor import AHT10, BMP280, TSL2561

aht = AHT10()
data = aht.update_state()   # dict[variable_name, value]

bmp = BMP280()
data = bmp.update_state()

tsl = TSL2561()
data = tsl.update_state()

Actuators

from greenthumb_rpi5.actuator import RGBLED, WaterPump

led = RGBLED(instance_config={"pins": {"r": 17, "g": 27, "b": 22}})
await led.command({"r": 255, "g": 128, "b": 0})

pump = WaterPump(instance_config={"pin": 18})
await pump.command({"duty_cycle": 100})

greenthumb-core

Location: rasp5/greenthumb-core/
Purpose: Backward-compatibility shim — re-exports greenthumb_models and greenthumb_rpi5 under the greenthumb_core namespace.

# These imports still work for any code written against the old namespace:
from greenthumb_core.models import Device
from greenthumb_core.rpi5 import DeviceManager

New code should import directly from greenthumb_models or greenthumb_rpi5.


CRUD Router Utility

Both the Pi API and the cloud API share a make_crud_router() helper:

from utils.api import make_crud_router
from greenthumb_models.models import PlantSpecies, PlantSpeciesCreate, PlantSpeciesRead, PlantSpeciesUpdate

router = make_crud_router(
    PlantSpecies, PlantSpeciesCreate, PlantSpeciesRead, PlantSpeciesUpdate,
    prefix="/plant-species", tags=["Plant Species"]
)

# Creates:
# GET    /plant-species/        list all
# GET    /plant-species/{id}    get one
# POST   /plant-species/        create
# PATCH  /plant-species/{id}    update
# DELETE /plant-species/{id}    delete

The Device entity uses a custom router (routes/admin/devices.py) instead of make_crud_router because device creation requires server-side token generation and id_user auto-assignment.