Daily pump schedule implementation.
Waters the plant at fixed times each day with specified dosages. The
schedule repeats every 24 hours.
Schedule JSON format::
{
"type": "daily",
"schedule": [
{"time": "08:00", "dose": 50},
{"time": "18:00", "dose": 75}
]
}
Each entry's "time" must be an ISO 8601 time string (HH:MM or HH:MM:SS).
Each "dose" is an integer number of milliliters.
Schedule
Bases: PumpSchedule
Daily repeating watering schedule.
Waters at fixed times each day. Events are sorted chronologically.
If the current time is past all events for today, the schedule sleeps
until the first event tomorrow.
Attributes:
| Name |
Type |
Description |
schedule_list |
|
Sorted list of (time, dose) tuples.
|
Source code in pt/controller_3/src/plant_controller/pump_schedules/daily.py
| class Schedule(PumpSchedule):
"""Daily repeating watering schedule.
Waters at fixed times each day. Events are sorted chronologically.
If the current time is past all events for today, the schedule sleeps
until the first event tomorrow.
Attributes:
schedule_list: Sorted list of (time, dose) tuples.
"""
def __init__(self, schedule: Any | None):
"""Parse the schedule list into sorted (time, dose) tuples.
Args:
schedule: List of dicts with 'time' (ISO time str) and
'dose' (int, ml) keys.
"""
self.schedule_list = sorted(list(
map(
lambda event: (datetime.time.fromisoformat(event["time"]), event["dose"]),
schedule
)
),
key=lambda event: event[0]
)
def get_schedule(self) -> str | dict:
"""Return a dict describing the daily schedule and its events."""
return {
"type": "daily",
"description": "Daily watering schedule. The plant is watered each day at the given times.",
"schedule": self.schedule_list
}
async def run_schedule(self, pump_function: Coroutine[Any, int]):
"""Sleep until the next scheduled time, then pump. Repeats forever.
Args:
pump_function: Async callback to activate the pump with a dosage.
"""
if len(self.schedule_list) < 1:
_logger.warning("No watering events in schedule, skipping watering.")
await anyio.sleep_forever()
while True:
sleep_time = None
today = datetime.date.today()
current_time = datetime.datetime.now()
for event in self.schedule_list:
datetime_event = datetime.datetime.combine(today, event[0])
if current_time < datetime_event:
sleep_time_delta = datetime_event - current_time
sleep_time = sleep_time_delta.total_seconds()
dose = event[1]
break
# Current time is later than last time for the day:
if sleep_time == None:
tomorrow = today + datetime.timedelta(days=1)
tomorrows_first_event = self.schedule_list[0]
datetime_event = datetime.datetime.combine(
tomorrow, tomorrows_first_event[0]
)
sleep_time_delta = datetime_event - current_time
sleep_time = sleep_time_delta.total_seconds()
dose = tomorrows_first_event[1]
_logger.info(f"Current time is {current_time.isoformat()}. Next watering event is at {datetime_event.isoformat()} with a dose of {dose} ml. Scheduled to sleep for {sleep_time_delta}.")
await anyio.sleep(sleep_time)
await pump_function(dose)
@staticmethod
def validate_schedule_conf(schedule_conf: Any):
"""Validate that schedule_conf is a list of {time, dose} dicts.
Args:
schedule_conf: The "schedule" field from the config JSON.
Raises:
ValueError: If format requirements are not met.
"""
if not isinstance(schedule_conf, list):
raise ValueError("A schedule of type 'daily' needs a list of dictionaries containing watering events in the 'schedule' value.")
for entry in schedule_conf:
if not isinstance(entry, dict):
raise ValueError("Each entry in the daily schedules 'schedule' list must be a dictionary.")
if "time" not in entry:
raise ValueError("Each entry in the daily schedules 'schedule' list must contain a 'time' entry.")
try:
datetime.time.fromisoformat(entry["time"])
except Exception as e:
raise ValueError("Each 'time' in each entry in the daily schedules 'schedule' list must be a proper time following the ISO 8601 standard.")
if "dose" not in entry:
raise ValueError("Each entry in the daily schedules 'schedule' list must contain a 'dose' entry.")
if not isinstance(entry["dose"], int):
raise ValueError("Each 'dose' in each entry in the daily schedules 'schedule' list must be an integer.")
|
__init__(schedule)
Parse the schedule list into sorted (time, dose) tuples.
Parameters:
| Name |
Type |
Description |
Default |
schedule
|
Any | None
|
List of dicts with 'time' (ISO time str) and
'dose' (int, ml) keys.
|
required
|
Source code in pt/controller_3/src/plant_controller/pump_schedules/daily.py
| def __init__(self, schedule: Any | None):
"""Parse the schedule list into sorted (time, dose) tuples.
Args:
schedule: List of dicts with 'time' (ISO time str) and
'dose' (int, ml) keys.
"""
self.schedule_list = sorted(list(
map(
lambda event: (datetime.time.fromisoformat(event["time"]), event["dose"]),
schedule
)
),
key=lambda event: event[0]
)
|
get_schedule()
Return a dict describing the daily schedule and its events.
Source code in pt/controller_3/src/plant_controller/pump_schedules/daily.py
| def get_schedule(self) -> str | dict:
"""Return a dict describing the daily schedule and its events."""
return {
"type": "daily",
"description": "Daily watering schedule. The plant is watered each day at the given times.",
"schedule": self.schedule_list
}
|
run_schedule(pump_function)
async
Sleep until the next scheduled time, then pump. Repeats forever.
Parameters:
| Name |
Type |
Description |
Default |
pump_function
|
Coroutine[Any, int]
|
Async callback to activate the pump with a dosage.
|
required
|
Source code in pt/controller_3/src/plant_controller/pump_schedules/daily.py
| async def run_schedule(self, pump_function: Coroutine[Any, int]):
"""Sleep until the next scheduled time, then pump. Repeats forever.
Args:
pump_function: Async callback to activate the pump with a dosage.
"""
if len(self.schedule_list) < 1:
_logger.warning("No watering events in schedule, skipping watering.")
await anyio.sleep_forever()
while True:
sleep_time = None
today = datetime.date.today()
current_time = datetime.datetime.now()
for event in self.schedule_list:
datetime_event = datetime.datetime.combine(today, event[0])
if current_time < datetime_event:
sleep_time_delta = datetime_event - current_time
sleep_time = sleep_time_delta.total_seconds()
dose = event[1]
break
# Current time is later than last time for the day:
if sleep_time == None:
tomorrow = today + datetime.timedelta(days=1)
tomorrows_first_event = self.schedule_list[0]
datetime_event = datetime.datetime.combine(
tomorrow, tomorrows_first_event[0]
)
sleep_time_delta = datetime_event - current_time
sleep_time = sleep_time_delta.total_seconds()
dose = tomorrows_first_event[1]
_logger.info(f"Current time is {current_time.isoformat()}. Next watering event is at {datetime_event.isoformat()} with a dose of {dose} ml. Scheduled to sleep for {sleep_time_delta}.")
await anyio.sleep(sleep_time)
await pump_function(dose)
|
validate_schedule_conf(schedule_conf)
staticmethod
Validate that schedule_conf is a list of {time, dose} dicts.
Parameters:
| Name |
Type |
Description |
Default |
schedule_conf
|
Any
|
The "schedule" field from the config JSON.
|
required
|
Raises:
| Type |
Description |
ValueError
|
If format requirements are not met.
|
Source code in pt/controller_3/src/plant_controller/pump_schedules/daily.py
| @staticmethod
def validate_schedule_conf(schedule_conf: Any):
"""Validate that schedule_conf is a list of {time, dose} dicts.
Args:
schedule_conf: The "schedule" field from the config JSON.
Raises:
ValueError: If format requirements are not met.
"""
if not isinstance(schedule_conf, list):
raise ValueError("A schedule of type 'daily' needs a list of dictionaries containing watering events in the 'schedule' value.")
for entry in schedule_conf:
if not isinstance(entry, dict):
raise ValueError("Each entry in the daily schedules 'schedule' list must be a dictionary.")
if "time" not in entry:
raise ValueError("Each entry in the daily schedules 'schedule' list must contain a 'time' entry.")
try:
datetime.time.fromisoformat(entry["time"])
except Exception as e:
raise ValueError("Each 'time' in each entry in the daily schedules 'schedule' list must be a proper time following the ISO 8601 standard.")
if "dose" not in entry:
raise ValueError("Each entry in the daily schedules 'schedule' list must contain a 'dose' entry.")
if not isinstance(entry["dose"], int):
raise ValueError("Each 'dose' in each entry in the daily schedules 'schedule' list must be an integer.")
|