Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions src/growmax/pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from growmax import constants
from growmax.utils.mcu import get_gpio_for_mcu
from growmax.pump_tracker import record_pump_dose


PUMP_PWM_FREQ = 10000
Expand All @@ -22,6 +23,7 @@ def __init__(self, channel=1):
:param channel: One of 1, 2 or 3.
"""
self._speed = 0
self._channel = channel # Store channel for pump tracking
rp2040_pin = constants.PUMP_GPIOS[channel - 1]
self._pin = get_gpio_for_mcu(rp2040_pin)
self._gpio_pin = machine.Pin(self._pin)
Expand Down Expand Up @@ -63,6 +65,9 @@ def dose(self, speed, timeout=0.1):
"""
print(f"Dose pump on GPIO pin {self._pin} at speed {speed} for {timeout} seconds.")
if self.set_speed(speed):
# Record pump activity for API reporting
record_pump_dose(self._channel, speed, timeout)

time.sleep(timeout)
self.stop()
return True
Expand Down
127 changes: 127 additions & 0 deletions src/growmax/pump_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Pump Activity Tracker for GrowMax
Tracks pump dosing events and provides data for API reporting
"""

import utime
from typing import List, Dict, Optional


class PumpActivity:
"""Represents a single pump activity event"""

def __init__(self, position: int, speed: float, duration: float, timestamp: Optional[float] = None):
self.position = position # 1-8
self.speed = speed # 0.0 - 1.0
self.duration = duration # seconds
self.timestamp = timestamp or utime.time()
self.enabled = True
self.description = f"Pump {position} dose at {speed*100:.0f}% for {duration}s"


class PumpTracker:
"""Tracks pump activities for reporting to OpenSensor API"""

def __init__(self, max_activities: int = 50):
self.activities: List[PumpActivity] = []
self.max_activities = max_activities
self.current_session_activities: List[PumpActivity] = []

def record_pump_activity(self, position: int, speed: float, duration: float) -> None:
"""Record a pump dosing event"""
activity = PumpActivity(position, speed, duration)

# Add to current session (for immediate reporting)
self.current_session_activities.append(activity)

# Add to historical activities
self.activities.append(activity)

# Keep only the most recent activities
if len(self.activities) > self.max_activities:
self.activities = self.activities[-self.max_activities:]

print(f"Recorded pump activity: {activity.description}")
Copy link

Copilot AI Jun 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using print for production logging can clutter stdout; consider using the logging module at a debug or info level instead.

Suggested change
print(f"Recorded pump activity: {activity.description}")
logging.info(f"Recorded pump activity: {activity.description}")

Copilot uses AI. Check for mistakes.

def get_session_activities(self) -> List[Dict]:
"""Get activities from current session for API reporting"""
activities_data = []

for activity in self.current_session_activities:
activities_data.append({
"position": activity.position,
"enabled": activity.enabled,
"speed": activity.speed,
"duration": activity.duration,
"timestamp": activity.timestamp,
"description": activity.description
})

return activities_data

def clear_session_activities(self) -> None:
"""Clear current session activities after successful API report"""
self.current_session_activities.clear()

def get_recent_activities(self, minutes: int = 60) -> List[Dict]:
"""Get activities from the last N minutes"""
cutoff_time = utime.time() - (minutes * 60)
recent_activities = []

for activity in self.activities:
if activity.timestamp >= cutoff_time:
recent_activities.append({
"position": activity.position,
"enabled": activity.enabled,
"speed": activity.speed,
"duration": activity.duration,
"timestamp": activity.timestamp,
"description": activity.description
})

return recent_activities

def get_pump_statistics(self) -> Dict:
"""Get pump usage statistics"""
stats = {
"total_activities": len(self.activities),
"session_activities": len(self.current_session_activities),
"pump_usage": {}
}

# Calculate per-pump statistics
for position in range(1, 9): # Pumps 1-8
Copy link

Copilot AI Jun 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Hard-coded pump count; consider using a named constant or configuration value instead of a magic number to support changes in pump capacity.

Suggested change
for position in range(1, 9): # Pumps 1-8
for position in range(1, NUM_PUMPS + 1): # Iterate over all pumps

Copilot uses AI. Check for mistakes.
pump_activities = [a for a in self.activities if a.position == position]
total_runtime = sum(a.duration for a in pump_activities)

stats["pump_usage"][f"pump_{position}"] = {
"activations": len(pump_activities),
"total_runtime": total_runtime,
"avg_duration": total_runtime / len(pump_activities) if pump_activities else 0
}

return stats


# Global pump tracker instance
pump_tracker = PumpTracker()


def record_pump_dose(position: int, speed: float, duration: float) -> None:
"""Convenience function to record pump activity"""
pump_tracker.record_pump_activity(position, speed, duration)


def get_pump_activities_for_api() -> List[Dict]:
"""Get pump activities for API reporting"""
return pump_tracker.get_session_activities()


def clear_reported_activities() -> None:
"""Clear activities after successful API report"""
pump_tracker.clear_session_activities()


def get_pump_stats() -> Dict:
"""Get pump usage statistics"""
return pump_tracker.get_pump_statistics()
39 changes: 30 additions & 9 deletions src/growmax/routine.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from growmax.pump import Pump
from growmax.utils import api
from growmax.utils.configs import get_config_value, get_moisture_threshold_for_position
from growmax.pump_tracker import get_pump_activities_for_api, clear_reported_activities
from growmax.utils.displays import boot_sequence, display_basic_stats, display_ph_reading, display_scd4x_reading
from growmax.utils.mcu import get_gpio_for_mcu
from growmax.utils.relays import initialize_relay_board
Expand Down Expand Up @@ -113,18 +114,38 @@ def main():
report_data["pH"] = {
"pH": ph_reading
}

# Collect pump activities for reporting
pump_activities = get_pump_activities_for_api()
relay_activities = []

# Add pump activities as "pumps" data
if pump_activities:
report_data["pumps"] = {
"pumps": pump_activities
}

# Add relay activities (auto-refill)
if relay_refilled:
relay_activities.append({
"position": relay_water_position,
"enabled": True,
"seconds": relay_refill_duration,
"description": "Auto refill water reservoir"
})

if relay_activities:
report_data["relays"] = {
"relays": [
{
"position": relay_water_position,
"enabled": True,
"seconds": relay_refill_duration,
"description": "Auto refill water reservoir"
}
]
"relays": relay_activities
}
api.report_environment_data(report_data)

# Report to API
try:
api.report_environment_data(report_data)
# Clear pump activities after successful report
clear_reported_activities()
except Exception as e:
print(f"Error reporting to API: {e}")
Copy link

Copilot AI Jun 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using structured logging (e.g., logging.error) instead of print to capture error context and stack traces in production environments.

Suggested change
print(f"Error reporting to API: {e}")
logging.error(f"Error reporting to API: {e}", exc_info=True)

Copilot uses AI. Check for mistakes.
if scd40x:
display_scd4x_reading(temp, rh, ppm_carbon_dioxide)
utime.sleep(3)
Expand Down