---
title: "AWDB (Air and Water Database)"
---

```{python}
#| include: false
import asyncio
import sys
import os

# Add the src directory to the path so we can import soildb
sys.path.insert(0, os.path.join(os.getcwd(), 'src'))

import soildb
```

# AWDB (Air and Water Database)

The Air and Water Database (AWDB) provides access to real-time monitoring data from NRCS networks including SCAN (Soil Climate Analysis Network), SNOTEL (SNOwpack TELemetry), and others.

## Overview

AWDB contains data from thousands of automated monitoring stations across the United States, providing:

- **Soil moisture and temperature** at multiple depths
- **Precipitation and weather data**
- **Snow water equivalent and depth**
- **Wind speed and direction**
- **Solar radiation**
- **Water supply forecasts**

## Quick Start

### Finding Data for Specific Properties in an Area

The most common workflow is finding stations that measure specific environmental properties within a geographic area. Here's the recommended approach:

```{python}
#| eval: true
from soildb.awdb.convenience import get_property_data_near, discover_stations

async def find_property_data():
    """Find and retrieve data for specific properties."""

    target_lat, target_lon = 39.7392, -104.9903
    property_name = 'soil_moisture'
    start_date = '2024-01-01'
    end_date = '2024-01-03'

    result = await get_property_data_near(
        latitude=target_lat,
        longitude=target_lon,
        property_name=property_name,
        start_date=start_date,
        end_date=end_date,
        auto_select_sensor=True
    )

    print(f"Found data at: {result['site_name']}")
    print(f"Distance: {result['metadata']['distance_km']:.1f} km")
    print(f"Sensor: {result['metadata']['element_string']}")
    print(f"Data points: {len(result['data_points'])}")
    print(f"Unit: {result.get('unit', 'N/A')}")

    if result['data_points']:
        sample = result['data_points'][0]
        print(f"Sample: {sample['timestamp'][:10]} = {sample['value']} {result.get('unit', '')}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(find_property_data())
except RuntimeError:
    asyncio.run(find_property_data())
```

### Basic Station Query (Alternative)

For more control, you can use the lower-level AWDBClient directly:

```{python}
#| eval: true
from soildb.awdb import AWDBClient

async def basic_awdb():
    async with AWDBClient() as client:
        # Get all SCAN stations in California
        stations = await client.get_stations(
            network_codes=['SCAN'],
            state_codes=['CA'],
            limit=3  # Limit for demo
        )

        print(f"Found {len(stations)} SCAN stations in California")

        # Get data from first station
        if stations:
            station = stations[0]
            data = await client.get_station_data(
                station.station_triplet,
                'SMS',  # Soil moisture
                '2024-01-01',
                '2024-01-10'
            )
            print(f"Retrieved {len(data)} soil moisture readings from {station.name}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(basic_awdb())
except RuntimeError:
    asyncio.run(basic_awdb())
```

## Station Discovery

### Recommended Workflow: Property-Based Station Search

The most effective way to find data for specific properties is to search for stations that actually measure those properties:

