Skip to content

Commit

Permalink
Merging SoilMoisturePoller and WateringEventPoller
Browse files Browse the repository at this point in the history
It turns out things are a lot simpler when we combine the poller for checking
soil moisture with the poller responsible for running the pump based on soil
moisture. Now, the SoilWateringPoller can check soil moisture, store the
record in the DB, but then use the same reading to determine if it needs to
pump water.

Because of this, we can get rid of the get_latest method on SoilMoistureStore,
which is nice because now all the DbStore classes have the exact sameinterface
and we can delete the old SoilMoisturePoller and not add much code to the
former WateringEventPoller.
  • Loading branch information
mtlynch committed Mar 26, 2017
1 parent a562f1f commit 4c4d019
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 139 deletions.
13 changes: 0 additions & 13 deletions greenpithumb/db_store.py
Expand Up @@ -137,19 +137,6 @@ def insert(self, soil_moisture_record):
soil_moisture_record.soil_moisture))
self._connection.commit()

def get_latest(self):
"""Returns the most recent soil moisture reading."""
query = '''SELECT soil_moisture FROM soil_moisture
ORDER BY timestamp DESC
LIMIT 1;'''

self._cursor.execute(query)
results = self._cursor.fetchall()
if not results:
return None
else:
return results[0][0]

def get(self):
"""Retrieves timestamp and soil moisture readings.
Expand Down
13 changes: 11 additions & 2 deletions greenpithumb/greenpithumb.py
Expand Up @@ -43,17 +43,26 @@ def make_sensor_pollers(poll_interval, wiring_config, record_queue):
poller_factory = poller.SensorPollerFactory(local_clock, poll_interval,
record_queue)

# Temporary PumpManager class until we finish the real class.
class DummyPumpManager(object):

def pump_if_needed(self, _):
return 0

pump_manager = DummyPumpManager()

return [
poller_factory.create_temperature_poller(
temperature_sensor.TemperatureSensor(local_dht11)),
poller_factory.create_humidity_poller(
humidity_sensor.HumiditySensor(local_dht11)),
poller_factory.create_moisture_poller(
poller_factory.create_soil_watering_poller(
moisture_sensor.MoistureSensor(
adc,
pi_io.IO(GPIO), wiring_config.adc_channels.soil_moisture_sensor,
wiring_config.gpio_pins.soil_moisture_1,
wiring_config.gpio_pins.soil_moisture_2, local_clock)),
wiring_config.gpio_pins.soil_moisture_2, local_clock),
pump_manager),
poller_factory.create_ambient_light_poller(
light_sensor.LightSensor(adc,
wiring_config.adc_channels.light_sensor)),
Expand Down
84 changes: 29 additions & 55 deletions greenpithumb/poller.py
Expand Up @@ -22,9 +22,10 @@ def create_humidity_poller(self, humidity_sensor):
return HumidityPoller(self._local_clock, self._poll_interval,
humidity_sensor, self._record_queue)

def create_moisture_poller(self, moisture_sensor):
return MoisturePoller(self._local_clock, self._poll_interval,
moisture_sensor, self._record_queue)
def create_soil_watering_poller(self, moisture_sensor, pump_manager):
return SoilWateringPoller(self._local_clock, self._poll_interval,
moisture_sensor, pump_manager,
self._record_queue)

def create_ambient_light_poller(self, light_sensor):
return AmbientLightPoller(self._local_clock, self._poll_interval,
Expand Down Expand Up @@ -116,31 +117,6 @@ def _poll_once(self):
db_store.HumidityRecord(self._local_clock.now(), humidity))


class MoisturePoller(SensorPollerBase):
"""Polls a soil moisture sensor and stores the readings."""

def __init__(self, local_clock, poll_interval, moisture_sensor,
record_queue):
"""Creates a MoisturePoller object.
Args:
local_clock: A local time zone clock interface.
poll_interval: An int of how often the sensor should be polled, in
seconds.
moisture_sensor: An interface for reading the soil moisture level.
record_queue: Queue on which to place moisture records for storage.
"""
super(MoisturePoller, self).__init__(local_clock, poll_interval)
self._moisture_sensor = moisture_sensor
self._record_queue = record_queue

def _poll_once(self):
"""Polls current soil moisture."""
soil_moisture = self._moisture_sensor.moisture()
self._record_queue.put(
db_store.SoilMoistureRecord(self._local_clock.now(), soil_moisture))


class AmbientLightPoller(SensorPollerBase):
"""Polls an ambient light sensor and stores the readings."""

Expand All @@ -165,49 +141,47 @@ def _poll_once(self):
db_store.AmbientLightRecord(self._local_clock.now(), ambient_light))


# TODO(mtlynch): Fix this so that it can access soil moisture data in a
# thread-safe way. Currently it won't be able to access the soil moisture store
# because the thread that calls it is different from the thread that created
# the store's DB connection.
class WateringEventPoller(SensorPollerBase):
class SoilWateringPoller(SensorPollerBase):
"""Polls for and records watering event data.
Polls for latest soil moisture readings and oversees a water pump based on
those readings.
Polls soil moisture sensor and oversees a water pump based to add water when
the moisture drops too low. Records both soil moisture and watering events.
"""

def __init__(self, local_clock, poll_interval, pump_manager,
soil_moisture_store, record_queue):
"""Creates a new WateringEventPoller object.
def __init__(self, local_clock, poll_interval, soil_moisture_sensor,
pump_manager, record_queue):
"""Creates a new SoilWateringPoller object.
Args:
local_clock: A local time zone clock interface.
poll_interval: An int of how often the data should be polled for,
in seconds.
soil_moisture_sensor: An interface for reading the soil moisture
level.
pump_manager: An interface to manage a water pump.
soil_moisture_store: An interface for retrieving soil moisture
readings.
record_queue: Queue on which to place watering event records for
storage.
record_queue: Queue on which to place soil moisture records and
watering event records for storage.
"""
super(WateringEventPoller, self).__init__(local_clock, poll_interval)
super(SoilWateringPoller, self).__init__(local_clock, poll_interval)
self._pump_manager = pump_manager
self._soil_moisture_store = soil_moisture_store
self.record_queue = record_queue
self._soil_moisture_sensor = soil_moisture_sensor
self._record_queue = record_queue

