diff --git a/enviro/__init__.py b/enviro/__init__.py index 956e704..bd3587e 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -93,12 +93,17 @@ def stop_activity_led(): # control never returns to here, provisioning takes over completely # all the other imports, so many shiny modules -import machine, sys, os, json +import machine, sys, os, ujson from machine import RTC, ADC import phew from pcf85063a import PCF85063A import enviro.helpers as helpers +# read the state of vbus to know if we were woken up by USB +vbus_present = Pin("WL_GPIO2", Pin.IN).value() + +#BUG Temporarily disabling battery reading, as it seems to cause issues when connected to Thonny +""" # read battery voltage - we have to toggle the wifi chip select # pin to take the reading - this is probably not ideal but doesn't # seem to cause issues. there is no obvious way to shut down the @@ -113,10 +118,12 @@ def stop_activity_led(): battery_voltage /= sample_count battery_voltage = round(battery_voltage, 3) Pin(WIFI_CS_PIN).value(old_state) +""" # set up the button, external trigger, and rtc alarm pins rtc_alarm_pin = Pin(RTC_ALARM_PIN, Pin.IN, Pin.PULL_DOWN) -external_trigger_pin = Pin(EXTERNAL_INTERRUPT_PIN, Pin.IN, Pin.PULL_DOWN) +# BUG This should only be set up for Enviro Camera +# external_trigger_pin = Pin(EXTERNAL_INTERRUPT_PIN, Pin.IN, Pin.PULL_DOWN) # intialise the pcf85063a real time clock chip rtc = PCF85063A(i2c) @@ -124,6 +131,7 @@ def stop_activity_led(): rtc.enable_timer_interrupt(False) t = rtc.datetime() +# BUG ERRNO 22, EINVAL, when date read from RTC is invalid for the pico's RTC. RTC().datetime((t[0], t[1], t[2], t[6], t[3], t[4], t[5], 0)) # synch PR2040 rtc too # jazz up that console! toot toot! @@ -143,16 +151,19 @@ def stop_activity_led(): print("") - +import network # TODO this was removed from 0.0.8 def connect_to_wifi(): + """ TODO what it was changed to if phew.is_connected_to_wifi(): logging.info(f"> already connected to wifi") return True + """ wifi_ssid = config.wifi_ssid wifi_password = config.wifi_password logging.info(f"> connecting to wifi network '{wifi_ssid}'") + """ TODO what it was changed to ip = phew.connect_to_wifi(wifi_ssid, wifi_password, timeout_seconds=30) if not ip: @@ -160,13 +171,32 @@ def connect_to_wifi(): return False logging.info(" - ip address: ", ip) + """ + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + wlan.connect(wifi_ssid, wifi_password) + + start = time.ticks_ms() + while time.ticks_diff(time.ticks_ms(), start) < 30000: + if wlan.status() < 0 or wlan.status() >= 3: + break + time.sleep(0.5) - return True + seconds_to_connect = int(time.ticks_diff(time.ticks_ms(), start) / 1000) -# returns the reason we woke up -def wake_reason(): - reason = get_wake_reason() - return wake_reason_name(reason) + if wlan.status() != 3: + logging.error(f"! failed to connect to wireless network {wifi_ssid}") + return False + + # a slow connection time will drain the battery faster and may + # indicate a poor quality connection + if seconds_to_connect > 5: + logging.warn(" - took", seconds_to_connect, "seconds to connect to wifi") + + ip_address = wlan.ifconfig()[0] + logging.info(" - ip address: ", ip_address) + + return True # log the error, blink the warning led, and go back to sleep def halt(message): @@ -174,6 +204,15 @@ def halt(message): warn_led(WARN_LED_BLINK) sleep() +# log the exception, blink the warning led, and go back to sleep +def exception(exc): + import sys, io + buf = io.StringIO() + sys.print_exception(exc, buf) + logging.exception("! " + buf.getvalue()) + warn_led(WARN_LED_BLINK) + sleep() + # returns True if we've used up 90% of the internal filesystem def low_disk_space(): if not phew.remote_mount: # os.statvfs doesn't exist on remote mounts @@ -189,10 +228,13 @@ def sync_clock_from_ntp(): from phew import ntp if not connect_to_wifi(): return False + #TODO Fetch only does one attempt. Can also optionally set Pico RTC (do we want this?) timestamp = ntp.fetch() if not timestamp: + logging.error(" - failed to fetch time from ntp server") return False rtc.datetime(timestamp) # set the time on the rtc chip + logging.info(" - rtc synched") return True # set the state of the warning led (off, on, blinking) @@ -210,13 +252,18 @@ def warn_led(state): # returns the reason the board woke up from deep sleep def get_wake_reason(): + import wakeup + wake_reason = None - if button_pin.value(): + if wakeup.get_gpio_state() & (1 << BUTTON_PIN): wake_reason = WAKE_REASON_BUTTON_PRESS - elif rtc_alarm_pin.value(): + elif wakeup.get_gpio_state() & (1 << RTC_ALARM_PIN): wake_reason = WAKE_REASON_RTC_ALARM - elif not external_trigger_pin.value(): - wake_reason = WAKE_REASON_EXTERNAL_TRIGGER + # TODO Temporarily removing this as false reporting on non-camera boards + #elif not external_trigger_pin.value(): + # wake_reason = WAKE_REASON_EXTERNAL_TRIGGER + elif vbus_present: + wake_reason = WAKE_REASON_USB_POWERED return wake_reason # convert a wake reason into it's name @@ -227,26 +274,53 @@ def wake_reason_name(wake_reason): WAKE_REASON_BUTTON_PRESS: "button", WAKE_REASON_RTC_ALARM: "rtc_alarm", WAKE_REASON_EXTERNAL_TRIGGER: "external_trigger", - WAKE_REASON_RAIN_TRIGGER: "rain_sensor" + WAKE_REASON_RAIN_TRIGGER: "rain_sensor", + WAKE_REASON_USB_POWERED: "usb_powered" } return names.get(wake_reason) # get the readings from the on board sensors def get_sensor_readings(): - readings = get_board().get_sensor_readings() - readings["voltage"] = battery_voltage + seconds_since_last = 0 + now_str = helpers.datetime_string() + if helpers.file_exists("last_time.txt"): + now = helpers.timestamp(now_str) + + time_entries = [] + with open("last_time.txt", "r") as timefile: + time_entries = timefile.read().split("\n") + + # read the first line from the time file + last = now + for entry in time_entries: + if entry: + last = helpers.timestamp(entry) + break + + seconds_since_last = now - last + logging.info(f" - seconds since last reading: {seconds_since_last}") + + + readings = get_board().get_sensor_readings(seconds_since_last) + readings["voltage"] = 0.0 # battery_voltage #Temporarily removed until issue is fixed + + # write out the last time log + with open("last_time.txt", "w") as timefile: + timefile.write(now_str) + return readings # save the provided readings into a todays readings data file def save_reading(readings): # open todays reading file and save readings helpers.mkdir_safe("readings") - readings_filename = f"readings/{helpers.date_string()}.txt" + readings_filename = f"readings/{helpers.datetime_file_string()}.txt" new_file = not helpers.file_exists(readings_filename) with open(readings_filename, "a") as f: if new_file: # new readings file so write out column headings first f.write("timestamp," + ",".join(readings.keys()) + "\r\n") + # write sensor data row = [helpers.datetime_string()] for key in readings.keys(): @@ -263,10 +337,12 @@ def cache_upload(readings): "model": model, "uid": helpers.uid() } - uploads_filename = f"uploads/{helpers.datetime_string()}.json" + + uploads_filename = f"uploads/{helpers.datetime_file_string()}.json" helpers.mkdir_safe("uploads") with open(uploads_filename, "w") as upload_file: - json.dump(payload, upload_file) + #json.dump(payload, upload_file) # TODO what it was changed to + upload_file.write(ujson.dumps(payload)) # return the number of cached results waiting to be uploaded def cached_upload_count(): @@ -279,47 +355,91 @@ def is_upload_needed(): # upload cached readings to the configured destination def upload_readings(): if not connect_to_wifi(): + logging.error(f" - cannot upload readings, wifi connection failed") return False destination = config.destination - exec(f"import enviro.destinations.{destination}") - destination_module = sys.modules[f"enviro.destinations.{destination}"] - for cache_file in os.ilistdir("uploads"): - with open(f"uploads/{cache_file[0]}", "r") as upload_file: - success = destination_module.upload_reading(json.load(upload_file)) - if not success: - logging.error(f"! failed to upload '{cache_file[0]}' to {destination}") + try: + exec(f"import enviro.destinations.{destination}") + destination_module = sys.modules[f"enviro.destinations.{destination}"] + destination_module.log_destination() + + for cache_file in os.ilistdir("uploads"): + try: + with open(f"uploads/{cache_file[0]}", "r") as upload_file: + status = destination_module.upload_reading(ujson.load(upload_file)) + if status == UPLOAD_SUCCESS: + os.remove(f"uploads/{cache_file[0]}") + logging.info(f" - uploaded {cache_file[0]}") + elif status == UPLOAD_RATE_LIMITED: + # write out that we want to attempt a reupload + with open("reattempt_upload.txt", "w") as attemptfile: + attemptfile.write("") + + logging.info(f" - rate limited, going to sleep for 1 minute") + sleep(1) + else: + logging.error(f" ! failed to upload '{cache_file[0]}' to {destination}") + return False + + except OSError: + logging.error(f" ! failed to open '{cache_file[0]}'") return False - # remove the cache file now uploaded - logging.info(f" - uploaded {cache_file[0]} to {destination}") - - os.remove(f"uploads/{cache_file[0]}") + except KeyError: + logging.error(f" ! skipping '{cache_file[0]}' as it is missing data. It was likely created by an older version of the enviro firmware") + except ImportError: + logging.error(f"! cannot find destination {destination}") + return False + return True def startup(): # write startup banner into log file logging.debug("> performing startup") + # get the reason we were woken up + reason = get_wake_reason() + # give each board a chance to perform any startup it needs # =========================================================================== board = get_board() if hasattr(board, "startup"): - board.startup() + continue_startup = board.startup(reason) + # put the board back to sleep if the startup doesn't need to continue + # and the RTC has not triggered since we were awoken + if not continue_startup and not rtc.read_alarm_flag(): + logging.debug(" - wake reason: trigger") + sleep() # log the wake reason - logging.info(" - wake reason:", wake_reason()) + logging.info(" - wake reason:", wake_reason_name(reason)) # also immediately turn on the LED to indicate that we're doing something logging.debug(" - turn on activity led") pulse_activity_led(0.5) -def sleep(): + # see if we were woken to attempt a reupload + if helpers.file_exists("reattempt_upload.txt"): + os.remove("reattempt_upload.txt") + + logging.info(f"> {cached_upload_count()} cache file(s) still to upload") + if not upload_readings(): + halt("! reading upload failed") + + # if it was the RTC that woke us, go to sleep until our next scheduled reading + # otherwise continue with taking new readings etc + # Note, this *may* result in a missed reading + if reason == WAKE_REASON_RTC_ALARM: + sleep() + +def sleep(time_override=None): logging.info("> going to sleep") # make sure the rtc flags are cleared before going back to sleep - logging.debug(" - clearing and disabling timer and alarm") + logging.debug(" - clearing and disabling previous alarm") + rtc.clear_timer_flag() # TODO this was removed from 0.0.8 rtc.clear_alarm_flag() # set alarm to wake us up for next reading @@ -327,8 +447,12 @@ def sleep(): hour, minute = dt[3:5] # calculate how many minutes into the day we are - minute = math.floor(minute / config.reading_frequency) * config.reading_frequency - minute += config.reading_frequency + if time_override is not None: + minute += time_override + else: + minute = math.floor(minute / config.reading_frequency) * config.reading_frequency + minute += config.reading_frequency + while minute >= 60: minute -= 60 hour += 1 @@ -356,9 +480,13 @@ def sleep(): sys.exit() # we'll wait here until the rtc timer triggers and then reset the board - logging.debug(" - on usb power (so can't shutdown) halt and reset instead") - while not rtc.read_alarm_flag(): - time.sleep(0.25) + logging.debug(" - on usb power (so can't shutdown). Halt and wait for alarm or user reset instead") + board = get_board() + while not rtc.read_alarm_flag(): + if hasattr(board, "check_trigger"): + board.check_trigger() + + #time.sleep(0.25) if button_pin.value(): # allow button to force reset break @@ -366,4 +494,4 @@ def sleep(): logging.debug(" - reset") # reset the board - machine.reset() \ No newline at end of file + machine.reset() diff --git a/enviro/boards/grow.py b/enviro/boards/grow.py index dc4a1d8..48158cc 100644 --- a/enviro/boards/grow.py +++ b/enviro/boards/grow.py @@ -5,6 +5,8 @@ from enviro import i2c from phew import logging +CHANNEL_NAMES = ['A', 'B', 'C'] + bme280 = BreakoutBME280(i2c, 0x77) ltr559 = BreakoutLTR559(i2c) @@ -34,7 +36,7 @@ def moisture_readings(): first = None last = None ticks = 0 - while ticks < 10 and time.ticks_ms() - start <= 1000: + while ticks < 10 and time.ticks_diff(time.ticks_ms(), start) <= 1000: value = sensor.value() if last_value != value: if first == None: @@ -48,7 +50,7 @@ def moisture_readings(): continue # calculate the average tick between transitions in ms - average = (last - first) / ticks + average = time.ticks_diff(last, first) / ticks # scale the result to a 0...100 range where 0 is very dry # and 100 is standing in water # @@ -73,9 +75,9 @@ def drip_noise(): def water(moisture_levels): from enviro import config targets = [ - config.moisture_target_1, - config.moisture_target_2, - config.moisture_target_3 + config.moisture_target_a, + config.moisture_target_b, + config.moisture_target_c ] for i in range(0, 3): @@ -83,17 +85,20 @@ def water(moisture_levels): # determine a duration to run the pump for duration = round((targets[i] - moisture_levels[i]) / 25, 1) + logging.info(f"> sensor {CHANNEL_NAMES[i]} below moisture target {targets[i]} (currently at {int(moisture_levels[i])}).") + if config.auto_water: - logging.info(f"> running pump {i} for {duration} second (currently at {int(moisture_levels[i])}, target {targets[i]})") + logging.info(f" - running pump {CHANNEL_NAMES[i]} for {duration} second(s)") pump_pins[i].value(1) time.sleep(duration) pump_pins[i].value(0) else: + logging.info(f" - playing beep") for j in range(0, i + 1): drip_noise() time.sleep(0.5) -def get_sensor_readings(): +def get_sensor_readings(seconds_since_last): # bme280 returns the register contents immediately and then starts a new reading # we want the current reading so do a dummy read to discard register contents first bme280.read() @@ -103,11 +108,10 @@ def get_sensor_readings(): ltr_data = ltr559.get_reading() moisture_levels = moisture_readings() - + water(moisture_levels) # run pumps if needed from ucollections import OrderedDict - return OrderedDict({ "temperature": round(bme280_data[0], 2), "humidity": round(bme280_data[2], 2), diff --git a/enviro/boards/indoor.py b/enviro/boards/indoor.py index 2b8b794..a06bc24 100644 --- a/enviro/boards/indoor.py +++ b/enviro/boards/indoor.py @@ -40,7 +40,7 @@ def colour_temperature_from_rgbc(r, g, b, c): ct = 10000 return round(ct) -def get_sensor_readings(): +def get_sensor_readings(seconds_since_last): data = bme688.read() temperature = round(data[0], 2) @@ -63,5 +63,5 @@ def get_sensor_readings(): "gas_resistance": gas_resistance, "aqi": aqi, "luminance": lux_from_rgbc(r, g, b, c), - "color_temperature": colour_temperature_from_rgbc(r, g, b, c), + "color_temperature": colour_temperature_from_rgbc(r, g, b, c) }) \ No newline at end of file diff --git a/enviro/boards/urban.py b/enviro/boards/urban.py index 4180fd5..6f5a07e 100644 --- a/enviro/boards/urban.py +++ b/enviro/boards/urban.py @@ -5,6 +5,9 @@ from phew import logging from enviro import i2c +# how long to capture the microphone signal for when taking a reading, in milliseconds +MIC_SAMPLE_TIME_MS = 500 + sensor_reset_pin = Pin(9, Pin.OUT, value=True) sensor_enable_pin = Pin(10, Pin.OUT, value=False) boost_enable_pin = Pin(11, Pin.OUT, value=False) @@ -31,7 +34,7 @@ def particulates(particulate_data, measure): multiplier = 10 if measure >= PM0_3_PER_LITRE else 1 return ((particulate_data[measure * 2] << 8) | particulate_data[measure * 2 + 1]) * multiplier -def get_sensor_readings(): +def get_sensor_readings(seconds_since_last): # bme280 returns the register contents immediately and then starts a new reading # we want the current reading so do a dummy read to discard register contents first bme280.read() @@ -52,25 +55,25 @@ def get_sensor_readings(): sensor_enable_pin.value(False) boost_enable_pin.value(False) - sample_time_ms = 500 + logging.debug(" - taking microphone reading") start = time.ticks_ms() min_value = 1.65 max_value = 1.65 - while time.ticks_ms() - start < sample_time_ms: - value = noise_adc.read_u16() / (3.3 * 65535) + while time.ticks_diff(time.ticks_ms(), start) < MIC_SAMPLE_TIME_MS: + value = (noise_adc.read_u16() * 3.3) / 65535 min_value = min(min_value, value) max_value = max(max_value, value) - noise_vpp = round((max_value - min_value), 3) + noise_vpp = max_value - min_value from ucollections import OrderedDict return OrderedDict({ "temperature": round(bme280_data[0], 2), "humidity": round(bme280_data[2], 2), "pressure": round(bme280_data[1] / 100.0, 2), - "noise": round(noise_vpp, 2), + "noise": round(noise_vpp, 3), "pm1": particulates(particulate_data, PM1_UGM3), "pm2_5": particulates(particulate_data, PM2_5_UGM3), - "pm10": particulates(particulate_data, PM10_UGM3), + "pm10": particulates(particulate_data, PM10_UGM3) }) diff --git a/enviro/boards/weather.py b/enviro/boards/weather.py index a64328e..3fc4b8e 100644 --- a/enviro/boards/weather.py +++ b/enviro/boards/weather.py @@ -1,11 +1,12 @@ -import time, math +import time, math, os from breakout_bme280 import BreakoutBME280 from breakout_ltr559 import BreakoutLTR559 from machine import Pin, PWM from pimoroni import Analog -from enviro import i2c, hold_vsys_en_pin +from enviro import i2c, activity_led import enviro.helpers as helpers from phew import logging +from enviro.constants import WAKE_REASON_RTC_ALARM, WAKE_REASON_BUTTON_PRESS # amount of rain required for the bucket to tip in mm RAIN_MM_PER_TICK = 0.2794 @@ -21,8 +22,11 @@ wind_direction_pin = Analog(26) wind_speed_pin = Pin(9, Pin.IN, Pin.PULL_UP) +rain_pin = Pin(10, Pin.IN, Pin.PULL_DOWN) +last_rain_trigger = False -def startup(): +def startup(reason): + global last_rain_trigger import wakeup # check if rain sensor triggered wake @@ -36,7 +40,7 @@ def startup(): rain_entries = rainfile.read().split("\n") # add new entry - logging.info("> add new rain trigger at {helpers.datetime_string()}") + logging.info(f"> add new rain trigger at {helpers.datetime_string()}") rain_entries.append(helpers.datetime_string()) # limit number of entries to 190 - each entry is 21 bytes including @@ -48,8 +52,44 @@ def startup(): with open("rain.txt", "w") as rainfile: rainfile.write("\n".join(rain_entries)) - # go immediately back to sleep, we'll wake up at next scheduled reading - hold_vsys_en_pin.init(Pin.IN) + last_rain_trigger = True + + # if we were woken by the RTC or a Poke continue with the startup + return (reason is WAKE_REASON_RTC_ALARM + or reason is WAKE_REASON_BUTTON_PRESS) + + # there was no rain trigger so continue with the startup + return True + +def check_trigger(): + global last_rain_trigger + rain_sensor_trigger = rain_pin.value() + + if rain_sensor_trigger and not last_rain_trigger: + activity_led(100) + time.sleep(0.05) + activity_led(0) + + # read the current rain entries + rain_entries = [] + if helpers.file_exists("rain.txt"): + with open("rain.txt", "r") as rainfile: + rain_entries = rainfile.read().split("\n") + + # add new entry + logging.info(f"> add new rain trigger at {helpers.datetime_string()}") + rain_entries.append(helpers.datetime_string()) + + # limit number of entries to 190 - each entry is 21 bytes including + # newline so this keeps the total rain.txt filesize just under one + # filesystem block (4096 bytes) + rain_entries = rain_entries[-190:] + + # write out adjusted rain log + with open("rain.txt", "w") as rainfile: + rainfile.write("\n".join(rain_entries)) + + last_rain_trigger = rain_sensor_trigger def wind_speed(sample_time_ms=1000): # get initial sensor state @@ -118,34 +158,29 @@ def wind_direction(): return closest_index * 45 -def timestamp(dt): - year = int(dt[0:4]) - month = int(dt[5:7]) - day = int(dt[8:10]) - hour = int(dt[11:13]) - minute = int(dt[14:16]) - second = int(dt[17:19]) - return time.mktime((year, month, day, hour, minute, second, 0, 0)) - -def rainfall(): - if not helpers.file_exists("rain.txt"): - return 0 - - now = timestamp(helpers.datetime_string()) - with open("rain.txt", "r") as rainfile: - rain_entries = rainfile.read().split("\n") - - # count how many rain ticks in past hour +def rainfall(seconds_since_last): amount = 0 - for entry in rain_entries: - if entry: - ts = timestamp(entry) - if now - ts < 60 * 60: - amount += RAIN_MM_PER_TICK - - return amount - -def get_sensor_readings(): + now = helpers.timestamp(helpers.datetime_string()) + if helpers.file_exists("rain.txt"): + with open("rain.txt", "r") as rainfile: + rain_entries = rainfile.read().split("\n") + + # count how many rain ticks since the last reading + for entry in rain_entries: + if entry: + ts = helpers.timestamp(entry) + if now - ts < seconds_since_last: + amount += RAIN_MM_PER_TICK + + os.remove("rain.txt") + + per_second = 0 + if seconds_since_last > 0: + per_second = amount / seconds_since_last + + return amount, per_second + +def get_sensor_readings(seconds_since_last): # bme280 returns the register contents immediately and then starts a new reading # we want the current reading so do a dummy read to discard register contents first bme280.read() @@ -153,15 +188,16 @@ def get_sensor_readings(): bme280_data = bme280.read() ltr_data = ltr559.get_reading() + rain, rain_per_second = rainfall(seconds_since_last) from ucollections import OrderedDict return OrderedDict({ "temperature": round(bme280_data[0], 2), "humidity": round(bme280_data[2], 2), "pressure": round(bme280_data[1] / 100.0, 2), - "light": round(ltr_data[BreakoutLTR559.LUX], 2), + "luminance": round(ltr_data[BreakoutLTR559.LUX], 2), "wind_speed": wind_speed(), - "rain": rainfall(), + "rain": rain, + "rain_per_second": rain_per_second, "wind_direction": wind_direction() }) - diff --git a/enviro/config_template.py b/enviro/config_template.py index 8cc8641..92d36d3 100644 --- a/enviro/config_template.py +++ b/enviro/config_template.py @@ -15,7 +15,7 @@ # how often to wake up and take a reading (in minutes) reading_frequency = 15 -# where to upload to ("web_hook", "mqtt", "adafruitio") +# where to upload to ("http", "mqtt", "adafruit_io", "influxdb") destination = None # how often to upload data (number of cached readings) @@ -43,6 +43,6 @@ # grow specific settings auto_water = False -moisture_target_1 = 50 -moisture_target_2 = 50 -moisture_target_3 = 50 \ No newline at end of file +moisture_target_a = 50 +moisture_target_b = 50 +moisture_target_c = 50 \ No newline at end of file diff --git a/enviro/constants.py b/enviro/constants.py index de1b2d2..8d57b80 100644 --- a/enviro/constants.py +++ b/enviro/constants.py @@ -26,8 +26,14 @@ WAKE_REASON_RTC_ALARM = 3 WAKE_REASON_EXTERNAL_TRIGGER = 4 WAKE_REASON_RAIN_TRIGGER = 5 +WAKE_REASON_USB_POWERED = 6 # warning led states WARN_LED_OFF = 0 WARN_LED_ON = 1 WARN_LED_BLINK = 2 + +# upload status +UPLOAD_SUCCESS = 0 +UPLOAD_FAILED = 1 +UPLOAD_RATE_LIMITED = 2 diff --git a/enviro/destinations/adafruit_io.py b/enviro/destinations/adafruit_io.py index 742e860..4409bbb 100644 --- a/enviro/destinations/adafruit_io.py +++ b/enviro/destinations/adafruit_io.py @@ -1,6 +1,11 @@ +from enviro import logging +from enviro.constants import UPLOAD_SUCCESS, UPLOAD_FAILED, UPLOAD_RATE_LIMITED import urequests import config +def log_destination(): + logging.info(f"> uploading cached readings to Adafruit.io: {config.adafruit_io_username}") + def upload_reading(reading): # create adafruit.io payload format payload = { @@ -12,7 +17,10 @@ def upload_reading(reading): nickname = config.nickname for key, value in reading["readings"].items(): key = key.replace("_", "-") - payload["feeds"].append({"key": f"{nickname}-{key}", "value": value}) + payload["feeds"].append({ + "key": f"{nickname}-{key}", + "value": value + }) # send the payload username = config.adafruit_io_username @@ -22,8 +30,14 @@ def upload_reading(reading): try: result = urequests.post(url, json=payload, headers=headers) result.close() - return result.status_code == 200 + if result.status_code == 429: + return UPLOAD_RATE_LIMITED + + if result.status_code == 200: + return UPLOAD_SUCCESS + + logging.debug(f" - upload issue ({result.status_code} {result.reason})") except: - pass - - return False \ No newline at end of file + logging.debug(f" - an exception occurred when uploading") + + return UPLOAD_FAILED diff --git a/enviro/destinations/http.py b/enviro/destinations/http.py index 0b5b42d..681bbd7 100644 --- a/enviro/destinations/http.py +++ b/enviro/destinations/http.py @@ -1,6 +1,11 @@ +from enviro import logging +from enviro.constants import UPLOAD_SUCCESS, UPLOAD_FAILED import urequests import config +def log_destination(): + logging.info(f"> uploading cached readings to url: {config.custom_http_url}") + def upload_reading(reading): url = config.custom_http_url @@ -12,8 +17,12 @@ def upload_reading(reading): # post reading data to http endpoint result = urequests.post(url, auth=auth, json=reading) result.close() - return result.status_code in [200, 201, 202] + + if result.status_code in [200, 201, 202]: + return UPLOAD_SUCCESS + + logging.debug(f" - upload issue ({result.status_code} {result.reason})") except: - pass + logging.debug(f" - an exception occurred when uploading") - return False \ No newline at end of file + return UPLOAD_FAILED diff --git a/enviro/destinations/influxdb.py b/enviro/destinations/influxdb.py index b449aac..57e013d 100644 --- a/enviro/destinations/influxdb.py +++ b/enviro/destinations/influxdb.py @@ -1,3 +1,5 @@ +from enviro import logging +from enviro.constants import UPLOAD_SUCCESS, UPLOAD_FAILED import urequests, time import config @@ -13,6 +15,9 @@ def url_encode(t): result += f"%{ord(c):02X}" return result +def log_destination(): + logging.info(f"> uploading cached readings to Influxdb bucket: {config.influxdb_bucket}") + def upload_reading(reading): bucket = config.influxdb_bucket @@ -46,8 +51,12 @@ def upload_reading(reading): # post reading data to http endpoint result = urequests.post(url, headers=headers, data=payload) result.close() - return result.status_code == 204 # why 204? we'll never know... + + if result.status_code == 204: # why 204? we'll never know... + return UPLOAD_SUCCESS + + logging.debug(f" - upload issue ({result.status_code} {result.reason})") except: - pass + logging.debug(f" - an exception occurred when uploading") - return False \ No newline at end of file + return UPLOAD_FAILED \ No newline at end of file diff --git a/enviro/destinations/mqtt.py b/enviro/destinations/mqtt.py index 804cf52..e6d26cf 100644 --- a/enviro/destinations/mqtt.py +++ b/enviro/destinations/mqtt.py @@ -1,7 +1,12 @@ +from enviro import logging +from enviro.constants import UPLOAD_SUCCESS, UPLOAD_FAILED from enviro.mqttsimple import MQTTClient import ujson import config +def log_destination(): + logging.info(f"> uploading cached readings to MQTT broker: {config.mqtt_broker_address}") + def upload_reading(reading): server = config.mqtt_broker_address username = config.mqtt_broker_username @@ -10,12 +15,12 @@ def upload_reading(reading): try: # attempt to publish reading - mqtt_client = MQTTClient(reading["uid"], server, user=username, password=password) + mqtt_client = MQTTClient(reading["uid"], server, user=username, password=password, keepalive=60) mqtt_client.connect() mqtt_client.publish(f"enviro/{nickname}", ujson.dumps(reading), retain=True) mqtt_client.disconnect() - return True + return UPLOAD_SUCCESS except: - pass + logging.debug(f" - an exception occurred when uploading") - return False \ No newline at end of file + return UPLOAD_FAILED diff --git a/enviro/helpers.py b/enviro/helpers.py index c2e123a..a9ead63 100644 --- a/enviro/helpers.py +++ b/enviro/helpers.py @@ -1,6 +1,5 @@ from enviro.constants import * - -import machine, os +import machine, os, time # miscellany # =========================================================================== @@ -8,10 +7,23 @@ def datetime_string(): dt = machine.RTC().datetime() return "{0:04d}-{1:02d}-{2:02d}T{4:02d}:{5:02d}:{6:02d}Z".format(*dt) +def datetime_file_string(): + dt = machine.RTC().datetime() + return "{0:04d}-{1:02d}-{2:02d}T{4:02d}_{5:02d}_{6:02d}Z".format(*dt) + def date_string(): dt = machine.RTC().datetime() return "{0:04d}-{1:02d}-{2:02d}".format(*dt) +def timestamp(dt): + year = int(dt[0:4]) + month = int(dt[5:7]) + day = int(dt[8:10]) + hour = int(dt[11:13]) + minute = int(dt[14:16]) + second = int(dt[17:19]) + return time.mktime((year, month, day, hour, minute, second, 0, 0)) + def uid(): return "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}".format(*machine.unique_id()) diff --git a/enviro/html/header.html b/enviro/html/header.html index bdebc4e..070ca6b 100644 --- a/enviro/html/header.html +++ b/enviro/html/header.html @@ -43,7 +43,7 @@ select {appearance: none; font-family: var(--theme-font); outline: none; background-color: var(--black); color: var(--white); border-radius: var(--input-radius); padding: 1rem 2rem; margin: 1rem 0; border: 0; cursor: pointer; font-size: 1.6rem;} fieldset {display: grid;} fieldset input[type="text"] {box-shadow: none !important;} - input[type="text"], input[type="email"], input[type="password"] {appearance: none; font-family: var(--theme-font); outline: none; background-color: var(--black); color: var(--white); border-radius: var(--input-radius); padding: 1rem 2rem; margin: 1rem 0; border: 0; cursor: pointer; font-size: 1.6rem;} + input[type="text"], input[type="email"], input[type="password"], input[type="number"] {appearance: none; font-family: var(--theme-font); outline: none; background-color: var(--black); color: var(--white); border-radius: var(--input-radius); padding: 1rem 2rem; margin: 1rem 0; border: 0; cursor: pointer; font-size: 1.6rem;} input[type="checkbox"] {cursor: pointer; appearance: none;} input[type="checkbox"]::before {content: ""; color: var(--white); line-height: 0.9rem; font-size: 1.6rem; border: solid 1px var(--grey); width: 1rem; height: 1rem; margin: 0rem 0.1rem 0rem 0rem; vertical-align: middle; display: inline-block; position: relative; top: -0.1rem;} input[type="checkbox"]:checked::before {content: "×"; background-color: var(--black); border: solid 1px var(--black);} diff --git a/enviro/html/provision-step-4-destination.html b/enviro/html/provision-step-4-destination.html index fe28a73..afacdc4 100644 --- a/enviro/html/provision-step-4-destination.html +++ b/enviro/html/provision-step-4-destination.html @@ -140,7 +140,7 @@