```{python}
#| eval: true
from soildb.awdb.convenience import discover_stations

async def property_based_search():
    """Find stations that measure specific environmental properties."""

    # Example 1: Find stations with comprehensive soil monitoring
    print("Finding stations with soil moisture and temperature sensors...")

    soil_stations = await discover_stations(
        elements=['SMS:*', 'STO:*'],  # Soil moisture AND temperature sensors
        network_codes=['SCAN'],       # Focus on SCAN network
        state_codes=['CA'],           # California only
        active_only=True,
        include_sensor_metadata=True, # Get detailed sensor info
        limit=3  # Limit for demo
    )

    print(f"Found {len(soil_stations)} SCAN stations with soil sensors")

    for station in soil_stations[:3]:  # Show first 3
        print(f"\n{station['name']} ({station['station_triplet']})")

        # Show what soil sensors are available
        if 'sensor_metadata' in station:
            soil_moisture_count = len(station['sensor_metadata'].get('soil_moisture', []))
            soil_temp_count = len(station['sensor_metadata'].get('soil_temp', []))
            print(f"   Soil moisture sensors: {soil_moisture_count}")
            print(f"   Soil temperature sensors: {soil_temp_count}")

    # Example 2: Find stations with snow measurements in mountainous areas
    print("\n\nFinding SNOTEL stations with snow data...")

    snow_stations = await discover_stations(
        network_codes=['SNTL'],
        state_codes=['CO', 'WY'],
        elements=['WTEQ:*', 'SNWD:*'],
        active_only=True,
        limit=5
    )

    print(f"Found {len(snow_stations)} SNOTEL stations with snow measurements")

    for station in snow_stations:
        elevation = station.get('elevation', 'N/A')
        print(f"   {station['name']}: {elevation} ft elevation")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(property_based_search())
except RuntimeError:
    asyncio.run(property_based_search())
```

### Advanced Filtering with Wildcards

For more complex queries, AWDB supports powerful server-side filtering:

```{python}
#| eval: true
async def advanced_filtering():
    snotel_stations = await discover_stations(
        station_triplets=['*:OR:SNTL']
    )
    print(f"Found {len(snotel_stations)} SNOTEL stations in OR")

    soil_stations = await discover_stations(
        elements=['SMS:*'],
        state_codes=['CA']
    )
    print(f"Found {len(soil_stations)} stations with soil moisture sensors in CA")

    lake_stations = await discover_stations(
        station_names=['*Lake*']
    )
    print(f"Found {len(lake_stations)} stations with lake in name")

    scan_mountain_stations = await discover_stations(
        network_codes=['SCAN'],
        state_codes=['CO', 'WY']
    )
    print(f"Found {len(scan_mountain_stations)} SCAN stations in CO/WY")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(advanced_filtering())
except RuntimeError:
    asyncio.run(advanced_filtering())
```

### Station Metadata

Stations include rich metadata:

```{python}
#| eval: true
async def station_metadata():
    async with AWDBClient() as client:
        # Get detailed station information
        stations = await client.get_stations(
            station_triplets=['2057:AL:SCAN'],
            return_station_elements=True,  # Include sensor details
            limit=1
        )

        if stations:
            station = stations[0]
            print(f"Station: {station.name}")
            print(f"Location: {station.latitude}, {station.longitude}")
            print(f"Elevation: {station.elevation} ft")
            print(f"Network: {station.network_code}")
            print(f"State: {station.state}")

            # Show available sensors
            if station.station_elements:
                print(f"Available sensors: {len(station.station_elements)}")
                for element in station.station_elements[:3]:  # Show first 3
                    print(f"  - {element['elementCode']}: {element.get('description', 'N/A')}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(station_metadata())
except RuntimeError:
    asyncio.run(station_metadata())
```

## Automatic Sensor Selection

### Intelligent Property Data Retrieval

The recommended approach for getting data for specific environmental properties:

