├── Adafruit_I2C.py ├── HTU21D.py ├── LICENSE ├── MCP342X.py ├── README.md ├── annual_review_analysis.py ├── bmp085.py ├── bmpBackend.py ├── create.sql ├── credentials.mysql ├── credentials.oracle.template ├── crontab.save ├── database.py ├── ds18b20_therm.py ├── i2c_base.py ├── install.sh ├── interrupt_client.py ├── interrupt_daemon.py ├── log_all_sensors-test.py ├── log_all_sensors.py ├── rpi-weather-overlay.dts ├── tgs2600.py ├── upload_to_oracle.py ├── wind_direction.json ├── wind_direction.py └── wsinstall.sh /Adafruit_I2C.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Origin: https://github.com/adafruit/Adafruit-Raspberry-Pi-Python-Code 3 | 4 | import smbus 5 | 6 | # =========================================================================== 7 | # Adafruit_I2C Base Class 8 | # =========================================================================== 9 | 10 | class Adafruit_I2C : 11 | 12 | def __init__(self, address, bus=0, debug=False): 13 | self.address = address 14 | self.bus = smbus.SMBus(bus) 15 | self.debug = debug 16 | 17 | def reverseByteOrder(self, data): 18 | "Reverses the byte order of an int (16-bit) or long (32-bit) value" 19 | # Courtesy Vishal Sapre 20 | dstr = hex(data)[2:].replace('L','') 21 | byteCount = len(dstr[::2]) 22 | val = 0 23 | for i, n in enumerate(range(byteCount)): 24 | d = data & 0xFF 25 | val |= (d << (8 * (byteCount - i - 1))) 26 | data >>= 8 27 | return val 28 | 29 | def write8(self, reg, value): 30 | "Writes an 8-bit value to the specified register/address" 31 | try: 32 | self.bus.write_byte_data(self.address, reg, value) 33 | if (self.debug): 34 | print("I2C: Wrote 0x%02X to register 0x%02X" % (value, reg)) 35 | except IOError as err: 36 | print( "Error accessing 0x%02X: Check your I2C address" % self.address) 37 | return -1 38 | 39 | def writeList(self, reg, list): 40 | "Writes an array of bytes using I2C format" 41 | try: 42 | self.bus.write_i2c_block_data(self.address, reg, list) 43 | except IOError as err: 44 | print( "Error accessing 0x%02X: Check your I2C address" % self.address) 45 | return -1 46 | 47 | def readU8(self, reg): 48 | "Read an unsigned byte from the I2C device" 49 | try: 50 | result = self.bus.read_byte_data(self.address, reg) 51 | if (self.debug): 52 | print( "I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % (self.address, result & 0xFF, reg)) 53 | return result 54 | except IOError as err: 55 | print( "Error accessing 0x%02X: Check your I2C address" % self.address) 56 | return -1 57 | 58 | def readS8(self, reg): 59 | "Reads a signed byte from the I2C device" 60 | try: 61 | result = self.bus.read_byte_data(self.address, reg) 62 | if (self.debug): 63 | print( "I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % (self.address, result & 0xFF, reg)) 64 | if (result > 127): 65 | return result - 256 66 | else: 67 | return result 68 | except IOError as err: 69 | print( "Error accessing 0x%02X: Check your I2C address" % self.address) 70 | return -1 71 | 72 | def readU16(self, reg): 73 | "Reads an unsigned 16-bit value from the I2C device" 74 | try: 75 | hibyte = self.bus.read_byte_data(self.address, reg) 76 | result = (hibyte << 8) + self.bus.read_byte_data(self.address, reg+1) 77 | if (self.debug): 78 | print( "I2C: Device 0x%02X returned 0x%04X from reg 0x%02X" % (self.address, result & 0xFFFF, reg)) 79 | return result 80 | except IOError as err: 81 | print("Error accessing 0x%02X: Check your I2C address" % self.address) 82 | return -1 83 | 84 | def readS16(self, reg): 85 | "Reads a signed 16-bit value from the I2C device" 86 | try: 87 | hibyte = self.bus.read_byte_data(self.address, reg) 88 | if (hibyte > 127): 89 | hibyte -= 256 90 | result = (hibyte << 8) + self.bus.read_byte_data(self.address, reg+1) 91 | if (self.debug): 92 | print( "I2C: Device 0x%02X returned 0x%04X from reg 0x%02X" % (self.address, result & 0xFFFF, reg)) 93 | return result 94 | except IOError as err: 95 | print( "Error accessing 0x%02X: Check your I2C address" % self.address) 96 | return -1 97 | -------------------------------------------------------------------------------- /HTU21D.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import struct 3 | import array 4 | import time 5 | import i2c_base 6 | 7 | HTU21D_ADDR = 0x40 8 | CMD_READ_TEMP_HOLD = b"\xE3" 9 | CMD_READ_HUM_HOLD = b"\xE5" 10 | CMD_READ_TEMP_NOHOLD = b"\xF3" 11 | CMD_READ_HUM_NOHOLD = b"\xF5" 12 | CMD_WRITE_USER_REG = b"\xE6" 13 | CMD_READ_USER_REG = b"\xE7" 14 | CMD_SOFT_RESET = b"\xFE" 15 | 16 | 17 | class HTU21D(object): 18 | def __init__(self): 19 | self.dev = i2c_base.i2c(HTU21D_ADDR, 1) # HTU21D 0x40, bus 1 20 | self.dev.write(CMD_SOFT_RESET) # Soft reset 21 | time.sleep(.1) 22 | 23 | def ctemp(self, sensor_temp): 24 | t_sensor_temp = sensor_temp / 65536.0 25 | return -46.85 + (175.72 * t_sensor_temp) 26 | 27 | def chumid(self, sensor_humid): 28 | t_sensor_humid = sensor_humid / 65536.0 29 | return -6.0 + (125.0 * t_sensor_humid) 30 | 31 | def temp_coefficient(self, rh_actual, temp_actual, coefficient=-0.15): 32 | return rh_actual + (25 - temp_actual) * coefficient 33 | 34 | def crc8check(self, value): 35 | # Ported from Sparkfun Arduino HTU21D Library: 36 | # https://github.com/sparkfun/HTU21D_Breakout 37 | remainder = ((value[0] << 8) + value[1]) << 8 38 | remainder |= value[2] 39 | 40 | # POLYNOMIAL = 0x0131 = x^8 + x^5 + x^4 + 1 divisor = 41 | # 0x988000 is the 0x0131 polynomial shifted to farthest 42 | # left of three bytes 43 | divisor = 0x988000 44 | 45 | for i in range(0, 16): 46 | if(remainder & 1 << (23 - i)): 47 | remainder ^= divisor 48 | divisor = divisor >> 1 49 | 50 | if remainder == 0: 51 | return True 52 | else: 53 | return False 54 | 55 | def read_temperature(self): 56 | self.dev.write(CMD_READ_TEMP_NOHOLD) # Measure temp 57 | time.sleep(.1) 58 | data = self.dev.read(3) 59 | buf = array.array('B', data) 60 | if self.crc8check(buf): 61 | temp = (buf[0] << 8 | buf[1]) & 0xFFFC 62 | return self.ctemp(temp) 63 | else: 64 | return -255 65 | 66 | def read_humidity(self): 67 | temp_actual = self.read_temperature() # For temperature coefficient compensation 68 | self.dev.write(CMD_READ_HUM_NOHOLD) # Measure humidity 69 | time.sleep(.1) 70 | data = self.dev.read(3) 71 | buf = array.array('B', data) 72 | 73 | if self.crc8check(buf): 74 | humid = (buf[0] << 8 | buf[1]) & 0xFFFC 75 | rh_actual = self.chumid(humid) 76 | 77 | rh_final = self.temp_coefficient(rh_actual, temp_actual) 78 | 79 | rh_final = 100.0 if rh_final > 100 else rh_final # Clamp > 100 80 | rh_final = 0.0 if rh_final < 0 else rh_final # Clamp < 0 81 | 82 | return rh_final 83 | else: 84 | return -255 85 | 86 | if __name__ == "__main__": 87 | obj = HTU21D() 88 | print("Temp: %s C" % obj.read_temperature()) 89 | print("Humid: %s %% rH" % obj.read_humidity()) 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 Limor Fried, Kevin Townsend and Mikey Sklar for Adafruit Industries. All rights reserved. 2 | Copyright (c) 2017, Raspberry Pi Foundation. All rights reserved. 3 | 4 | Redistribution. Redistribution and use in source and binary forms, without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions must reproduce the above copyright notice and the 9 | following disclaimer in the documentation and/or other materials 10 | provided with the distribution. 11 | * Neither the name of the Raspberry Pi Foundation nor the names of its suppliers 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | DISCLAIMER. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 16 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 17 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 18 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 22 | OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 24 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 25 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 26 | DAMAGE. 27 | -------------------------------------------------------------------------------- /MCP342X.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import struct, array, time, i2c_base 3 | 4 | CHANNEL_0 = 0 5 | CHANNEL_1 = 1 6 | 7 | CMD_ZERO = b"\x00" 8 | CMD_RESET = b"\x06" 9 | CMD_LATCH = b"\x04" 10 | CMD_CONVERSION = b"\x08" 11 | CMD_READ_CH0_16BIT = b"\x88" 12 | CMD_READ_CH1_16BIT = b"\xA8" 13 | 14 | msleep = lambda x: time.sleep(x/1000.0) 15 | 16 | class MCP342X(object): 17 | def __init__(self, address = 0x69): 18 | self.dev = i2c_base.i2c(address, 1) 19 | self.max = 32767.0 #15 bits 20 | self.vref = 2.048 21 | self.tolerance_percent = 0.5 22 | self.reset() 23 | 24 | def reset(self): 25 | self.dev.write(CMD_ZERO) 26 | self.dev.write(CMD_RESET) 27 | msleep(1) 28 | 29 | def latch(self): 30 | self.dev.write(CMD_ZERO) 31 | self.dev.write(CMD_LATCH) 32 | msleep(1) 33 | 34 | def conversion(self): 35 | self.dev.write(CMD_ZERO) 36 | self.dev.write(CMD_CONVERSION) 37 | msleep(1) 38 | 39 | def configure(self, channel = 0): 40 | if channel == 1: 41 | self.dev.write(CMD_READ_CH1_16BIT) 42 | else: 43 | self.dev.write(CMD_READ_CH0_16BIT) 44 | msleep(300) 45 | 46 | def read(self, channel = None): 47 | if channel != None: 48 | self.configure(channel) 49 | 50 | data = self.dev.read(3) 51 | buf = array.array('B', data) 52 | 53 | status = buf[2] 54 | result = None 55 | 56 | if status & 128 != 128: # check ready bit = 0 57 | result = buf[0] << 8 | buf[1] 58 | else: 59 | print("Not ready") 60 | 61 | return result 62 | 63 | if __name__ == "__main__": 64 | adc_main = MCP342X(address = 0x69) # ADC on the main HAT board 65 | adc_main.conversion() 66 | 67 | adc_air = MCP342X(address = 0x6A) # ADC on the snap off part with the AIR sensors 68 | adc_air.conversion() 69 | 70 | print("MAIN CH0: %s" % adc_main.read(CHANNEL_0)) # wind vane 71 | print("MAIN CH1: %s" % adc_main.read(CHANNEL_1)) # not populated 72 | 73 | print("AIR CH0: %s" % adc_air.read(CHANNEL_0)) # air quality 74 | print("AIR CH1: %s" % adc_air.read(CHANNEL_1)) # not populated 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Oracle Weather Station 2 | 3 | ## Installation 4 | 5 | Follow the guides and tutorials at [https://github.com/raspberrypilearning/weather\_station\_guide](https://github.com/raspberrypilearning/weather_station_guide) (published at [www.raspberrypi.org/weather-station](https://www.raspberrypi.org/weather-station/)) 6 | 7 | ## Version 8 | 9 | This repo contains the updated version of the software, re-engineered for the [Stretch version of Raspbian](https://www.raspberrypi.org/blog/raspbian-stretch/). If you are an existing Weather Station owner and are using a Pi running the Jessie version of Raspbian, then this code will not work without modification. You should flash your SD card with the [latest Raspbian image](https://www.raspberrypi.org/downloads/raspbian/) and perform a fresh install of this software (you may wish to take a copy of your local MYSQL database first). 10 | 11 | ---------- 12 | -------------------------------------------------------------------------------- /annual_review_analysis.py: -------------------------------------------------------------------------------- 1 | # This program can be used to process and analyse data from your local MYSQL or 2 | # MariaDB database. It looks ate air temperature and rainfall for 2017 but you should be able to modify it to 3 | # process different weather measurements and other time periods. 4 | # 5 | import MySQLdb 6 | import calendar 7 | import pprint 8 | import operator 9 | # Connect to local database - add your username and password 10 | db=MySQLdb.connect(user="",passwd="",db="weather") 11 | c = db.cursor() # Create a cursor 12 | 13 | # Process Rainfall data 14 | rainfall_avs={} 15 | for mon in range(1,13): # Perform for every month 16 | c.execute("""select AVG(RAINFALL) from WEATHER_MEASUREMENT WHERE MONTH(CREATED) = %s AND YEAR(CREATED) = 2017;""",(mon,)) # Change the year if required 17 | r=c.fetchone()[0] 18 | if r: 19 | rainfall_avs[calendar.month_name[mon]] = float(r) 20 | print("Average rainfall") 21 | sorted_rainfall_avs= sorted(rainfall_avs.items(), key=operator.itemgetter(1)) 22 | pprint.pprint(sorted_rainfall_avs) 23 | rainfall_tots={} 24 | for mon in range(1,13): # Perform for every month 25 | c.execute("""select SUM(RAINFALL) from WEATHER_MEASUREMENT WHERE MONTH(CREATED) = %s AND YEAR(CREATED) = 2017;""",(mon,)) # Change the year if required 26 | r=c.fetchone()[0] 27 | if r: 28 | rainfall_tots[calendar.month_name[mon]] = float(r) 29 | print("Total rainfall") 30 | sorted_rainfall_tots= sorted(rainfall_tots.items(), key=operator.itemgetter(1)) 31 | pprint.pprint(rainfall_tots) 32 | rainfall_max_av_month = max(rainfall_avs, key=lambda i: rainfall_avs[i]) 33 | rainfall_max_tots_month = max(rainfall_tots, key=lambda i: rainfall_avs[i]) 34 | print("Wettest month: " + str(rainfall_max_av_month) +" (average) " + str(rainfall_max_tots_month) +" (total) " ) 35 | rainfall_min_av_month = min(rainfall_avs, key=lambda i: rainfall_avs[i]) 36 | rainfall_min_tots_month = min(rainfall_tots, key=lambda i: rainfall_avs[i]) 37 | print("Driest month: " + str(rainfall_min_av_month) +" (average) " + str(rainfall_min_tots_month) +" (total) " ) 38 | c.execute("""select CREATED, RAINFALL from WEATHER_MEASUREMENT where RAINFALL=(select max(RAINFALL) from WEATHER_MEASUREMENT WHERE YEAR(CREATED) = 2017);""") 39 | r = c.fetchall() 40 | print("Most rain in 5 minutes):") # Find heaviest downpour 41 | for d in range(len(r)): 42 | print(str(r[d][0])+ " " + str(float(r[d][1]))) 43 | 44 | # Ambient temp 45 | amb_temp_avs={} 46 | for mon in range(1,13): 47 | c.execute("""select AVG(AMBIENT_TEMPERATURE) from WEATHER_MEASUREMENT WHERE MONTH(CREATED) = %s AND YEAR(CREATED) = 2017;""",(mon,)) 48 | r=c.fetchone()[0] 49 | if r: 50 | amb_temp_avs[calendar.month_name[mon]] = float(r) 51 | print("Average ambient temp") 52 | sorted_amb_temp_avs= sorted(amb_temp_avs.items(), key=operator.itemgetter(1)) 53 | pprint.pprint(sorted_amb_temp_avs) 54 | amb_temp_max_av_month = max(amb_temp_avs, key=lambda i: amb_temp_avs[i]) 55 | print("Hottest month: " + str(amb_temp_max_av_month) +" (average) " ) 56 | amb_temp_min_av_month = min(amb_temp_avs, key=lambda i: amb_temp_avs[i]) 57 | print("Coolest month: " + str(amb_temp_min_av_month) +" (average) " ) 58 | -------------------------------------------------------------------------------- /bmp085.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import bmpBackend 3 | 4 | class BMP085(object): 5 | def __init__(self): 6 | self.bmp = bmpBackend.BMP085(bus=1) 7 | self.temperature = 0 8 | self.pressure = 0 9 | self.lastValue = (0, 0) 10 | 11 | def get_pressure(self): 12 | return self.bmp.readPressure() * 0.01 #hPa 13 | 14 | def get_temperature(self): 15 | return self.bmp.readTemperature() 16 | -------------------------------------------------------------------------------- /bmpBackend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #Origin: https://github.com/adafruit/Adafruit-Raspberry-Pi-Python-Code/tree/master/Adafruit_BMP085 3 | 4 | import time 5 | import math 6 | 7 | from Adafruit_I2C import Adafruit_I2C 8 | 9 | # =========================================================================== 10 | # BMP085 Class 11 | # =========================================================================== 12 | 13 | class BMP085 : 14 | i2c = None 15 | 16 | # Operating Modes 17 | __BMP085_ULTRALOWPOWER = 0 18 | __BMP085_STANDARD = 1 19 | __BMP085_HIGHRES = 2 20 | __BMP085_ULTRAHIGHRES = 3 21 | 22 | # BMP085 Registers 23 | __BMP085_CAL_AC1 = 0xAA # R Calibration data (16 bits) 24 | __BMP085_CAL_AC2 = 0xAC # R Calibration data (16 bits) 25 | __BMP085_CAL_AC3 = 0xAE # R Calibration data (16 bits) 26 | __BMP085_CAL_AC4 = 0xB0 # R Calibration data (16 bits) 27 | __BMP085_CAL_AC5 = 0xB2 # R Calibration data (16 bits) 28 | __BMP085_CAL_AC6 = 0xB4 # R Calibration data (16 bits) 29 | __BMP085_CAL_B1 = 0xB6 # R Calibration data (16 bits) 30 | __BMP085_CAL_B2 = 0xB8 # R Calibration data (16 bits) 31 | __BMP085_CAL_MB = 0xBA # R Calibration data (16 bits) 32 | __BMP085_CAL_MC = 0xBC # R Calibration data (16 bits) 33 | __BMP085_CAL_MD = 0xBE # R Calibration data (16 bits) 34 | __BMP085_CONTROL = 0xF4 35 | __BMP085_TEMPDATA = 0xF6 36 | __BMP085_PRESSUREDATA = 0xF6 37 | __BMP085_READTEMPCMD = 0x2E 38 | __BMP085_READPRESSURECMD = 0x34 39 | 40 | # Private Fields 41 | _cal_AC1 = 0 42 | _cal_AC2 = 0 43 | _cal_AC3 = 0 44 | _cal_AC4 = 0 45 | _cal_AC5 = 0 46 | _cal_AC6 = 0 47 | _cal_B1 = 0 48 | _cal_B2 = 0 49 | _cal_MB = 0 50 | _cal_MC = 0 51 | _cal_MD = 0 52 | 53 | # Constructor 54 | def __init__(self, address=0x77, mode=1, bus=0, debug=False): 55 | self.i2c = Adafruit_I2C(address, bus) 56 | 57 | self.address = address 58 | self.debug = debug 59 | # Make sure the specified mode is in the appropriate range 60 | if ((mode < 0) | (mode > 3)): 61 | if (self.debug): 62 | print("Invalid Mode: Using STANDARD by default") 63 | self.mode = self.__BMP085_STANDARD 64 | else: 65 | self.mode = mode 66 | # Read the calibration data 67 | self.readCalibrationData() 68 | 69 | def readCalibrationData(self): 70 | "Reads the calibration data from the IC" 71 | self._cal_AC1 = self.i2c.readS16(self.__BMP085_CAL_AC1) # INT16 72 | self._cal_AC2 = self.i2c.readS16(self.__BMP085_CAL_AC2) # INT16 73 | self._cal_AC3 = self.i2c.readS16(self.__BMP085_CAL_AC3) # INT16 74 | self._cal_AC4 = self.i2c.readU16(self.__BMP085_CAL_AC4) # UINT16 75 | self._cal_AC5 = self.i2c.readU16(self.__BMP085_CAL_AC5) # UINT16 76 | self._cal_AC6 = self.i2c.readU16(self.__BMP085_CAL_AC6) # UINT16 77 | self._cal_B1 = self.i2c.readS16(self.__BMP085_CAL_B1) # INT16 78 | self._cal_B2 = self.i2c.readS16(self.__BMP085_CAL_B2) # INT16 79 | self._cal_MB = self.i2c.readS16(self.__BMP085_CAL_MB) # INT16 80 | self._cal_MC = self.i2c.readS16(self.__BMP085_CAL_MC) # INT16 81 | self._cal_MD = self.i2c.readS16(self.__BMP085_CAL_MD) # INT16 82 | if (self.debug): 83 | self.showCalibrationData() 84 | 85 | def showCalibrationData(self): 86 | "Displays the calibration values for debugging purposes" 87 | print( "DBG: AC1 = %6d" % (self._cal_AC1)) 88 | print( "DBG: AC2 = %6d" % (self._cal_AC2)) 89 | print( "DBG: AC3 = %6d" % (self._cal_AC3)) 90 | print( "DBG: AC4 = %6d" % (self._cal_AC4)) 91 | print( "DBG: AC5 = %6d" % (self._cal_AC5)) 92 | print( "DBG: AC6 = %6d" % (self._cal_AC6)) 93 | print( "DBG: B1 = %6d" % (self._cal_B1)) 94 | print( "DBG: B2 = %6d" % (self._cal_B2)) 95 | print( "DBG: MB = %6d" % (self._cal_MB)) 96 | print( "DBG: MC = %6d" % (self._cal_MC)) 97 | print( "DBG: MD = %6d" % (self._cal_MD)) 98 | 99 | def readRawTemp(self): 100 | "Reads the raw (uncompensated) temperature from the sensor" 101 | self.i2c.write8(self.__BMP085_CONTROL, self.__BMP085_READTEMPCMD) 102 | time.sleep(0.005) # Wait 5ms 103 | raw = self.i2c.readU16(self.__BMP085_TEMPDATA) 104 | if (self.debug): 105 | print( "DBG: Raw Temp: 0x%04X (%d)" % (raw & 0xFFFF, raw)) 106 | return raw 107 | 108 | def readRawPressure(self): 109 | "Reads the raw (uncompensated) pressure level from the sensor" 110 | self.i2c.write8(self.__BMP085_CONTROL, self.__BMP085_READPRESSURECMD + (self.mode << 6)) 111 | if (self.mode == self.__BMP085_ULTRALOWPOWER): 112 | time.sleep(0.005) 113 | elif (self.mode == self.__BMP085_HIGHRES): 114 | time.sleep(0.014) 115 | elif (self.mode == self.__BMP085_ULTRAHIGHRES): 116 | time.sleep(0.026) 117 | else: 118 | time.sleep(0.008) 119 | msb = self.i2c.readU8(self.__BMP085_PRESSUREDATA) 120 | lsb = self.i2c.readU8(self.__BMP085_PRESSUREDATA+1) 121 | xlsb = self.i2c.readU8(self.__BMP085_PRESSUREDATA+2) 122 | raw = ((msb << 16) + (lsb << 8) + xlsb) >> (8 - self.mode) 123 | if (self.debug): 124 | print( "DBG: Raw Pressure: 0x%04X (%d)" % (raw & 0xFFFF, raw)) 125 | return raw 126 | 127 | def readTemperature(self): 128 | "Gets the compensated temperature in degrees celcius" 129 | UT = 0 130 | X1 = 0 131 | X2 = 0 132 | B5 = 0 133 | temp = 0.0 134 | 135 | # Read raw temp before aligning it with the calibration values 136 | UT = self.readRawTemp() 137 | X1 = ((UT - self._cal_AC6) * self._cal_AC5) >> 15 138 | X2 = (self._cal_MC << 11) / (X1 + self._cal_MD) 139 | B5 = X1 + X2 140 | temp = ((B5 + 8) >> 4) / 10.0 141 | if (self.debug): 142 | print("DBG: Calibrated temperature = %f C" % temp) 143 | return temp 144 | 145 | def readPressure(self): 146 | "Gets the compensated pressure in pascal" 147 | UT = 0 148 | UP = 0 149 | B3 = 0 150 | B5 = 0 151 | B6 = 0 152 | X1 = 0 153 | X2 = 0 154 | X3 = 0 155 | p = 0 156 | B4 = 0 157 | B7 = 0 158 | 159 | UT = self.readRawTemp() 160 | UP = self.readRawPressure() 161 | 162 | # You can use the datasheet values to test the conversion results 163 | # dsValues = True 164 | dsValues = False 165 | 166 | if (dsValues): 167 | UT = 27898 168 | UP = 23843 169 | self._cal_AC6 = 23153 170 | self._cal_AC5 = 32757 171 | self._cal_MC = -8711 172 | self._cal_MD = 2868 173 | self._cal_B1 = 6190 174 | self._cal_B2 = 4 175 | self._cal_AC3 = -14383 176 | self._cal_AC2 = -72 177 | self._cal_AC1 = 408 178 | self._cal_AC4 = 32741 179 | self.mode = self.__BMP085_ULTRALOWPOWER 180 | if (self.debug): 181 | self.showCalibrationData() 182 | 183 | # True Temperature Calculations 184 | X1 = ((UT - self._cal_AC6) * self._cal_AC5) >> 15 185 | X2 = (self._cal_MC << 11) // (X1 + self._cal_MD) 186 | B5 = X1 + X2 187 | if (self.debug): 188 | print( "DBG: X1 = %d" % (X1)) 189 | print( "DBG: X2 = %d" % (X2)) 190 | print( "DBG: B5 = %d" % (B5)) 191 | print( "DBG: True Temperature = %.2f C" % (((B5 + 8) >> 4) / 10.0)) 192 | 193 | # Pressure Calculations 194 | B6 = B5 - 4000 195 | X1 = (self._cal_B2 * (B6 * B6) >> 12) >> 11 196 | X2 = (self._cal_AC2 * B6) >> 11 197 | X3 = X1 + X2 198 | B3 = (((self._cal_AC1 * 4 + X3) << self.mode) + 2) // 4 199 | if (self.debug): 200 | print( "DBG: B6 = %d" % (B6)) 201 | print( "DBG: X1 = %d" % (X1)) 202 | print( "DBG: X2 = %d" % (X2)) 203 | print( "DBG: B3 = %d" % (B3)) 204 | 205 | X1 = (self._cal_AC3 * B6) >> 13 206 | X2 = (self._cal_B1 * ((B6 * B6) >> 12)) >> 16 207 | X3 = ((X1 + X2) + 2) >> 2 208 | B4 = (self._cal_AC4 * (X3 + 32768)) >> 15 209 | B7 = (UP - B3) * (50000 >> self.mode) 210 | if (self.debug): 211 | print( "DBG: X1 = %d" % (X1)) 212 | print( "DBG: X2 = %d" % (X2)) 213 | print( "DBG: B4 = %d" % (B4)) 214 | print( "DBG: B7 = %d" % (B7)) 215 | 216 | if (B7 < 0x80000000): 217 | p = (B7 * 2) // B4 218 | else: 219 | p = (B7 / B4) * 2 220 | 221 | X1 = (p >> 8) * (p >> 8) 222 | X1 = (X1 * 3038) >> 16 223 | X2 = (-7375 * p) >> 16 224 | if (self.debug): 225 | print( "DBG: p = %d" % (p)) 226 | print( "DBG: X1 = %d" % (X1)) 227 | print( "DBG: X2 = %d" % (X2)) 228 | 229 | p = p + ((X1 + X2 + 3791) >> 4) 230 | if (self.debug): 231 | print( "DBG: Pressure = %d Pa" % (p)) 232 | 233 | return p 234 | 235 | def readAltitude(self, seaLevelPressure=101325): 236 | "Calculates the altitude in meters" 237 | altitude = 0.0 238 | pressure = float(self.readPressure()) 239 | altitude = 44330.0 * (1.0 - pow(pressure / seaLevelPressure, 0.1903)) 240 | if (self.debug): 241 | print( "DBG: Altitude = %d" % (altitude)) 242 | return altitude 243 | 244 | return 0 245 | 246 | def readMSLPressure(self, altitude): 247 | "Calculates the mean sea level pressure" 248 | pressure = float(self.readPressure()) 249 | T0 = float(altitude) / 44330 250 | T1 = math.pow(1 - T0, 5.255) 251 | mslpressure = pressure / T1 252 | return mslpressure 253 | 254 | if __name__=="__main__": 255 | bmp = BMP085() 256 | print( str(bmp.readTemperature()) + " C") 257 | print( str(bmp.readPressure()) + " Pa") 258 | -------------------------------------------------------------------------------- /create.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE WEATHER_MEASUREMENT( 2 | ID BIGINT NOT NULL AUTO_INCREMENT, 3 | REMOTE_ID BIGINT, 4 | AMBIENT_TEMPERATURE DECIMAL(6,2) NOT NULL, 5 | GROUND_TEMPERATURE DECIMAL(6,2) NOT NULL, 6 | AIR_QUALITY DECIMAL(6,2) NOT NULL, 7 | AIR_PRESSURE DECIMAL(6,2) NOT NULL, 8 | HUMIDITY DECIMAL(6,2) NOT NULL, 9 | WIND_DIRECTION DECIMAL(6,2) NULL, 10 | WIND_SPEED DECIMAL(6,2) NOT NULL, 11 | WIND_GUST_SPEED DECIMAL(6,2) NOT NULL, 12 | RAINFALL DECIMAL (6,2) NOT NULL, 13 | CREATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | PRIMARY KEY ( ID ) 15 | ); 16 | -------------------------------------------------------------------------------- /credentials.mysql: -------------------------------------------------------------------------------- 1 | { 2 | "HOST": "localhost", 3 | "USERNAME": "pi", 4 | "PASSWORD": "password", 5 | "DATABASE": "weather" 6 | } 7 | -------------------------------------------------------------------------------- /credentials.oracle.template: -------------------------------------------------------------------------------- 1 | { 2 | "WEATHER_STN_NAME": "name", 3 | "WEATHER_STN_PASS": "key" 4 | } 5 | -------------------------------------------------------------------------------- /crontab.save: -------------------------------------------------------------------------------- 1 | # Edit this file to introduce tasks to be run by cron. 2 | # 3 | # Each task to run has to be defined through a single line 4 | # indicating with different fields when the task will be run 5 | # and what command to run for the task 6 | # 7 | # To define the time you can provide concrete values for 8 | # minute (m), hour (h), day of month (dom), month (mon), 9 | # and day of week (dow) or use '*' in these fields (for 'any').# 10 | # Notice that tasks will be started based on the cron's system 11 | # daemon's notion of time and timezones. 12 | # 13 | # Output of the crontab jobs (including errors) is sent through 14 | # email to the user the crontab file belongs to (unless redirected). 15 | # 16 | # For example, you can run a backup of all your user accounts 17 | # at 5 a.m every week with: 18 | # 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/ 19 | # 20 | # For more information see the manual pages of crontab(5) and cron(8) 21 | # 22 | # m h dom mon dow command 23 | */5 * * * * sudo ~/weather-station/log_all_sensors.py 24 | 0 */2 * * * ~/weather-station/upload_to_oracle.py 25 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import MySQLdb, datetime, http.client, json, os 3 | import io 4 | import gzip 5 | 6 | 7 | def gunzip_bytes(bytes_obj): 8 | in_ = io.BytesIO() 9 | in_.write(bytes_obj) 10 | in_.seek(0) 11 | with gzip.GzipFile(fileobj=in_, mode='rb') as fo: 12 | gunzipped_bytes_obj = fo.read() 13 | 14 | return gunzipped_bytes_obj.decode() 15 | 16 | class mysql_database: 17 | def __init__(self): 18 | credentials_file = os.path.join(os.path.dirname(__file__), "credentials.mysql") 19 | f = open(credentials_file, "r") 20 | credentials = json.load(f) 21 | f.close() 22 | for key, value in credentials.items(): #remove whitespace 23 | credentials[key] = value.strip() 24 | 25 | self.connection = MySQLdb.connect(user=credentials["USERNAME"], password=credentials["PASSWORD"], database=credentials["DATABASE"]) 26 | self.cursor = self.connection.cursor() 27 | 28 | def execute(self, query, params = []): 29 | try: 30 | self.cursor.execute(query, params) 31 | self.connection.commit() 32 | except: 33 | self.connection.rollback() 34 | raise 35 | 36 | def query(self, query): 37 | cursor = self.connection.cursor(MySQLdb.cursors.DictCursor) 38 | cursor.execute(query) 39 | return cursor.fetchall() 40 | 41 | def __del__(self): 42 | self.connection.close() 43 | 44 | class oracle_apex_database: 45 | def __init__(self, path, host = "apex.oracle.com"): 46 | self.host = host 47 | self.path = path 48 | #self.conn = httplib.HTTPSConnection(self.host) 49 | self.conn = http.client.HTTPSConnection(self.host) 50 | self.credentials = None 51 | credentials_file = os.path.join(os.path.dirname(__file__), "credentials.oracle") 52 | 53 | if os.path.isfile(credentials_file): 54 | f = open(credentials_file, "r") 55 | self.credentials = json.load(f) 56 | f.close() 57 | for key, value in self.credentials.items(): #remove whitespace 58 | self.credentials[key] = value.strip() 59 | else: 60 | print("Credentials file not found") 61 | 62 | self.default_data = { "Content-type": "text/plain", "Accept": "text/plain" } 63 | 64 | def upload(self, id, ambient_temperature, ground_temperature, air_quality, air_pressure, humidity, wind_direction, wind_speed, wind_gust_speed, rainfall, created): 65 | #keys must follow the names expected by the Orcale Apex REST service 66 | oracle_data = { 67 | "LOCAL_ID": str(id), 68 | "AMB_TEMP": str(ambient_temperature), 69 | "GND_TEMP": str(ground_temperature), 70 | "AIR_QUALITY": str(air_quality), 71 | "AIR_PRESSURE": str(air_pressure), 72 | "HUMIDITY": str(humidity), 73 | "WIND_DIRECTION": str(wind_direction), 74 | "WIND_SPEED": str(wind_speed), 75 | "WIND_GUST_SPEED": str(wind_gust_speed), 76 | "RAINFALL": str(rainfall), 77 | "READING_TIMESTAMP": str(created) } 78 | 79 | for key in oracle_data.keys(): 80 | if oracle_data[key] == str(None): 81 | del oracle_data[key] 82 | 83 | return self.https_post(oracle_data) 84 | 85 | def https_post(self, data, attempts = 3): 86 | attempt = 0 87 | headers = self.default_data.copy() 88 | headers.update(self.credentials) 89 | headers.update(data) 90 | 91 | #headers = dict(self.default_data.items() + self.credentials.items() + data.items()) 92 | success = False 93 | response_data = None 94 | 95 | while not success and attempt < attempts: 96 | try: 97 | self.conn.request("POST", self.path, None, headers) 98 | response = self.conn.getresponse() 99 | response_data = response.read() 100 | print("Response status: %s, Response reason: %s, Response data: %s" % (response.status, response.reason, response_data)) 101 | success = response.status == 200 or response.status == 201 102 | except Exception as e: 103 | print("Unexpected error", e) 104 | finally: 105 | attempt += 1 106 | 107 | return response_data if success else None 108 | 109 | def __del__(self): 110 | self.conn.close() 111 | 112 | class weather_database: 113 | def __init__(self): 114 | self.db = mysql_database() 115 | self.insert_template = "INSERT INTO WEATHER_MEASUREMENT (AMBIENT_TEMPERATURE, GROUND_TEMPERATURE, AIR_QUALITY, AIR_PRESSURE, HUMIDITY, WIND_DIRECTION, WIND_SPEED, WIND_GUST_SPEED, RAINFALL, CREATED) VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s);" 116 | self.update_template = "UPDATE WEATHER_MEASUREMENT SET REMOTE_ID=%s WHERE ID=%s;" 117 | self.upload_select_template = "SELECT * FROM WEATHER_MEASUREMENT WHERE REMOTE_ID IS NULL;" 118 | 119 | def is_number(self, s): 120 | try: 121 | float(s) 122 | return True 123 | except ValueError: 124 | return False 125 | 126 | def is_none(self, val): 127 | return val if val != None else "NULL" 128 | 129 | def insert(self, ambient_temperature, ground_temperature, air_quality, air_pressure, humidity, wind_direction, wind_speed, wind_gust_speed, rainfall, created = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")): 130 | params = ( ambient_temperature, 131 | ground_temperature, 132 | air_quality, 133 | air_pressure, 134 | humidity, 135 | wind_direction, 136 | wind_speed, 137 | wind_gust_speed, 138 | rainfall, 139 | created ) 140 | print(self.insert_template % params) 141 | self.db.execute(self.insert_template, params) 142 | 143 | def upload(self): 144 | results = self.db.query(self.upload_select_template) 145 | 146 | rows_count = len(results) 147 | if rows_count > 0: 148 | print("%d rows to send..." % rows_count) 149 | odb = oracle_apex_database(path = "/pls/apex/raspberrypi/weatherstation/submitmeasurement") 150 | 151 | if odb.credentials == None: 152 | return #cannot upload 153 | 154 | for row in results: 155 | response_data = odb.upload( 156 | row["ID"], 157 | row["AMBIENT_TEMPERATURE"], 158 | row["GROUND_TEMPERATURE"], 159 | row["AIR_QUALITY"], 160 | row["AIR_PRESSURE"], 161 | row["HUMIDITY"], 162 | row["WIND_DIRECTION"], 163 | row["WIND_SPEED"], 164 | row["WIND_GUST_SPEED"], 165 | row["RAINFALL"], 166 | row["CREATED"].strftime("%Y-%m-%dT%H:%M:%S")) 167 | 168 | if response_data != None and response_data != "-1": 169 | json_dict = json.loads(gunzip_bytes(response_data)) # 2019 post-apex upgrade change 170 | #json_dict = json.loads(response_data.decode()) # Python3 change 171 | oracle_id = json_dict["ORCL_RECORD_ID"] 172 | if self.is_number(oracle_id): 173 | local_id = str(row["ID"]) 174 | self.db.execute(self.update_template, (oracle_id, local_id)) 175 | print("ID: %s updated with REMOTE_ID = %s" % (local_id, oracle_id)) 176 | else: 177 | print("Bad response from Oracle") 178 | else: 179 | print("Nothing to upload") 180 | -------------------------------------------------------------------------------- /ds18b20_therm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os, glob, time 3 | 4 | # add the lines below to /etc/modules (reboot to take effect) 5 | # w1-gpio 6 | # w1-therm 7 | 8 | class DS18B20(object): 9 | def __init__(self): 10 | self.device_file = glob.glob("/sys/bus/w1/devices/28*")[0] + "/w1_slave" 11 | 12 | def read_temp_raw(self): 13 | f = open(self.device_file, "r") 14 | lines = f.readlines() 15 | f.close() 16 | return lines 17 | 18 | def crc_check(self, lines): 19 | return lines[0].strip()[-3:] == "YES" 20 | 21 | def read_temp(self): 22 | temp_c = -255 23 | attempts = 0 24 | 25 | lines = self.read_temp_raw() 26 | success = self.crc_check(lines) 27 | 28 | while not success and attempts < 3: 29 | time.sleep(.2) 30 | lines = self.read_temp_raw() 31 | success = self.crc_check(lines) 32 | attempts += 1 33 | 34 | if success: 35 | temp_line = lines[1] 36 | equal_pos = temp_line.find("t=") 37 | if equal_pos != -1: 38 | temp_string = temp_line[equal_pos+2:] 39 | temp_c = float(temp_string)/1000.0 40 | 41 | return temp_c 42 | 43 | if __name__ == "__main__": 44 | obj = DS18B20() 45 | print("Temp: %s C" % obj.read_temp()) 46 | -------------------------------------------------------------------------------- /i2c_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import io, fcntl 3 | I2C_SLAVE=0x0703 4 | 5 | class i2c(object): 6 | def __init__(self, device, bus): 7 | self.fr = io.open("/dev/i2c-"+str(bus), "rb", buffering=0) 8 | self.fw = io.open("/dev/i2c-"+str(bus), "wb", buffering=0) 9 | # set device address 10 | fcntl.ioctl(self.fr, I2C_SLAVE, device) 11 | fcntl.ioctl(self.fw, I2C_SLAVE, device) 12 | def write(self, bytes): 13 | self.fw.write(bytes) 14 | def read(self, bytes): 15 | return self.fr.read(bytes) 16 | def close(self): 17 | self.fw.close() 18 | self.fr.close() 19 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 'Please ensure your Weather Station HAT is connected to you Raspberry Pi, with the battery installed.' 4 | echo 'Please ensure your Raspberry Pi is connected to the Internet' 5 | echo 'Press any key to continue' 6 | 7 | read -n 1 -s 8 | 9 | ##Update and upgrade - especially important for old NOOBS installs and I2C integration 10 | sudo apt-get update && sudo apt-get upgrade -y 11 | 12 | ##Update config files. 13 | echo "dtoverlay=w1-gpio" | sudo tee -a /boot/config.txt 14 | echo "dtoverlay=pcf8523-rtc" | sudo tee -a /boot/config.txt 15 | 16 | #sudo tee /boot/config.txt < /dev/null 2>&1; then 30 | echo "RTC found" 31 | else 32 | echo "No RTC found - please follow manual setup to Troubleshoot." 33 | exit 1 34 | fi 35 | 36 | #Initialise RTC with correct time 37 | echo "The current date set is:" 38 | date 39 | read -r -p "Is this correct [y/N] " response 40 | response=${response,,} # tolower 41 | if [[ $response =~ ^(yes|y)$ ]]; then 42 | sudo hwclock -w 43 | else 44 | read -p "Enter todays date and time (yyyy-mm-dd hh:mm:ss): " user_date 45 | sudo hwclock --set --date="$user_date" --utc #set hardware clock 46 | fi 47 | #update system clock 48 | sudo hwclock -s 49 | 50 | #Update hwclock config 51 | sudo perl -pi -e 's/systz/hctosys/g' /lib/udev/hwclock-set 52 | 53 | #Remove hwc package 54 | sudo update-rc.d fake-hwclock remove 55 | sudo apt-get remove fake-hwclock -y 56 | 57 | sudo apt-get install i2c-tools python-smbus telnet -y 58 | 59 | echo "Install Complete" 60 | echo "Please run upon restart to test the sensors" 61 | echo "System will now reboot" 62 | sudo reboot 63 | -------------------------------------------------------------------------------- /interrupt_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import time, os, sys, socket 3 | 4 | class interrupt_client(object): 5 | def __init__(self, port): 6 | self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 7 | self.client.connect(("localhost", port)) 8 | assert(self.get_data() == "OK") 9 | print("Connected to interrupt daemon") 10 | 11 | def get_data(self): 12 | buf = self.client.recv(128) 13 | return buf.decode('utf-8').strip() 14 | 15 | def send_command(self, command): 16 | self.client.sendall(command.encode('utf-8')) 17 | data = self.get_data() 18 | try: 19 | return float(data) 20 | except ValueError: 21 | return None 22 | 23 | def get_rain(self): 24 | return self.send_command("RAIN") 25 | 26 | def get_wind(self): 27 | return self.send_command("WIND") 28 | 29 | def get_wind_gust(self): 30 | return self.send_command("GUST") 31 | 32 | def reset(self): 33 | self.client.sendall(b"RESET") 34 | assert(self.get_data() == "OK") 35 | print("Counts reset") 36 | 37 | def __del__(self): 38 | self.client.sendall("BYE".encode('utf-8')) 39 | self.client.close() 40 | print("Connection closed") 41 | 42 | if __name__ == "__main__": 43 | obj = interrupt_client(49501) 44 | print("RAIN: %s" % obj.get_rain()) 45 | print("WIND: %s" % obj.get_wind()) 46 | print("GUST: %s" % obj.get_wind_gust()) 47 | -------------------------------------------------------------------------------- /interrupt_daemon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import time, os, sys, socket, math, atexit 3 | import RPi.GPIO as GPIO 4 | try: 5 | import thread 6 | except ImportError: 7 | import _thread as thread 8 | 9 | class interrupt_watcher(object): 10 | def __init__(self, sensorPin, bounceTime, peak_sample = 5, peak_monitor = False): 11 | self.interrupt_count = 0 12 | self.running = True 13 | self.interrupt_peak_count = 0 14 | self.interrupt_peak_max = 0 15 | 16 | GPIO.setmode(GPIO.BCM) 17 | GPIO.setup(sensorPin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 18 | GPIO.add_event_detect(sensorPin, GPIO.FALLING, callback=self.interrupt_call_back, bouncetime=bounceTime) 19 | 20 | if peak_monitor: 21 | thread.start_new_thread(self.peak_monitor, (peak_sample,)) 22 | 23 | def interrupt_call_back(self, channel): 24 | self.interrupt_count += 1 25 | self.interrupt_peak_count += 1 26 | 27 | def get_value(self): 28 | return self.interrupt_count 29 | 30 | def get_peak(self): 31 | return self.interrupt_peak_max 32 | 33 | def reset_count(self): 34 | self.interrupt_count = 0 35 | self.interrupt_peak_count = 0 36 | self.interrupt_peak_max = 0 37 | 38 | def peak_monitor(self, sample_period): 39 | while self.running: 40 | time.sleep(sample_period) 41 | if self.interrupt_peak_count > self.interrupt_peak_max: 42 | self.interrupt_peak_max = self.interrupt_peak_count 43 | self.interrupt_peak_count = 0 44 | 45 | def __del__(self): 46 | self.running = False 47 | 48 | class wind_speed_interrupt_watcher(interrupt_watcher): 49 | def __init__(self, radius_cm, sensorPin, bounceTime, calibration = 2.36): 50 | super(wind_speed_interrupt_watcher, self).__init__(sensorPin, bounceTime, peak_sample = 5, peak_monitor = True) 51 | 52 | circumference_cm = (2 * math.pi) * radius_cm 53 | self.circumference = circumference_cm / 100000.0 #circumference in km 54 | self.calibration = calibration 55 | self.last_time = time.time() 56 | 57 | def calculate_speed(self, interrupt_count, interval_seconds): 58 | rotations = interrupt_count / 2.0 59 | distance_per_second = (self.circumference * rotations) / interval_seconds 60 | speed_per_hour = distance_per_second * 3600 61 | return speed_per_hour * self.calibration 62 | 63 | def get_wind_speed(self): 64 | return self.calculate_speed(self.get_value(), time.time() - self.last_time) 65 | 66 | def get_wind_gust_speed(self): 67 | return self.calculate_speed(self.get_peak(), 5) #5 seconds 68 | 69 | def reset_timer(self): 70 | self.last_time = time.time() 71 | 72 | class rainfall_interrupt_watcher(interrupt_watcher): 73 | def __init__(self, tip_volume, sensorPin, bounceTime): 74 | super(rainfall_interrupt_watcher, self).__init__(sensorPin, bounceTime) 75 | self.tip_volume = tip_volume 76 | 77 | def get_rainfall(self): 78 | return self.tip_volume * self.get_value() 79 | 80 | class interrupt_daemon(object): 81 | def __init__(self, port): 82 | self.running = False 83 | self.port = port 84 | self.socket_data = "{0}\n" 85 | 86 | def setup(self): 87 | self.rain = rainfall_interrupt_watcher(0.2794, 6, 300) #Maplin rain gauge = 0.2794 ml per bucket tip, was 27 on prototype 88 | self.wind = wind_speed_interrupt_watcher(9.0, 5, 1) #Maplin anemometer = radius of 9 cm, was 17 on prototype 89 | 90 | try: 91 | self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 92 | self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 93 | self.skt.bind(("127.0.0.1", self.port)) 94 | self.running = True 95 | except socket.error as e: 96 | print(e) 97 | raise 98 | 99 | self.skt.listen(10) 100 | 101 | def send(self, conn, s): 102 | conn.sendall(self.socket_data.format(s).encode('utf-8')) 103 | 104 | def receive(self, conn, length): 105 | data = conn.recv(length) 106 | return data.decode('utf-8') 107 | 108 | def handle_connection(self, conn): 109 | connected = True 110 | self.send(conn, "OK") 111 | 112 | while connected and self.running: 113 | data = self.receive(conn, 128) 114 | if len(data) > 0: 115 | data = data.strip() 116 | if data == "RAIN": 117 | self.send(conn, self.rain.get_rainfall()) 118 | elif data == "WIND": 119 | self.send(conn, self.wind.get_wind_speed()) 120 | elif data == "GUST": 121 | self.send(conn, self.wind.get_wind_gust_speed()) 122 | elif data == "RESET": 123 | self.reset_counts() 124 | self.send(conn, "OK") 125 | elif data == "BYE": 126 | connected = False 127 | elif data == "STOP": 128 | connected = False 129 | self.stop() 130 | 131 | conn.close() 132 | 133 | def reset_counts(self): 134 | self.rain.reset_count() 135 | self.wind.reset_count() 136 | self.wind.reset_timer() 137 | 138 | def daemonize(self): 139 | # do the UNIX double-fork magic, see Stevens' "Advanced Programming in the UNIX Environment" for details (ISBN 0201563177) 140 | # first fork 141 | try: 142 | self.pid = os.fork() 143 | if self.pid > 0: 144 | sys.exit(0) 145 | except OSError as e: 146 | print(e) 147 | raise 148 | 149 | # decouple from parent environment 150 | os.chdir("/") 151 | os.setsid() 152 | os.umask(0) 153 | 154 | # second fork 155 | try: 156 | self.pid = os.fork() 157 | if self.pid > 0: 158 | sys.exit(0) 159 | except OSError as e: 160 | print(e) 161 | raise 162 | 163 | # close file descriptors 164 | sys.stdout.flush() 165 | sys.stderr.flush() 166 | 167 | def start(self): 168 | try: 169 | self.daemon_pid = None 170 | self.daemonize() 171 | self.daemon_pid = os.getpid() 172 | print("PID: %d" % self.daemon_pid) 173 | self.setup() 174 | while self.running: 175 | conn, addr = self.skt.accept() #blocking call 176 | if self.running: 177 | thread.start_new_thread(self.handle_connection, (conn,)) 178 | except Exception: 179 | if self.running: 180 | self.stop() 181 | finally: 182 | if self.daemon_pid == os.getpid(): 183 | self.skt.shutdown(socket.SHUT_RDWR) 184 | self.skt.close() 185 | GPIO.cleanup() 186 | print("Stopped") 187 | 188 | def stop(self): 189 | self.running = False 190 | socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect(("localhost", self.port)) #release blocking call 191 | 192 | def send_stop_signal(port): 193 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 194 | client.connect(("localhost", port)) 195 | client.sendall("STOP".encode('utf-8')) 196 | client.close() 197 | 198 | if __name__ == "__main__": 199 | server_port = 49501 200 | if len(sys.argv) >= 2: 201 | arg = sys.argv[1].upper() 202 | if arg == "START": 203 | interrupt_daemon(server_port).start() 204 | elif arg == "STOP": 205 | send_stop_signal(server_port) 206 | elif arg == "RESTART": 207 | send_stop_signal(server_port) 208 | time.sleep(1) 209 | interrupt_daemon(server_port).start() 210 | else: 211 | print("usage: sudo {0} start|stop|restart".format(sys.argv[0])) 212 | -------------------------------------------------------------------------------- /log_all_sensors-test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import interrupt_client, MCP342X, wind_direction, HTU21D, bmp085, tgs2600, ds18b20_therm 3 | import database # requires MySQLdb python 2 library which is not ported to python 3 yet 4 | 5 | pressure = bmp085.BMP085() 6 | temp_probe = 21 7 | #temp_probe = ds18b20_therm.DS18B20() 8 | air_qual = 0 9 | #air_qual = tgs2600.TGS2600(adc_channel = 0) 10 | humidity = 50 11 | #humidity = HTU21D.HTU21D() 12 | wind_dir = 0 13 | #wind_dir = wind_direction.wind_direction(adc_channel = 0, config_file="wind_direction.json") 14 | #interrupts = interrupt_client.interrupt_client(port = 49501) 15 | wind =0 16 | gust =0 17 | rain=0 18 | temp=20 19 | 20 | db = database.weather_database() #Local MySQL db 21 | 22 | wind_average = 0 23 | #wind_average = wind_dir.get_value(10) #ten seconds 24 | 25 | print("Inserting...") 26 | db.insert(temp, temp_probe, air_qual, pressure.get_pressure(), humidity, wind_average, wind, gust, rain) 27 | print("done") 28 | 29 | #interrupts.reset() 30 | -------------------------------------------------------------------------------- /log_all_sensors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import interrupt_client, MCP342X, wind_direction, HTU21D, bmp085, tgs2600, ds18b20_therm 3 | import database 4 | 5 | pressure = bmp085.BMP085() 6 | temp_probe = ds18b20_therm.DS18B20() 7 | air_qual = tgs2600.TGS2600(adc_channel = 0) 8 | humidity = HTU21D.HTU21D() 9 | wind_dir = wind_direction.wind_direction(adc_channel = 0, config_file="wind_direction.json") 10 | interrupts = interrupt_client.interrupt_client(port = 49501) 11 | 12 | db = database.weather_database() #Local MySQL db 13 | 14 | wind_average = wind_dir.get_value(10) #ten seconds 15 | 16 | print("Inserting...") 17 | db.insert(humidity.read_temperature(), temp_probe.read_temp(), air_qual.get_value(), pressure.get_pressure(), humidity.read_humidity(), wind_average, interrupts.get_wind(), interrupts.get_wind_gust(), interrupts.get_rain()) 18 | print("done") 19 | 20 | interrupts.reset() 21 | -------------------------------------------------------------------------------- /rpi-weather-overlay.dts: -------------------------------------------------------------------------------- 1 | // rpi-sense HAT 2 | /dts-v1/; 3 | /plugin/; 4 | 5 | / { 6 | compatible = "brcm,bcm2708"; 7 | 8 | fragment@0 { 9 | target = <&i2c1>; 10 | __overlay__ { 11 | #address-cells = <1>; 12 | #size-cells = <0>; 13 | status = "okay"; 14 | 15 | pcf8523@68 { 16 | compatible = "nxp,pcf8523"; 17 | reg = <0x68>; 18 | status = "okay"; 19 | }; 20 | }; 21 | }; 22 | 23 | fragment@1 { 24 | target-path = "/"; 25 | __overlay__ { 26 | 27 | w1: onewire@0 { 28 | compatible = "w1-gpio"; 29 | pinctrl-names = "default"; 30 | pinctrl-0 = <&w1_pins>; 31 | gpios = <&gpio 4 0>; 32 | rpi,parasitic-power = <0>; 33 | status = "okay"; 34 | }; 35 | }; 36 | }; 37 | 38 | fragment@2 { 39 | target = <&gpio>; 40 | __overlay__ { 41 | w1_pins: w1_pins { 42 | brcm,pins = <4>; 43 | brcm,function = <0>; // in (initially) 44 | brcm,pull = <0>; // off 45 | }; 46 | }; 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /tgs2600.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import MCP342X 3 | 4 | class TGS2600(object): 5 | def __init__(self, adc_channel = 0): 6 | self.adc_channel = adc_channel 7 | 8 | def get_value(self): 9 | adc = MCP342X.MCP342X(address = 0x6a) 10 | adc_value = adc.read(self.adc_channel) 11 | return (100.0 / adc.max) * (adc.max - adc_value) #as percentage 12 | 13 | if __name__ == "__main__": 14 | obj = TGS2600(0) 15 | print("Air Quality: %s %%" % obj.get_value()) 16 | -------------------------------------------------------------------------------- /upload_to_oracle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os, fcntl 3 | credentials_file = os.path.join(os.path.dirname(__file__), "credentials.oracle") 4 | if os.path.isfile(credentials_file): 5 | lock_file = "/var/lock/oracle.lock" 6 | f = open(lock_file, 'w') 7 | try: 8 | fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB) 9 | print("No other uploads in progress, proceeding...") 10 | import database # requires MySQLdb python 2 library which is not ported to python 3 yet 11 | db = database.weather_database() 12 | db.upload() 13 | except IOError: 14 | print("Another upload is running exiting now") 15 | finally: 16 | f.close() 17 | else: 18 | print("Credentials file not found") 19 | -------------------------------------------------------------------------------- /wind_direction.json: -------------------------------------------------------------------------------- 1 | { 2 | "vin": 3.268, 3 | "vdivider": 75000, 4 | "directions": [ 5 | { "dir": "N", "angle": 0.0, "ohms": 33000 }, 6 | { "dir": "NNE", "angle": 22.5, "ohms": 6570 }, 7 | { "dir": "NE", "angle": 45.0, "ohms": 8200 }, 8 | { "dir": "ENE", "angle": 67.5, "ohms": 891 }, 9 | { "dir": "E", "angle": 90.0, "ohms": 1000 }, 10 | { "dir": "ESE", "angle": 112.5, "ohms": 688 }, 11 | { "dir": "SE", "angle": 135.0, "ohms": 2200 }, 12 | { "dir": "SSE", "angle": 157.5, "ohms": 1410 }, 13 | { "dir": "S", "angle": 180.0, "ohms": 3900 }, 14 | { "dir": "SSW", "angle": 202.5, "ohms": 3140 }, 15 | { "dir": "SW", "angle": 225.0, "ohms": 16000 }, 16 | { "dir": "WSW", "angle": 247.5, "ohms": 14120 }, 17 | { "dir": "W", "angle": 270.0, "ohms": 120000 }, 18 | { "dir": "WNW", "angle": 292.5, "ohms": 42120 }, 19 | { "dir": "NW", "angle": 315.0, "ohms": 64900 }, 20 | { "dir": "NNW", "angle": 337.5, "ohms": 21880 } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /wind_direction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import json 3 | import time 4 | import math 5 | import MCP342X 6 | import os 7 | 8 | class wind_direction(object): 9 | def __init__(self, adc_channel=0, config_file=None): 10 | self.adc_channel = adc_channel 11 | self.adc = MCP342X.MCP342X(address=0x69) 12 | 13 | config_file_path = os.path.join(os.path.dirname(__file__), config_file) 14 | 15 | with open(config_file_path, "r") as f: 16 | self.config = json.load(f) 17 | 18 | vin = self.config["vin"] 19 | vdivider = self.config["vdivider"] 20 | 21 | for dir in self.config["directions"]: 22 | dir["vout"] = self.calculate_vout(vdivider, dir["ohms"], vin) 23 | dir["adc"] = round(self.adc.max * (dir["vout"] / self.adc.vref)) 24 | 25 | sorted_by_adc = sorted(self.config["directions"], key=lambda x: x["adc"]) 26 | 27 | for index, dir in enumerate(sorted_by_adc): 28 | if index > 0: 29 | below = sorted_by_adc[index - 1] 30 | delta = (dir["adc"] - below["adc"]) / 2.0 31 | dir["adcmin"] = dir["adc"] - delta + 1 32 | else: 33 | dir["adcmin"] = 1 34 | 35 | if index < (len(sorted_by_adc) - 1): 36 | above = sorted_by_adc[index + 1] 37 | delta = (above["adc"] - dir["adc"]) / 2.0 38 | dir["adcmax"] = dir["adc"] + delta 39 | else: 40 | dir["adcmax"] = self.adc.max - 1 41 | 42 | def calculate_vout(self, ra, rb, vin): # Ohm's law resistive divider calculation 43 | return (float(rb) / float(ra + rb)) * float(vin) 44 | 45 | def get_dir(self, adc_value): 46 | angle = None 47 | 48 | for dir in self.config["directions"]: 49 | if (adc_value > 0 and 50 | adc_value >= dir["adcmin"] and 51 | adc_value <= dir["adcmax"] and 52 | adc_value < self.adc.max): 53 | angle = dir["angle"] 54 | break 55 | 56 | return angle 57 | 58 | def get_average(self, angles): 59 | """ 60 | Consider the following three angles as an example: 10, 20, and 30 61 | degrees. Intuitively, calculating the mean would involve adding these 62 | three angles together and dividing by 3, in this case indeed resulting 63 | in a correct mean angle of 20 degrees. By rotating this system 64 | anticlockwise through 15 degrees the three angles become 355 degrees, 65 | 5 degrees and 15 degrees. The naive mean is now 125 degrees, which is 66 | the wrong answer, as it should be 5 degrees. 67 | """ 68 | 69 | # http://en.wikipedia.org/wiki/Directional_statistics 70 | 71 | sin_sum = 0.0 72 | cos_sum = 0.0 73 | 74 | for angle in angles: 75 | r = math.radians(angle) 76 | sin_sum += math.sin(r) 77 | cos_sum += math.cos(r) 78 | 79 | flen = float(len(angles)) 80 | s = sin_sum / flen 81 | c = cos_sum / flen 82 | arc = math.degrees(math.atan(s / c)) 83 | average = 0.0 84 | 85 | if s > 0 and c > 0: 86 | average = arc 87 | elif c < 0: 88 | average = arc + 180 89 | elif s < 0 and c > 0: 90 | average = arc + 360 91 | 92 | return 0.0 if average == 360 else average 93 | 94 | def get_value(self, length=5): 95 | data = [] 96 | print("Measuring wind direction for %d seconds..." % length) 97 | start_time = time.time() 98 | 99 | while time.time() - start_time <= length: 100 | adc_value = self.adc.read(self.adc_channel) 101 | direction = self.get_dir(adc_value) 102 | if direction is not None: # keep only good measurements 103 | data.append(direction) 104 | else: 105 | print("Could not determine wind direction for ADC reading: %s" % adc_value) 106 | 107 | return self.get_average(data) 108 | 109 | if __name__ == "__main__": 110 | obj = wind_direction(0, "wind_direction.json") 111 | print(obj.get_value(10)) 112 | -------------------------------------------------------------------------------- /wsinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 'Please ensure your Weather Station HAT is connected to your Raspberry Pi, with the battery installed.' 4 | echo 'Please ensure your Raspberry Pi is connected to the Internet' 5 | 6 | ## Check ready to start 7 | echo "Do you want to install the Weather Station software?" 8 | read -r -p "$1 [y/N] " response < /dev/tty 9 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 10 | echo "Starting" 11 | else 12 | echo "Exiting" 13 | exit 14 | fi 15 | 16 | echo 'Updating Raspbian' 17 | ## Update and upgrade - especially important for old NOOBS installs and I2C integration 18 | sudo apt-get update && sudo apt-get upgrade -y 19 | 20 | # These pacakages needed if using Stretch-lite image 21 | 22 | sudo apt-get install python3-smbus git python3-pip -y 23 | sudo pip3 install RPi.GPIO 24 | ##E nable I2C 25 | echo ' Enabling I2C' 26 | sudo raspi-config nonint do_i2c 0 27 | 28 | ## Update config files. 29 | echo "dtoverlay=w1-gpio" | sudo tee -a /boot/config.txt 30 | echo "dtoverlay=pcf8523-rtc" | sudo tee -a /boot/config.txt 31 | echo "i2c-dev" | sudo tee -a /etc/modules 32 | echo "w1-therm" | sudo tee -a /etc/modules 33 | 34 | echo 'Setting up RTC' 35 | ## Check the RTC exists 36 | if ls /dev/rtc** 1> /dev/null 2>&1; then 37 | echo "RTC found" 38 | else 39 | echo "No RTC found - please follow manual setup to Troubleshoot." 40 | exit 1 41 | fi 42 | 43 | ## Initialise RTC with correct time 44 | echo "The current date and time set is:" 45 | date 46 | read -r -p "Is this correct [y/N] " response2 < /dev/tty 47 | response2=${response2,,} # tolower 48 | if [[ $response2 =~ ^(yes|y)$ ]]; then 49 | sudo hwclock -w 50 | else 51 | read -p "Enter todays date and time (yyyy-mm-dd hh:mm:ss): " user_date < /dev/tty 52 | sudo hwclock --set --date="$user_date" --utc #set hardware clock 53 | fi 54 | 55 | #update system clock 56 | sudo hwclock -s 57 | 58 | #Update hwclock config 59 | sudo perl -pi -e 's/systz/hctosys/g' /lib/udev/hwclock-set 60 | 61 | #Remove hwc package 62 | sudo update-rc.d fake-hwclock remove 63 | sudo apt-get remove fake-hwclock -y 64 | 65 | echo 'Installing required packages' 66 | ## Install com tools 67 | sudo apt-get install i2c-tools python-smbus telnet -y 68 | 69 | ## Set password for mysql-server 70 | echo 'Please choose a password for your database' 71 | read -s -p "Password: " PASS1 < /dev/tty 72 | echo 73 | read -s -p "Password (again): " PASS2 < /dev/tty 74 | 75 | # check if passwords match and if not ask again 76 | while [ "$PASS1" != "$PASS2" ]; 77 | do 78 | echo 79 | echo "Please try again" 80 | read -s -p "Password: " PASS1 < /dev/tty 81 | echo 82 | read -s -p "Password (again): " PASS2 < /dev/tty 83 | done 84 | 85 | echo 'Installing local database' 86 | sudo apt-get install -y mariadb-server mariadb-client libmariadbclient-dev 87 | # sudo apt-get install -y apache2 php5 libapache2-mod-php5 php-mysql 88 | sudo pip3 install mysqlclient 89 | 90 | 91 | ## Create a database and weather table 92 | echo 'Creating Database' 93 | sudo mysql < credentials.mysql 129 | { 130 | "HOST": "localhost", 131 | "USERNAME": "pi", 132 | "PASSWORD": "$PASS1", 133 | "DATABASE": "weather" 134 | } 135 | EOT 136 | 137 | ## Alter crontab for periodic uploads 138 | crontab < crontab.save 139 | 140 | ## Add credentials for weather station 141 | echo 'You should have registered you weather station at' 142 | echo 'https://apex.oracle.com/pls/apex/f?p=81290:LOGIN_DESKTOP:0:::::&tz=1:00' 143 | echo 'You should have a Weather Station Name' 144 | echo 'You should have a Weather Station Key' 145 | read -p "Please type in your Weather Station Name: " name < /dev/tty 146 | read -p "Please type in your Weather Station Key: " key < /dev/tty 147 | 148 | cat < credentials.oracle 149 | { 150 | "WEATHER_STN_NAME": "$name", 151 | "WEATHER_STN_PASS": "$key" 152 | } 153 | EOT 154 | echo "All done - rebooting" 155 | sudo reboot 156 | --------------------------------------------------------------------------------