def _poll_once(self):
"""Oversees a water pump, and polls for and stores watering event data.
"""Polls soil moisture and adds water if moisture is too low.
Polls for latest soil moisture readings and feeds them to a water pump.
If the pump runs, it stores the event data.
Checks soil moisture levels and records the current level. Using the
current soil moisture level, checks if the pump needs to run, and if so,
runs the pump and records the watering event.
"""
soil_moisture = self._soil_moisture_store.latest_soil_moisture()
if soil_moisture:
ml_pumped = self._pump_manager.pump_if_needed(soil_moisture)
if ml_pumped > 0:
self.record_queue.put(
db_store.WateringEventRecord(self._local_clock.now(),
ml_pumped))
soil_moisture = self._soil_moisture_sensor.moisture()
self._record_queue.put(
db_store.SoilMoistureRecord(self._local_clock.now(), soil_moisture))
ml_pumped = self._pump_manager.pump_if_needed(soil_moisture)
if ml_pumped > 0:
self._record_queue.put(
db_store.WateringEventRecord(self._local_clock.now(),
ml_pumped))


class CameraPoller(SensorPollerBase):
Expand Down
12 changes: 0 additions & 12 deletions tests/test_db_store.py
Expand Up @@ -77,18 +77,6 @@ def test_insert_soil_moisture(self):
'2016-07-23T10:51:09.928000+00:00', 300))
self.mock_connection.commit.assert_called_once()

def test_get_latest_soil_moisture(self):
store = db_store.SoilMoistureStore(self.mock_connection)
self.mock_cursor.fetchall.return_value = [(300,)]
moisture = store.get_latest()
self.assertEqual(300, moisture)

def test_get_latest_soil_moisture_empty_database(self):
store = db_store.SoilMoistureStore(self.mock_connection)
self.mock_cursor.fetchall.return_value = []
moisture = store.get_latest()
self.assertIsNone(moisture)

def test_get_soil_moisture(self):
store = db_store.SoilMoistureStore(self.mock_connection)
self.mock_cursor.fetchall.return_value = [
Expand Down
95 changes: 38 additions & 57 deletions tests/test_poller.py
Expand Up @@ -62,25 +62,6 @@ def test_humidity_poller(self):
self.record_queue.get(block=True, timeout=TEST_TIMEOUT_SECONDS))
self.mock_local_clock.wait.assert_called_with(POLL_INTERVAL)

def test_moisture_poller(self):
with contextlib.closing(
poller.MoisturePoller(self.mock_local_clock, POLL_INTERVAL,
self.mock_sensor,
self.record_queue)) as moisture_poller:
self.mock_local_clock.now.return_value = TIMESTAMP_A
self.mock_local_clock.wait.side_effect = (
lambda _: self.clock_wait_event.set())
self.mock_sensor.moisture.return_value = 300

moisture_poller.start_polling_async()
self.clock_wait_event.wait(TEST_TIMEOUT_SECONDS)

self.assertEqual(
db_store.SoilMoistureRecord(
timestamp=TIMESTAMP_A, soil_moisture=300),
self.record_queue.get(block=True, timeout=TEST_TIMEOUT_SECONDS))
self.mock_local_clock.wait.assert_called_with(POLL_INTERVAL)

def test_ambient_light_poller(self):
with contextlib.closing(
poller.AmbientLightPoller(
Expand All @@ -101,72 +82,72 @@ def test_ambient_light_poller(self):
self.mock_local_clock.wait.assert_called_with(POLL_INTERVAL)


class WateringEventPollerTest(unittest.TestCase):
class SoilWateringPollerTest(unittest.TestCase):

def setUp(self):
self.clock_wait_event = threading.Event()
self.mock_local_clock = mock.Mock()
self.mock_pump_manager = mock.Mock()
self.mock_soil_moisture_store = mock.Mock()
self.mock_soil_moisture_sensor = mock.Mock()
self.record_queue = Queue.Queue()

def test_watering_event_poller_when_pump_run(self):
def _stop_poller(self, poller):
poller.close()
self.clock_wait_event.set()

def test_soil_watering_poller_when_pump_run(self):
with contextlib.closing(
poller.WateringEventPoller(
poller.SoilWateringPoller(
self.mock_local_clock, POLL_INTERVAL,
self.mock_pump_manager, self.mock_soil_moisture_store,
self.record_queue)) as watering_event_poller:
self.mock_soil_moisture_sensor, self.mock_pump_manager,
self.record_queue)) as soil_watering_poller:
self.mock_local_clock.now.return_value = TIMESTAMP_A
self.mock_local_clock.wait.side_effect = (
lambda _: self.clock_wait_event.set())
lambda _: self._stop_poller(soil_watering_poller))
self.mock_pump_manager.pump_if_needed.return_value = 200
self.mock_soil_moisture_store.latest_soil_moisture.return_value = (
100)
self.mock_soil_moisture_sensor.moisture.return_value = 100

watering_event_poller.start_polling_async()
soil_watering_poller.start_polling_async()
self.clock_wait_event.wait(TEST_TIMEOUT_SECONDS)
self.assertEqual(
records_expected = [
db_store.SoilMoistureRecord(
timestamp=TIMESTAMP_A, soil_moisture=100),
db_store.WateringEventRecord(
timestamp=TIMESTAMP_A, water_pumped=200.0),
self.record_queue.get(block=True, timeout=TEST_TIMEOUT_SECONDS))
timestamp=TIMESTAMP_A, water_pumped=200.0)
]
records_actual = [
self.record_queue.get(block=True, timeout=TEST_TIMEOUT_SECONDS),
self.record_queue.get(block=True, timeout=TEST_TIMEOUT_SECONDS)
]
# Should be no more items in the queue.
self.assertTrue(self.record_queue.empty())
self.assertItemsEqual(records_expected, records_actual)
self.mock_local_clock.wait.assert_called_with(POLL_INTERVAL)
self.mock_pump_manager.pump_if_needed.assert_called_with(100)

def test_watering_event_poller_when_pump_not_run(self):
def test_soil_watering_poller_when_pump_not_run(self):
with contextlib.closing(
poller.WateringEventPoller(
poller.SoilWateringPoller(
self.mock_local_clock, POLL_INTERVAL,
self.mock_pump_manager, self.mock_soil_moisture_store,
self.record_queue)) as watering_event_poller:
self.mock_soil_moisture_sensor, self.mock_pump_manager,
self.record_queue)) as soil_watering_poller:
self.mock_local_clock.now.return_value = TIMESTAMP_A
self.mock_local_clock.wait.side_effect = (
lambda _: self.clock_wait_event.set())
lambda _: self._stop_poller(soil_watering_poller))
self.mock_pump_manager.pump_if_needed.return_value = 0
self.mock_soil_moisture_store.latest_soil_moisture.return_value = (
500)
self.mock_soil_moisture_sensor.moisture.return_value = 500

watering_event_poller.start_polling_async()
soil_watering_poller.start_polling_async()
self.clock_wait_event.wait(TEST_TIMEOUT_SECONDS)
self.assertEqual(
db_store.SoilMoistureRecord(
timestamp=TIMESTAMP_A, soil_moisture=500),
self.record_queue.get(block=True, timeout=TEST_TIMEOUT_SECONDS))
# Should be no more items in the queue.
self.assertTrue(self.record_queue.empty())
self.mock_local_clock.wait.assert_called_with(POLL_INTERVAL)
self.mock_pump_manager.pump_if_needed.assert_called_with(500)

def test_watering_event_poller_when_moisture_is_None(self):
with contextlib.closing(
poller.WateringEventPoller(
self.mock_local_clock, POLL_INTERVAL,
self.mock_pump_manager, self.mock_soil_moisture_store,
self.record_queue)) as watering_event_poller:
self.mock_local_clock.wait.side_effect = (
lambda _: self.clock_wait_event.set())
self.mock_soil_moisture_store.latest_soil_moisture.return_value = (
None)

watering_event_poller.start_polling_async()
self.clock_wait_event.wait(TEST_TIMEOUT_SECONDS)
self.assertTrue(self.record_queue.empty())
self.assertFalse(self.mock_pump_manager.pump_if_needed.called)
self.mock_local_clock.wait.assert_called_with(POLL_INTERVAL)


class CameraPollerTest(unittest.TestCase):

Expand Down

0 comments on commit 4c4d019

Please sign in to comment.