```{python}
#| eval: true
from soildb.awdb.convenience import get_property_data_near

async def property_data_retrieval():
    """Get data for specific environmental properties with automatic sensor selection."""

    # Example 1: Soil moisture (automatically selects best available sensor)
    print("Getting soil moisture data...")
    soil_result = await get_property_data_near(
        latitude=39.7392,   # Denver, CO
        longitude=-104.9903,
        property_name='soil_moisture',
        start_date='2024-01-01',
        end_date='2024-01-03',  # Shorter date range
        auto_select_sensor=True  # Automatically find best available sensor
    )

    print(f"Found soil moisture data at: {soil_result['site_name']}")
    print(f"   Sensor: {soil_result['metadata']['element_string']}")
    print(f"   Data points: {len(soil_result['data_points'])}")
    print(f"   Unit: {soil_result.get('unit', 'N/A')}")

    # Example 2: Air temperature
    print("\nGetting air temperature data...")
    temp_result = await get_property_data_near(
        latitude=39.7392,
        longitude=-104.9903,
        property_name='air_temp',
        start_date='2024-01-01',
        end_date='2024-01-03',  # Shorter date range
        auto_select_sensor=True
    )

    print(f"Found air temperature data at: {temp_result['site_name']}")
    print(f"   Data points: {len(temp_result['data_points'])}")
    print(f"   Unit: {temp_result.get('unit', 'N/A')}")

    # Example 3: Snow water equivalent (SWE)
    print("\nGetting snow water equivalent data...")
    snow_result = await get_property_data_near(
        latitude=39.7392,
        longitude=-104.9903,
        property_name='snow_water_equivalent',
        start_date='2024-01-01',
        end_date='2024-01-03',  # Shorter date range
        auto_select_sensor=True
    )

    print(f"Found SWE data at: {snow_result['site_name']}")
    print(f"   Data points: {len(snow_result['data_points'])}")
    print(f"   Unit: {snow_result.get('unit', 'N/A')}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(property_data_retrieval())
except RuntimeError:
    asyncio.run(property_data_retrieval())
```

### Manual Sensor Specification

For advanced users who want specific sensor control:

```{python}
#| eval: true
async def manual_sensor_control():
    # Specify exact sensor depth for soil moisture
    result = await get_property_data_near(
        latitude=39.7392,
        longitude=-104.9903,
        property_name='soil_moisture',
        start_date='2024-01-01',
        end_date='2024-01-03',  # Shorter date range
        height_depth_inches=-20,  # 20 inches below surface
        auto_select_sensor=False  # Use manual specification
    )

    print(f"Manual sensor selection: {result['metadata']['element_string']}")
    print(f"Data points: {len(result['data_points'])}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(manual_sensor_control())
except RuntimeError:
    asyncio.run(manual_sensor_control())
```

## Data Retrieval

### Soil Data at Multiple Depths

Retrieve soil moisture data at multiple depths from a specific station. Depths are specified in inches below the surface (negative values).

```{python}
#| eval: true
from soildb.awdb.convenience import get_soil_moisture_by_depth

async def multi_depth_soil():
    soil_data = await get_soil_moisture_by_depth(
        station_triplet='2237:CA:SCAN',
        depths_inches=[-2, -4, -8],
        start_date='2024-01-01',
        end_date='2024-01-03'
    )

    for depth, depth_data in soil_data['depths'].items():
        readings = depth_data['data_points']
        print(f"Depth {depth}\": {len(readings)} readings")
        if readings:
            reading = readings[0]
            print(f"  Sample: {reading['timestamp'][:10]} - {reading['value']}%")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(multi_depth_soil())
except RuntimeError:
    asyncio.run(multi_depth_soil())
```

### Hourly Data Retrieval

AWDB supports sub-daily data retrieval with hourly granularity for supported monitoring networks (SCAN and SNOTEL). Hourly data provides much higher temporal resolution than daily data and is useful for studying rapid changes in soil moisture, temperature, and weather conditions.

#### Basic Hourly Data Retrieval

Retrieve hourly data from the Alabama Hills SCAN station (`2237:CA:SCAN`) using `duration='HOURLY'`. The element code `SMS:-2:1` is the soil moisture sensor at 2 inches depth. Timestamps are timezone-aware (e.g., `2024-12-01T00:00:00-08:00`).

```{python}
#| eval: true
from soildb.awdb import AWDBClient

async def hourly_data_retrieval():
    """Retrieve hourly soil moisture data."""

    async with AWDBClient() as client:
        hourly_data = await client.get_station_data(
            station_triplet='2237:CA:SCAN',
            elements='SMS:-2:1',
            start_date='2024-12-01',
            end_date='2024-12-02',
            duration='HOURLY'
        )

        print(f"Retrieved {len(hourly_data)} hourly soil moisture readings")

        if hourly_data:
            for reading in hourly_data[:3]:
                print(f"  {reading.timestamp.isoformat()}: {reading.value}% (offset: {reading.station_timezone_offset}h)")

        if len(hourly_data) > 1:
            num_days = (hourly_data[-1].timestamp.date() - hourly_data[0].timestamp.date()).days + 1
            readings_per_day = len(hourly_data) / num_days if num_days > 0 else 0
            print(f"\nData spans {num_days} days with {readings_per_day:.1f} readings per day")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(hourly_data_retrieval())
except RuntimeError:
    asyncio.run(hourly_data_retrieval())
```

