Skip to content
Open
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
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
126 changes: 126 additions & 0 deletions src/growmax/pump_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""
Pump Activity Tracker for GrowMax
Tracks pump dosing events and provides data for API reporting
"""

import utime


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

def __init__(self, position, speed, duration, timestamp=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=50):
self.activities = []
self.max_activities = max_activities
self.current_session_activities = []

def record_pump_activity(self, position, speed, duration):
"""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):
"""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):
"""Clear current session activities after successful API report"""
self.current_session_activities.clear()

def get_recent_activities(self, minutes=60):
"""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):
"""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, speed, duration):
"""Convenience function to record pump activity"""
pump_tracker.record_pump_activity(position, speed, duration)


def get_pump_activities_for_api():
"""Get pump activities for API reporting"""
return pump_tracker.get_session_activities()


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


def get_pump_stats():
"""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