#### About Timezone Information

**Important:** All hourly data from AWDB includes station timezone information. The library automatically applies the station's timezone offset to hourly timestamps, ensuring that timestamps represent local station time rather than UTC.

- Timestamps are localized to the station's timezone using the `dataTimeZone` field from station metadata
- Each data point includes `station_timezone_offset` showing the UTC offset in hours (e.g., -8 for PST, -5 for EST)
- Timezone-aware timestamps are in ISO 8601 format (e.g., `2024-12-01T08:00:00-08:00`)
- This applies to all hourly examples and convenience functions in this section

#### Hourly Data with Multiple Elements

Retrieve multiple sensor readings with hourly granularity. Element codes follow the format `ElementCode:HeightDepth:Ordinal`. For example: `SMS:-2:1` (soil moisture %), `STO:-2:1` (soil temperature °F). The request below retrieves both sensors at 2 inches depth.

```{python}
#| eval: true
async def hourly_multi_element_data():
    """Retrieve multiple soil sensors at hourly granularity."""

    async with AWDBClient() as client:
        elements = "SMS:-2:1,STO:-2:1"

        data = await client.get_station_data(
            station_triplet='2237:CA:SCAN',
            elements=elements,
            start_date='2024-12-01',
            end_date='2024-12-02',
            duration='HOURLY'
        )

        print(f"Retrieved {len(data)} hourly readings from multiple elements")

        by_element = {}
        for reading in data:
            element = reading.element_code or 'unknown'
            if element not in by_element:
                by_element[element] = []
            by_element[element].append(reading)

        for element_code, readings in sorted(by_element.items()):
            if readings:
                first_value = readings[0].value
                last_value = readings[-1].value
                change = last_value - first_value if (first_value and last_value) else 0
                print(f"\n{element_code}:")
                print(f"  Readings: {len(readings)}")
                print(f"  Change over period: {change:+.2f}")
                print(f"  Sample: {readings[0].timestamp.isoformat()}: {readings[0].value}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(hourly_multi_element_data())
except RuntimeError:
    asyncio.run(hourly_multi_element_data())
```

#### Comparing Hourly vs Daily Data

Compare data density and temporal resolution. Hourly data provides ~24x more readings than daily aggregation for the same period.

```{python}
#| eval: true
async def hourly_vs_daily_comparison():
    """Compare hourly and daily data retrieval patterns."""

    async with AWDBClient() as client:
        station_triplet = '2237:CA:SCAN'
        elements = 'SMS:-2:1'
        start_date = '2024-12-01'
        end_date = '2024-12-03'

        daily_data = await client.get_station_data(
            station_triplet=station_triplet,
            elements=elements,
            start_date=start_date,
            end_date=end_date,
            duration='DAILY'
        )

        hourly_data = await client.get_station_data(
            station_triplet=station_triplet,
            elements=elements,
            start_date=start_date,
            end_date=end_date,
            duration='HOURLY'
        )

        print(f"Data for {station_triplet} ({start_date} to {end_date})")
        print(f"DAILY readings:  {len(daily_data):>4} (1 value per day)")
        print(f"HOURLY readings: {len(hourly_data):>4} (24 values per day)")
        print(f"Data density: {len(hourly_data)/len(daily_data):.0f}x more data in hourly")

        # Show timestamp format differences
        if daily_data:
            print(f"\nDaily timestamp format:  {daily_data[0].timestamp}")
        if hourly_data:
            print(f"Hourly timestamp format: {hourly_data[0].timestamp}")

        if hourly_data:
            hourly_by_date = {}
            for reading in hourly_data:
                date = reading.timestamp.date()
                if date not in hourly_by_date:
                    hourly_by_date[date] = []
                hourly_by_date[date].append(reading.value)

            print("\nIntra-day variability (min-max by date):")
            for date in sorted(hourly_by_date.keys())[:3]:
                values = [v for v in hourly_by_date[date] if v is not None]
                if values:
                    min_val, max_val = min(values), max(values)
                    print(f"  {date}: {min_val:.2f}% - {max_val:.2f}% (range: {max_val-min_val:.2f}%)")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(hourly_vs_daily_comparison())
except RuntimeError:
    asyncio.run(hourly_vs_daily_comparison())
```

#### Performance Considerations for Hourly Data

When working with hourly data, keep these considerations in mind:

- **Data Volume**: Hourly data is ~24x larger than daily data for the same period
- **Request Size**: Use smaller date ranges (1-2 weeks) to avoid timeouts
- **API Chunking**: The client automatically chunks requests into manageable monthly periods

```{python}
#| eval: true
async def hourly_data_best_practices():
    """Best practices for retrieving hourly datasets."""

    async with AWDBClient() as client:
        hourly_data = await client.get_station_data(
            station_triplet='2237:CA:SCAN',
            elements='SMS:-2:1,STO:-2:1',
            start_date='2024-12-01',
            end_date='2024-12-08',
            duration='HOURLY'
        )

        print(f"Retrieved {len(hourly_data)} hourly readings")

        by_element = {}
        for reading in hourly_data:
            elem = reading.element_code or 'unknown'
            if elem not in by_element:
                by_element[elem] = []
            by_element[elem].append(reading)

        print(f"\nReadings per element:")
        for elem in sorted(by_element.keys()):
            print(f"  {elem}: {len(by_element[elem])} readings")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(hourly_data_best_practices())
except RuntimeError:
    asyncio.run(hourly_data_best_practices())
```

### Weather and Snow Data

```{python}
#| eval: true
async def weather_snow_data():
    async with AWDBClient() as client:
        stations = await client.get_stations(
            network_codes=['SNTL'],
            state_codes=['CO'],
            limit=2
        )

        for station in stations:
            print(f"\nStation: {station.name} ({station.station_triplet})")

            snow_data = await client.get_station_data(
                station.station_triplet,
                'WTEQ',
                '2024-01-01',
                '2024-01-10'
            )

            precip_data = await client.get_station_data(
                station.station_triplet,
                'PREC',
                '2024-01-01',
                '2024-01-10'
            )

            print(f"  Snow data: {len(snow_data)} readings")
            print(f"  Precip data: {len(precip_data)} readings")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(weather_snow_data())
except RuntimeError:
    asyncio.run(weather_snow_data())
```

## Forecast Data

### Water Supply Forecasts

```{python}
#| eval: true
async def forecast_data():
    async with AWDBClient() as client:
        forecasts = await client.get_forecasts(
            station_triplets=['*:CA:*'],
            element_codes=['RESC'],
            start_publication_date='2024-01-01',
            end_publication_date='2024-01-31'
        )

        print(f"Retrieved forecasts for {len(forecasts)} stations")

        for forecast in forecasts[:3]:
            print(f"\nStation: {forecast.station_triplet}")
            print(f"Forecast point: {forecast.forecast_point_name}")

            for data_point in forecast.data[:2]:
                print(f"  Period: {data_point.get('forecastPeriod')}")
                print(f"  Values: {data_point.get('forecastValues', {})}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(forecast_data())
except RuntimeError:
    asyncio.run(forecast_data())
```

## Reference Data

### Available Elements and Networks

```{python}
#| eval: true
async def reference_data():
    async with AWDBClient() as client:
        ref_data = await client.get_reference_data()

        print(f"Available networks: {len(ref_data.networks)}")
        for network in ref_data.networks[:5]:
            print(f"  {network['code']}: {network['name']}")

        print(f"\nAvailable elements: {len(ref_data.elements)}")
        soil_elements = [e for e in ref_data.elements if 'soil' in e.get('name', '').lower()]
        for element in soil_elements[:5]:
            print(f"  {element['code']}: {element['name']}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(reference_data())
except RuntimeError:
    asyncio.run(reference_data())
```

## Advanced Features

### Quality Control and Flags

```{python}
#| eval: true
async def quality_control():
    async with AWDBClient() as client:
        data = await client.get_station_data(
            '2057:AL:SCAN',
            'SMS:-2:1',
            '2024-12-01',
            '2024-12-03',
            return_flags=True,
            return_original_values=True
        )

        print(f"Retrieved {len(data)} readings with quality information")

        for reading in data[:3]:
            print(f"\nTimestamp: {reading.timestamp}")
            print(f"Value: {reading.value}")
            print(f"QC Flag: {reading.qc_flag}")
            print(f"QA Flag: {reading.qa_flag}")
            if reading.orig_value is not None:
                print(f"Original value: {reading.orig_value}")
            print(f"Flags: {reading.flags}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(quality_control())
except RuntimeError:
    asyncio.run(quality_control())
```

### Timezone-Aware Timestamps in Convenience Functions

All convenience functions that return time series data automatically include timezone information when available. Results include `station_timezone_offset_hours` in metadata (UTC offset, e.g., -8 for PST), and timestamps are ISO 8601 formatted with timezone (e.g., `2024-12-01T00:00:00-08:00`).

```{python}
#| eval: true
from soildb.awdb.convenience import get_property_data_near

async def timezone_aware_convenience_example():
    """Demonstrate timezone-aware timestamps in convenience functions."""
    
    result = await get_property_data_near(
        latitude=36.54,
        longitude=-118.10,
        property_name='soil_moisture',
        start_date='2024-12-01',
        end_date='2024-12-02',
        height_depth_inches=-2
    )

    print(f"Station: {result['site_name']}")
    print(f"Timezone offset: UTC{result['metadata']['station_timezone_offset_hours']:+d}")
    print(f"\nFirst 3 readings (all in local station time):")
    
    for point in result['data_points'][:3]:
        ts = point['timestamp']
        print(f"  {ts}: {point['value']}%")

    print(f"\nTotal: {result['metadata']['n_data_points']} readings")
```

This is particularly useful when analyzing hourly data from multiple stations across different timezones—timestamps are automatically adjusted to each station's local time.

### Nearby Stations with Sensor Metadata

```{python}
#| eval: true
from soildb.awdb.convenience import discover_stations_nearby

async def nearby_stations_with_metadata():
    # Find stations near a point with sensor information
    nearby = await discover_stations_nearby(
        latitude=39.7392,   # Denver, CO
        longitude=-104.9903,
        max_distance_km=50,
        network_codes=['SCAN'],
        limit=3,
        include_sensor_metadata=True
    )

    print(f"Found {len(nearby)} SCAN stations within 50km of Denver")

    for station in nearby:
        print(f"\n{station['name']} ({station['station_triplet']}): {station['distance_km']:.1f} km")

        if 'sensor_metadata' in station:
            sensors = station['sensor_metadata']
            for prop_name, sensor_list in sensors.items():
                print(f"  {prop_name}: {len(sensor_list)} sensors")
                for sensor in sensor_list[:2]:
                    depth = sensor.get('height_depth_inches', 'N/A')
                    print(f"    Depth: {depth}\", Ordinal: {sensor.get('ordinal', 'N/A')}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(nearby_stations_with_metadata())
except RuntimeError:
    asyncio.run(nearby_stations_with_metadata())
```

## Data Availability

### Check Station Data Availability

```{python}
#| eval: true
async def check_availability():
    async with AWDBClient() as client:
        availability = await client.check_station_data_availability(
            station_triplet='2057:AL:SCAN',
            elements='SMS',
            start_date='2024-01-01',
            end_date='2024-03-31',
            duration='MONTHLY'
        )

        print(f"Station has data: {availability['has_data']}")
        print(f"Data points found: {availability['data_points']}")
        print(f"Sample values: {len(availability['sample_values'])}")

        if availability['sample_values']:
            print("Sample readings:")
            for sample in availability['sample_values'][:3]:
                print(f"  {sample['date']}: {sample['value']}")

# Run async code
try:
    loop = asyncio.get_running_loop()
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(check_availability())
except RuntimeError:
    asyncio.run(check_availability())
```

## API Reference

### Primary Workflow Functions

#### `get_property_data_near()` - **RECOMMENDED STARTING POINT**
Get data for specific environmental properties with automatic sensor selection near a location.

```python
result = await get_property_data_near(
    latitude=39.7392,           # Target location
    longitude=-104.9903,
    property_name='soil_moisture',  # Use semantic names like 'air_temp', 'precipitation'
    start_date='2024-01-01',
    end_date='2024-01-05',
    auto_select_sensor=True     # Automatically find best available sensor
)
```

#### `discover_stations()` - **FIND STATIONS WITH SPECIFIC PROPERTIES**
Advanced station discovery with sensor metadata and flexible filtering.

```python
stations = await discover_stations(
    network_codes=['SCAN'],     # Networks to search
    state_codes=['CO'],         # Geographic filters
    elements=['SMS:*'],         # Properties to find (with wildcards)
    include_sensor_metadata=True, # Get detailed sensor info
    active_only=True            # Only active stations
)
```

### AWDBClient Methods (Lower-Level Control)

- `get_stations()` - Find stations with advanced filtering
- `get_station_data()` - Retrieve time series data with full API parameters
- `get_forecasts()` - Get forecast data
- `get_reference_data()` - Get reference information
- `find_nearby_stations()` - Find stations near a location

### Additional Convenience Functions

- `discover_stations_nearby()` - Find nearby stations with optional sensor metadata
- `get_soil_moisture_by_depth()` - Multi-depth soil moisture data
- `station_available_properties()` - Get comprehensive property/variable inventory for a station
- `station_sensors()` - Detailed sensor metadata for stations organized by property
- `station_sensor_depths()` - Get available depths/heights for a property at a station

### Deprecated Names (Still Supported)

The following names are deprecated but still work. Use the new names listed above:
- `get_monitoring_station_data()` → `get_property_data_near()`
- `find_stations_by_criteria()` → `discover_stations()`
- `get_nearby_stations()` → `discover_stations_nearby()`
- `get_soil_moisture_data()` → `get_soil_moisture_by_depth()`
- `list_available_variables()` → `station_available_properties()`
- `get_station_sensor_metadata()` → `station_sensors()`
- `get_station_sensor_heights()` → `station_sensor_depths()`

## Property Reference

### Available Properties (Semantic Names)

Use these property names with `get_property_data_near()` and `discover_stations()`:

#### Soil Properties
- `soil_moisture` - Volumetric soil moisture (%)
- `soil_temp` - Soil temperature (deg F)
- `soil_conductivity` - Soil electrical conductivity
- `soil_salinity` - Soil salinity

#### Atmospheric Properties
- `air_temp` - Air temperature (deg F)
- `air_temp_avg` - Average air temperature (deg F)
- `air_temp_max` - Maximum air temperature (deg F)
- `air_temp_min` - Minimum air temperature (deg F)
- `relative_humidity` - Relative humidity (%)
- `vapor_pressure_partial` - Partial vapor pressure
- `wind_speed` - Wind speed (mph)
- `wind_direction` - Wind direction (degrees)
- `solar_radiation` - Solar radiation (W/m2)
- `atmospheric_pressure` - Atmospheric pressure

#### Hydrological Properties
- `precipitation` - Precipitation accumulation (inches)
- `precipitation_increment` - Precipitation increment (inches)
- `snow_water_equivalent` - Snow water equivalent (inches)
- `snow_depth` - Snow depth (inches)

#### Station Health
- `battery` - Battery voltage (volts)
- `logger_temp` - Data logger temperature (deg F)

### Networks and Coverage

| Network | Best For | Geographic Coverage | Update Frequency |
|---------|----------|-------------------|------------------|
| **SCAN** | Soil moisture, comprehensive weather | Continental US | Hourly |
| **SNOTEL** | Snow, precipitation, temperature | Mountainous West | Hourly |
| **COOP** | General weather observations | Nationwide | Daily |
| **USGS** | Streamflow, reservoir levels | Rivers nationwide | 15-60 min |
| **SNOW** | Manual snow measurements | Mountainous areas | Weekly |

### Element Code Reference

For advanced users, the underlying AWDB element codes:

| Property | Element Code | Units | Notes |
|----------|--------------|-------|-------|
| soil_moisture | SMS | % volumetric | Requires depth specification |
| soil_temp | STO | deg F | Requires depth specification |
| air_temp | TOBS/TAVG | deg F | TOBS=instantaneous, TAVG=average |
| precipitation | PREC | inches | Accumulation |
| snow_water_equivalent | WTEQ | inches | SNOTEL network |
| snow_depth | SNWD | inches | SNOTEL network |
| wind_speed | WSPD | mph |  |
| solar_radiation | SRAD | W/m2 | SCAN network |
| battery | BATT | volts | Station health |

## Best Practices

### Recommended Workflow for Finding Property Data

1. **Start with Property-Based Search**: Use `discover_stations()` with `elements=['SMS:*']` to find stations that actually measure your property of interest

2. **Include Sensor Metadata**: Always use `include_sensor_metadata=True` to understand what sensors are available at each station

3. **Use Automatic Sensor Selection**: For most use cases, let `get_property_data_near()` automatically select the best available sensor with `auto_select_sensor=True`

4. **Check Data Availability**: Before requesting large datasets, verify data exists for your date range

5. **Use Appropriate Networks**:
   - **SCAN**: Soil moisture, temperature, comprehensive weather
   - **SNOTEL**: Snow, precipitation, temperature in mountains
   - **USGS**: Streamflow and reservoir data
   - **COOP**: General weather observations

### Performance Tips

6. **Server-Side Filtering**: Use API parameters (network_codes, state_codes, elements) instead of client-side filtering for better performance

7. **Limit Date Ranges**: Smaller date ranges = faster responses and lower API load

8. **Cache Station Metadata**: Station configurations don't change frequently - cache sensor metadata to avoid repeated API calls

9. **Use Appropriate Durations**: DAILY for detailed time series, MONTHLY for availability checks

### Data Quality

10. **Handle Quality Flags**: Always check QC/QA flags for data validation
11. **Consider Suspect Data**: Use `return_suspect_data=False` to filter out flagged data
12. **Monitor Data Completeness**: Check for gaps in time series data

### Common Property Mappings

| Property Name | Element Code | Units | Best Networks |
|---------------|--------------|-------|----------------|
| soil_moisture | SMS | % volumetric | SCAN |
| soil_temp | STO | deg F | SCAN |
| air_temp | TOBS/TAVG | deg F | SCAN, SNOTEL, COOP |
| precipitation | PREC | inches | SCAN, SNOTEL, COOP |
| snow_water_equivalent | WTEQ | inches | SNOTEL |
| snow_depth | SNWD | inches | SNOTEL |
| wind_speed | WSPD | mph | SCAN |
| solar_radiation | SRAD | W/m2 | SCAN |
| battery_voltage | BATT | volts | SCAN, SNOTEL |

## Error Handling

AWDB operations may raise:

- `AWDBQueryError`: Invalid parameters or no data found
- `AWDBConnectionError`: Network or server issues
- `AWDBError`: General AWDB-related errors

Always wrap AWDB calls in try-except blocks for production use.