├── .gitignore ├── README.md ├── airpi.py ├── outputs.cfg ├── outputs ├── __init__.py ├── output.py ├── print.py └── xively.py ├── sensors.cfg ├── sensors ├── Adafruit_I2C.py ├── __init__.py ├── analogue.py ├── bmp085.py ├── bmpBackend.py ├── dht22.py ├── dhtreader.so ├── mcp3008.py └── sensor.py └── settings.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AirPi 2 | ======== 3 | 4 | A Raspberry Pi weather station and air quality monitor. 5 | 6 | This is the code for the project located at http://airpi.es 7 | 8 | Currently it is split into airpi.py, as well as multiple input and multiple output plugins. airpi.py collects data from each of the input plugins specified in sensors.cfg, and then passes the data provided by them to each output defined in outputs.cfg. The code for each sensor plugin is contained in the 'sensors' folder and the code for each output plugin in the 'outputs' folder. 9 | 10 | Some of the files are based off code for the Raspberry Pi written by Adafruit: https://github.com/adafruit/Adafruit-Raspberry-Pi-Python-Code 11 | 12 | For installation instructions, see http://airpi.es/kit.php 13 | 14 | ## Installation 15 | 16 | ### Prerequisites 17 | 18 | You will need to install the following dependencies: 19 | 20 | `sudo apt-get install git-core python-dev python-pip python-smbus libxml2-dev libxslt1-dev python-lxml i2c-tools` 21 | 22 | and 23 | 24 | `sudo pip install rpi.gpio requests` 25 | 26 | AirPi requires python-eeml. To install: 27 | 28 | ``` 29 | cd ~/git 30 | git clone https://github.com/petervizi/python-eeml.git 31 | cd python-eeml 32 | sudo python setup.py install 33 | ``` 34 | 35 | ### i2c 36 | 37 | To set up i2c, first add your user to the i2c group. For example, if your username is "pi": 38 | 39 | `sudo adduser pi i2c` 40 | 41 | Now, add the modules needed. 42 | 43 | `sudo nano /etc/modules` 44 | 45 | Add the following two lines to the end of the file: 46 | 47 | ``` 48 | i2c-bcm2708 49 | i2c-dev 50 | ``` 51 | 52 | Exit by pressing CTRL+X, followed by y to confirm you want to save, and ⏎ (enter) to confirm the filename. 53 | 54 | Finally, unblacklist i2c by running the following command: 55 | 56 | `sudo nano /etc/modprobe.d/raspi-blacklist.conf` 57 | 58 | Add a `#` character at the beginning of the line `blacklist i2c-bcm2708`. Then exit in the same way as last time. 59 | 60 | Now, reboot your Raspberry Pi: 61 | 62 | `sudo reboot` 63 | 64 | ### Board Version 65 | 66 | Let's check to see which board version you have. Run: 67 | 68 | `sudo i2cdetect -y 0` 69 | 70 | You should see this as the output: 71 | 72 | ``` 73 | 0 1 2 3 4 5 6 7 8 9 a b c d e f 74 | 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 75 | 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 76 | 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 77 | 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 78 | 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 79 | 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 80 | 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 81 | 70: -- -- -- -- -- -- -- 77 82 | ``` 83 | 84 | If not, run: 85 | 86 | `sudo i2cdetect -y 1` 87 | 88 | and you should see the above. This tells you if your board is version 0 or 1. This is important for the next step. 89 | 90 | ### Get The AirPi Code 91 | 92 | Clone this repo into your git directory (or wherever you want): 93 | 94 | ``` 95 | cd ~/git 96 | git clone https://github.com/tomhartley/AirPi.git 97 | cd AirPi 98 | ``` 99 | 100 | ### Configuring 101 | 102 | Edit the settings file by running: 103 | 104 | `nano sensors.cfg` 105 | 106 | The start of the file should look like this: 107 | 108 | ``` 109 | [BMP085-temp] 110 | filename=bmp085 111 | enabled=on 112 | measurement=temp 113 | i2cbus = 1 114 | 115 | [BMP085-pres] 116 | filename=bmp085 117 | enabled=on 118 | measurement=pres 119 | mslp=on 120 | i2cbus = 1 121 | altitude=40 122 | ``` 123 | 124 | If your board version is "0" change both instances of `i2cbus = 1` to `i2cbus = 1` 125 | 126 | Press CTRL+X to exit the file, when prompted, press "y" to save the file. 127 | 128 | If you want to push the data to Xively, edit the `outputs.cfg` file: 129 | 130 | `nano outputs.cfg` 131 | 132 | The start of the file should look like this: 133 | 134 | ``` 135 | [Print] 136 | filename=print 137 | enabled=on 138 | 139 | [Xively] 140 | filename=xively 141 | enabled=on 142 | APIKey=xxxxxxxxxx 143 | FeedID=xxxxxxxxxx 144 | ``` 145 | 146 | If you have registered with https://xively.com - you can add your API Key and Feed ID here. 147 | 148 | ## Running 149 | 150 | AirPi **must** be run as root. 151 | 152 | `sudo python airpi.py` 153 | 154 | If everything is working, you should see output similar to this: 155 | 156 | ``` 157 | Success: Loaded sensor plugin BMP085-temp 158 | Success: Loaded sensor plugin BMP085-pres 159 | Success: Loaded sensor plugin MCP3008 160 | Success: Loaded sensor plugin DHT22 161 | Success: Loaded sensor plugin LDR 162 | Success: Loaded sensor plugin MiCS-2710 163 | Success: Loaded sensor plugin MiCS-5525 164 | Success: Loaded sensor plugin Mic 165 | Success: Loaded output plugin Print 166 | Success: Loaded output plugin Xively 167 | 168 | Time: 2014-06-04 09:10:18.942625 169 | Temperature: 30.2 C 170 | Pressure: 992.55 hPa 171 | Relative_Humidity: 35.9000015259 % 172 | Light_Level: 3149.10025707 Ohms 173 | Nitrogen_Dioxide: 9085.82089552 Ohms 174 | Carbon_Monoxide: 389473.684211 Ohms 175 | Volume: 338.709677419 mV 176 | Uploaded successfully 177 | 178 | ``` 179 | -------------------------------------------------------------------------------- /airpi.py: -------------------------------------------------------------------------------- 1 | #This file takes in inputs from a variety of sensor files, and outputs information to a variety of services 2 | import sys 3 | sys.dont_write_bytecode = True 4 | 5 | import RPi.GPIO as GPIO 6 | import ConfigParser 7 | import time 8 | import inspect 9 | import os 10 | from sys import exit 11 | from sensors import sensor 12 | from outputs import output 13 | 14 | def get_subclasses(mod,cls): 15 | for name, obj in inspect.getmembers(mod): 16 | if hasattr(obj, "__bases__") and cls in obj.__bases__: 17 | return obj 18 | 19 | 20 | if not os.path.isfile('sensors.cfg'): 21 | print "Unable to access config file: sensors.cfg" 22 | exit(1) 23 | 24 | sensorConfig = ConfigParser.SafeConfigParser() 25 | sensorConfig.read('sensors.cfg') 26 | 27 | sensorNames = sensorConfig.sections() 28 | 29 | GPIO.setwarnings(False) 30 | GPIO.setmode(GPIO.BCM) #Use BCM GPIO numbers. 31 | 32 | sensorPlugins = [] 33 | for i in sensorNames: 34 | try: 35 | try: 36 | filename = sensorConfig.get(i,"filename") 37 | except Exception: 38 | print("Error: no filename config option found for sensor plugin " + i) 39 | raise 40 | try: 41 | enabled = sensorConfig.getboolean(i,"enabled") 42 | except Exception: 43 | enabled = True 44 | 45 | #if enabled, load the plugin 46 | if enabled: 47 | try: 48 | mod = __import__('sensors.'+filename,fromlist=['a']) #Why does this work? 49 | except Exception: 50 | print("Error: could not import sensor module " + filename) 51 | raise 52 | 53 | try: 54 | sensorClass = get_subclasses(mod,sensor.Sensor) 55 | if sensorClass == None: 56 | raise AttributeError 57 | except Exception: 58 | print("Error: could not find a subclass of sensor.Sensor in module " + filename) 59 | raise 60 | 61 | try: 62 | reqd = sensorClass.requiredData 63 | except Exception: 64 | reqd = [] 65 | try: 66 | opt = sensorClass.optionalData 67 | except Exception: 68 | opt = [] 69 | 70 | pluginData = {} 71 | 72 | class MissingField(Exception): pass 73 | 74 | for requiredField in reqd: 75 | if sensorConfig.has_option(i,requiredField): 76 | pluginData[requiredField]=sensorConfig.get(i,requiredField) 77 | else: 78 | print "Error: Missing required field '" + requiredField + "' for sensor plugin " + i 79 | raise MissingField 80 | for optionalField in opt: 81 | if sensorConfig.has_option(i,optionalField): 82 | pluginData[optionalField]=sensorConfig.get(i,optionalField) 83 | instClass = sensorClass(pluginData) 84 | sensorPlugins.append(instClass) 85 | print ("Success: Loaded sensor plugin " + i) 86 | except Exception as e: #add specific exception for missing module 87 | print("Error: Did not import sensor plugin " + i ) 88 | raise e 89 | 90 | 91 | if not os.path.isfile("outputs.cfg"): 92 | print "Unable to access config file: outputs.cfg" 93 | 94 | outputConfig = ConfigParser.SafeConfigParser() 95 | outputConfig.read("outputs.cfg") 96 | 97 | outputNames = outputConfig.sections() 98 | 99 | outputPlugins = [] 100 | 101 | for i in outputNames: 102 | try: 103 | try: 104 | filename = outputConfig.get(i,"filename") 105 | except Exception: 106 | print("Error: no filename config option found for output plugin " + i) 107 | raise 108 | try: 109 | enabled = outputConfig.getboolean(i,"enabled") 110 | except Exception: 111 | enabled = True 112 | 113 | #if enabled, load the plugin 114 | if enabled: 115 | try: 116 | mod = __import__('outputs.'+filename,fromlist=['a']) #Why does this work? 117 | except Exception: 118 | print("Error: could not import output module " + filename) 119 | raise 120 | 121 | try: 122 | outputClass = get_subclasses(mod,output.Output) 123 | if outputClass == None: 124 | raise AttributeError 125 | except Exception: 126 | print("Error: could not find a subclass of output.Output in module " + filename) 127 | raise 128 | try: 129 | reqd = outputClass.requiredData 130 | except Exception: 131 | reqd = [] 132 | try: 133 | opt = outputClass.optionalData 134 | except Exception: 135 | opt = [] 136 | 137 | if outputConfig.has_option(i,"async"): 138 | async = outputConfig.getbool(i,"async") 139 | else: 140 | async = False 141 | 142 | pluginData = {} 143 | 144 | class MissingField(Exception): pass 145 | 146 | for requiredField in reqd: 147 | if outputConfig.has_option(i,requiredField): 148 | pluginData[requiredField]=outputConfig.get(i,requiredField) 149 | else: 150 | print "Error: Missing required field '" + requiredField + "' for output plugin " + i 151 | raise MissingField 152 | for optionalField in opt: 153 | if outputConfig.has_option(i,optionalField): 154 | pluginData[optionalField]=outputConfig.get(i,optionalField) 155 | instClass = outputClass(pluginData) 156 | instClass.async = async 157 | outputPlugins.append(instClass) 158 | print ("Success: Loaded output plugin " + i) 159 | except Exception as e: #add specific exception for missing module 160 | print("Error: Did not import output plugin " + i ) 161 | raise e 162 | 163 | if not os.path.isfile("settings.cfg"): 164 | print "Unable to access config file: settings.cfg" 165 | 166 | mainConfig = ConfigParser.SafeConfigParser() 167 | mainConfig.read("settings.cfg") 168 | 169 | lastUpdated = 0 170 | delayTime = mainConfig.getfloat("Main","uploadDelay") 171 | redPin = mainConfig.getint("Main","redPin") 172 | greenPin = mainConfig.getint("Main","greenPin") 173 | GPIO.setup(redPin,GPIO.OUT,initial=GPIO.LOW) 174 | GPIO.setup(greenPin,GPIO.OUT,initial=GPIO.LOW) 175 | while True: 176 | curTime = time.time() 177 | if (curTime-lastUpdated)>delayTime: 178 | lastUpdated = curTime 179 | data = [] 180 | #Collect the data from each sensor 181 | for i in sensorPlugins: 182 | dataDict = {} 183 | val = i.getVal() 184 | if val==None: #this means it has no data to upload. 185 | continue 186 | dataDict["value"] = i.getVal() 187 | dataDict["unit"] = i.valUnit 188 | dataDict["symbol"] = i.valSymbol 189 | dataDict["name"] = i.valName 190 | dataDict["sensor"] = i.sensorName 191 | data.append(dataDict) 192 | working = True 193 | for i in outputPlugins: 194 | working = working and i.outputData(data) 195 | if working: 196 | print "Uploaded successfully" 197 | GPIO.output(greenPin,GPIO.HIGH) 198 | else: 199 | print "Failed to upload" 200 | GPIO.output(redPin,GPIO.HIGH) 201 | time.sleep(1) 202 | GPIO.output(greenPin,GPIO.LOW) 203 | GPIO.output(redPin,GPIO.LOW) 204 | -------------------------------------------------------------------------------- /outputs.cfg: -------------------------------------------------------------------------------- 1 | [Print] 2 | filename=print 3 | enabled=on 4 | 5 | [Xively] 6 | filename=xively 7 | enabled=on 8 | APIKey=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY 9 | FeedID=XXXXXXXXXX 10 | -------------------------------------------------------------------------------- /outputs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomhartley/AirPi/572bcd2ae5d76a95f22e01cef076bb483ce668ee/outputs/__init__.py -------------------------------------------------------------------------------- /outputs/output.py: -------------------------------------------------------------------------------- 1 | class Output(): 2 | def __init__(self,data): 3 | raise NotImplementedError 4 | 5 | def outputData(self,dataPoints): 6 | raise NotImplementedError 7 | -------------------------------------------------------------------------------- /outputs/print.py: -------------------------------------------------------------------------------- 1 | import output 2 | import datetime 3 | 4 | class Print(output.Output): 5 | requiredData = [] 6 | optionalData = [] 7 | def __init__(self,data): 8 | pass 9 | def outputData(self,dataPoints): 10 | print "" 11 | print "Time: " + str(datetime.datetime.now()) 12 | for i in dataPoints: 13 | print i["name"] + ": " + str(i["value"]) + " " + i["symbol"] 14 | return True 15 | -------------------------------------------------------------------------------- /outputs/xively.py: -------------------------------------------------------------------------------- 1 | import output 2 | import requests 3 | import json 4 | 5 | class Xively(output.Output): 6 | requiredData = ["APIKey","FeedID"] 7 | optionalData = [] 8 | def __init__(self,data): 9 | self.APIKey=data["APIKey"] 10 | self.FeedID=data["FeedID"] 11 | def outputData(self,dataPoints): 12 | arr = [] 13 | for i in dataPoints: 14 | arr.append({"id":i["name"],"current_value":i["value"]}) 15 | a = json.dumps({"version":"1.0.0","datastreams":arr}) 16 | try: 17 | z = requests.put("https://api.xively.com/v2/feeds/"+self.FeedID+".json",headers={"X-ApiKey":self.APIKey},data=a) 18 | if z.text!="": 19 | print "Xively Error: " + z.text 20 | return False 21 | except Exception: 22 | return False 23 | return True 24 | -------------------------------------------------------------------------------- /sensors.cfg: -------------------------------------------------------------------------------- 1 | [BMP085-temp] 2 | filename=bmp085 3 | enabled=on 4 | measurement=temp 5 | i2cbus = 1 6 | 7 | [BMP085-pres] 8 | filename=bmp085 9 | enabled=on 10 | measurement=pres 11 | mslp=on 12 | i2cbus = 1 13 | altitude=40 14 | 15 | [MCP3008] 16 | filename=mcp3008 17 | enabled=on 18 | 19 | [DHT22] 20 | filename=dht22 21 | enabled=on 22 | measurement=humidity 23 | pinNumber=4 24 | 25 | [LDR] 26 | filename=analogue 27 | enabled=on 28 | pullUpResistance=10000 29 | measurement=Light_Level 30 | adcPin = 0 31 | sensorName = LDR 32 | 33 | [TGS2600] 34 | filename=analogue 35 | enabled=off 36 | pullDownResistance=22000 37 | measurement=Smoke_Level 38 | adcPin=1 39 | sensorName=TGS2600 40 | 41 | [MiCS-2710] 42 | filename=analogue 43 | enabled=on 44 | pullDownResistance=10000 45 | measurement=Nitrogen_Dioxide 46 | adcPin=2 47 | sensorName=MiCS-2710 48 | 49 | [MiCS-5525] 50 | filename=analogue 51 | enabled=on 52 | pullDownResistance=100000 53 | measurement=Carbon_Monoxide 54 | adcPin=3 55 | sensorName=MiCS-5525 56 | 57 | [Mic] 58 | filename=analogue 59 | enabled=on 60 | measurement=Volume 61 | adcPin=4 62 | sensorName=ABM_713_RC 63 | -------------------------------------------------------------------------------- /sensors/Adafruit_I2C.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import smbus 4 | 5 | # =========================================================================== 6 | # Adafruit_I2C Base Class 7 | # =========================================================================== 8 | 9 | class Adafruit_I2C : 10 | 11 | def __init__(self, address, bus=0, debug=False): 12 | self.address = address 13 | self.bus = smbus.SMBus(bus) 14 | self.debug = debug 15 | 16 | def reverseByteOrder(self, data): 17 | "Reverses the byte order of an int (16-bit) or long (32-bit) value" 18 | # Courtesy Vishal Sapre 19 | dstr = hex(data)[2:].replace('L','') 20 | byteCount = len(dstr[::2]) 21 | val = 0 22 | for i, n in enumerate(range(byteCount)): 23 | d = data & 0xFF 24 | val |= (d << (8 * (byteCount - i - 1))) 25 | data >>= 8 26 | return val 27 | 28 | def write8(self, reg, value): 29 | "Writes an 8-bit value to the specified register/address" 30 | try: 31 | self.bus.write_byte_data(self.address, reg, value) 32 | if (self.debug): 33 | print("I2C: Wrote 0x%02X to register 0x%02X" % (value, reg)) 34 | except IOError, err: 35 | print "Error accessing 0x%02X: Check your I2C address" % self.address 36 | return -1 37 | 38 | def writeList(self, reg, list): 39 | "Writes an array of bytes using I2C format" 40 | try: 41 | self.bus.write_i2c_block_data(self.address, reg, list) 42 | except IOError, err: 43 | print "Error accessing 0x%02X: Check your I2C address" % self.address 44 | return -1 45 | 46 | def readU8(self, reg): 47 | "Read an unsigned byte from the I2C device" 48 | try: 49 | result = self.bus.read_byte_data(self.address, reg) 50 | if (self.debug): 51 | print "I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % (self.address, result & 0xFF, reg) 52 | return result 53 | except IOError, err: 54 | print "Error accessing 0x%02X: Check your I2C address" % self.address 55 | return -1 56 | 57 | def readS8(self, reg): 58 | "Reads a signed byte from the I2C device" 59 | try: 60 | result = self.bus.read_byte_data(self.address, reg) 61 | if (self.debug): 62 | print "I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % (self.address, result & 0xFF, reg) 63 | if (result > 127): 64 | return result - 256 65 | else: 66 | return result 67 | except IOError, err: 68 | print "Error accessing 0x%02X: Check your I2C address" % self.address 69 | return -1 70 | 71 | def readU16(self, reg): 72 | "Reads an unsigned 16-bit value from the I2C device" 73 | try: 74 | hibyte = self.bus.read_byte_data(self.address, reg) 75 | result = (hibyte << 8) + self.bus.read_byte_data(self.address, reg+1) 76 | if (self.debug): 77 | print "I2C: Device 0x%02X returned 0x%04X from reg 0x%02X" % (self.address, result & 0xFFFF, reg) 78 | return result 79 | except IOError, err: 80 | print "Error accessing 0x%02X: Check your I2C address" % self.address 81 | return -1 82 | 83 | def readS16(self, reg): 84 | "Reads a signed 16-bit value from the I2C device" 85 | try: 86 | hibyte = self.bus.read_byte_data(self.address, reg) 87 | if (hibyte > 127): 88 | hibyte -= 256 89 | result = (hibyte << 8) + self.bus.read_byte_data(self.address, reg+1) 90 | if (self.debug): 91 | print "I2C: Device 0x%02X returned 0x%04X from reg 0x%02X" % (self.address, result & 0xFFFF, reg) 92 | return result 93 | except IOError, err: 94 | print "Error accessing 0x%02X: Check your I2C address" % self.address 95 | return -1 96 | -------------------------------------------------------------------------------- /sensors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomhartley/AirPi/572bcd2ae5d76a95f22e01cef076bb483ce668ee/sensors/__init__.py -------------------------------------------------------------------------------- /sensors/analogue.py: -------------------------------------------------------------------------------- 1 | import mcp3008 2 | import sensor 3 | class Analogue(sensor.Sensor): 4 | requiredData = ["adcPin","measurement","sensorName"] 5 | optionalData = ["pullUpResistance","pullDownResistance"] 6 | def __init__(self, data): 7 | self.adc = mcp3008.MCP3008.sharedClass 8 | self.adcPin = int(data["adcPin"]) 9 | self.valName = data["measurement"] 10 | self.sensorName = data["sensorName"] 11 | self.pullUp, self.pullDown = None, None 12 | if "pullUpResistance" in data: 13 | self.pullUp = int(data["pullUpResistance"]) 14 | if "pullDownResistance" in data: 15 | self.pullDown = int(data["pullDownResistance"]) 16 | class ConfigError(Exception): pass 17 | if self.pullUp!=None and self.pullDown!=None: 18 | print "Please choose whether there is a pull up or pull down resistor for the " + self.valName + " measurement by only entering one of them into the settings file" 19 | raise ConfigError 20 | self.valUnit = "Ohms" 21 | self.valSymbol = "Ohms" 22 | if self.pullUp==None and self.pullDown==None: 23 | self.valUnit = "millvolts" 24 | self.valSymbol = "mV" 25 | 26 | def getVal(self): 27 | result = self.adc.readADC(self.adcPin) 28 | if result==0: 29 | print "Check wiring for the " + self.sensorName + " measurement, no voltage detected on ADC input " + str(self.adcPin) 30 | return None 31 | if result == 1023: 32 | print "Check wiring for the " + self.sensorName + " measurement, full voltage detected on ADC input " + str(self.adcPin) 33 | return None 34 | vin = 3.3 35 | vout = float(result)/1023 * vin 36 | 37 | if self.pullDown!=None: 38 | #Its a pull down resistor 39 | resOut = (self.pullDown*vin)/vout - self.pullDown 40 | elif self.pullUp!=None: 41 | resOut = self.pullUp/((vin/vout)-1) 42 | else: 43 | resOut = vout*1000 44 | return resOut 45 | 46 | -------------------------------------------------------------------------------- /sensors/bmp085.py: -------------------------------------------------------------------------------- 1 | import sensor 2 | import bmpBackend 3 | 4 | class BMP085(sensor.Sensor): 5 | bmpClass = None 6 | requiredData = ["measurement","i2cbus"] 7 | optionalData = ["altitude","mslp","unit"] 8 | def __init__(self,data): 9 | self.sensorName = "BMP085" 10 | if "temp" in data["measurement"].lower(): 11 | self.valName = "Temperature" 12 | self.valUnit = "Celsius" 13 | self.valSymbol = "C" 14 | if "unit" in data: 15 | if data["unit"]=="F": 16 | self.valUnit = "Fahrenheit" 17 | self.valSymbol = "F" 18 | elif "pres" in data["measurement"].lower(): 19 | self.valName = "Pressure" 20 | self.valSymbol = "hPa" 21 | self.valUnit = "Hectopascal" 22 | self.altitude = 0 23 | self.mslp = False 24 | if "mslp" in data: 25 | if data["mslp"].lower in ["on","true","1","yes"]: 26 | self.mslp = True 27 | if "altitude" in data: 28 | self.altitude=data["altitude"] 29 | else: 30 | print "To calculate MSLP, please provide an 'altitude' config setting (in m) for the BMP085 pressure module" 31 | self.mslp = False 32 | if (BMP085.bmpClass==None): 33 | BMP085.bmpClass = bmpBackend.BMP085(bus=int(data["i2cbus"])) 34 | return 35 | 36 | def getVal(self): 37 | if self.valName == "Temperature": 38 | temp = BMP085.bmpClass.readTemperature() 39 | if self.valUnit == "Fahrenheit": 40 | temp = temp * 1.8 + 32 41 | return temp 42 | elif self.valName == "Pressure": 43 | if self.mslp: 44 | return BMP085.bmpClass.readMSLPressure(self.altitude) * 0.01 #to convert to Hectopascals 45 | else: 46 | return BMP085.bmpClass.readPressure() * 0.01 #to convert to Hectopascals 47 | -------------------------------------------------------------------------------- /sensors/bmpBackend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import time 4 | import math 5 | 6 | from Adafruit_I2C import Adafruit_I2C 7 | 8 | # =========================================================================== 9 | # BMP085 Class 10 | # =========================================================================== 11 | 12 | class BMP085 : 13 | i2c = None 14 | 15 | # Operating Modes 16 | __BMP085_ULTRALOWPOWER = 0 17 | __BMP085_STANDARD = 1 18 | __BMP085_HIGHRES = 2 19 | __BMP085_ULTRAHIGHRES = 3 20 | 21 | # BMP085 Registers 22 | __BMP085_CAL_AC1 = 0xAA # R Calibration data (16 bits) 23 | __BMP085_CAL_AC2 = 0xAC # R Calibration data (16 bits) 24 | __BMP085_CAL_AC3 = 0xAE # R Calibration data (16 bits) 25 | __BMP085_CAL_AC4 = 0xB0 # R Calibration data (16 bits) 26 | __BMP085_CAL_AC5 = 0xB2 # R Calibration data (16 bits) 27 | __BMP085_CAL_AC6 = 0xB4 # R Calibration data (16 bits) 28 | __BMP085_CAL_B1 = 0xB6 # R Calibration data (16 bits) 29 | __BMP085_CAL_B2 = 0xB8 # R Calibration data (16 bits) 30 | __BMP085_CAL_MB = 0xBA # R Calibration data (16 bits) 31 | __BMP085_CAL_MC = 0xBC # R Calibration data (16 bits) 32 | __BMP085_CAL_MD = 0xBE # R Calibration data (16 bits) 33 | __BMP085_CONTROL = 0xF4 34 | __BMP085_TEMPDATA = 0xF6 35 | __BMP085_PRESSUREDATA = 0xF6 36 | __BMP085_READTEMPCMD = 0x2E 37 | __BMP085_READPRESSURECMD = 0x34 38 | 39 | # Private Fields 40 | _cal_AC1 = 0 41 | _cal_AC2 = 0 42 | _cal_AC3 = 0 43 | _cal_AC4 = 0 44 | _cal_AC5 = 0 45 | _cal_AC6 = 0 46 | _cal_B1 = 0 47 | _cal_B2 = 0 48 | _cal_MB = 0 49 | _cal_MC = 0 50 | _cal_MD = 0 51 | 52 | # Constructor 53 | def __init__(self, address=0x77, mode=1, bus=0, debug=False): 54 | self.i2c = Adafruit_I2C(address, bus) 55 | 56 | self.address = address 57 | self.debug = debug 58 | # Make sure the specified mode is in the appropriate range 59 | if ((mode < 0) | (mode > 3)): 60 | if (self.debug): 61 | print "Invalid Mode: Using STANDARD by default" 62 | self.mode = self.__BMP085_STANDARD 63 | else: 64 | self.mode = mode 65 | # Read the calibration data 66 | self.readCalibrationData() 67 | 68 | def readCalibrationData(self): 69 | "Reads the calibration data from the IC" 70 | self._cal_AC1 = self.i2c.readS16(self.__BMP085_CAL_AC1) # INT16 71 | self._cal_AC2 = self.i2c.readS16(self.__BMP085_CAL_AC2) # INT16 72 | self._cal_AC3 = self.i2c.readS16(self.__BMP085_CAL_AC3) # INT16 73 | self._cal_AC4 = self.i2c.readU16(self.__BMP085_CAL_AC4) # UINT16 74 | self._cal_AC5 = self.i2c.readU16(self.__BMP085_CAL_AC5) # UINT16 75 | self._cal_AC6 = self.i2c.readU16(self.__BMP085_CAL_AC6) # UINT16 76 | self._cal_B1 = self.i2c.readS16(self.__BMP085_CAL_B1) # INT16 77 | self._cal_B2 = self.i2c.readS16(self.__BMP085_CAL_B2) # INT16 78 | self._cal_MB = self.i2c.readS16(self.__BMP085_CAL_MB) # INT16 79 | self._cal_MC = self.i2c.readS16(self.__BMP085_CAL_MC) # INT16 80 | self._cal_MD = self.i2c.readS16(self.__BMP085_CAL_MD) # INT16 81 | if (self.debug): 82 | self.showCalibrationData() 83 | 84 | def showCalibrationData(self): 85 | "Displays the calibration values for debugging purposes" 86 | print "DBG: AC1 = %6d" % (self._cal_AC1) 87 | print "DBG: AC2 = %6d" % (self._cal_AC2) 88 | print "DBG: AC3 = %6d" % (self._cal_AC3) 89 | print "DBG: AC4 = %6d" % (self._cal_AC4) 90 | print "DBG: AC5 = %6d" % (self._cal_AC5) 91 | print "DBG: AC6 = %6d" % (self._cal_AC6) 92 | print "DBG: B1 = %6d" % (self._cal_B1) 93 | print "DBG: B2 = %6d" % (self._cal_B2) 94 | print "DBG: MB = %6d" % (self._cal_MB) 95 | print "DBG: MC = %6d" % (self._cal_MC) 96 | print "DBG: MD = %6d" % (self._cal_MD) 97 | 98 | def readRawTemp(self): 99 | "Reads the raw (uncompensated) temperature from the sensor" 100 | self.i2c.write8(self.__BMP085_CONTROL, self.__BMP085_READTEMPCMD) 101 | time.sleep(0.005) # Wait 5ms 102 | raw = self.i2c.readU16(self.__BMP085_TEMPDATA) 103 | if (self.debug): 104 | print "DBG: Raw Temp: 0x%04X (%d)" % (raw & 0xFFFF, raw) 105 | return raw 106 | 107 | def readRawPressure(self): 108 | "Reads the raw (uncompensated) pressure level from the sensor" 109 | self.i2c.write8(self.__BMP085_CONTROL, self.__BMP085_READPRESSURECMD + (self.mode << 6)) 110 | if (self.mode == self.__BMP085_ULTRALOWPOWER): 111 | time.sleep(0.005) 112 | elif (self.mode == self.__BMP085_HIGHRES): 113 | time.sleep(0.014) 114 | elif (self.mode == self.__BMP085_ULTRAHIGHRES): 115 | time.sleep(0.026) 116 | else: 117 | time.sleep(0.008) 118 | msb = self.i2c.readU8(self.__BMP085_PRESSUREDATA) 119 | lsb = self.i2c.readU8(self.__BMP085_PRESSUREDATA+1) 120 | xlsb = self.i2c.readU8(self.__BMP085_PRESSUREDATA+2) 121 | raw = ((msb << 16) + (lsb << 8) + xlsb) >> (8 - self.mode) 122 | if (self.debug): 123 | print "DBG: Raw Pressure: 0x%04X (%d)" % (raw & 0xFFFF, raw) 124 | return raw 125 | 126 | def readTemperature(self): 127 | "Gets the compensated temperature in degrees celcius" 128 | UT = 0 129 | X1 = 0 130 | X2 = 0 131 | B5 = 0 132 | temp = 0.0 133 | 134 | # Read raw temp before aligning it with the calibration values 135 | UT = self.readRawTemp() 136 | X1 = ((UT - self._cal_AC6) * self._cal_AC5) >> 15 137 | X2 = (self._cal_MC << 11) / (X1 + self._cal_MD) 138 | B5 = X1 + X2 139 | temp = ((B5 + 8) >> 4) / 10.0 140 | if (self.debug): 141 | print "DBG: Calibrated temperature = %f C" % temp 142 | return temp 143 | 144 | def readPressure(self): 145 | "Gets the compensated pressure in pascal" 146 | UT = 0 147 | UP = 0 148 | B3 = 0 149 | B5 = 0 150 | B6 = 0 151 | X1 = 0 152 | X2 = 0 153 | X3 = 0 154 | p = 0 155 | B4 = 0 156 | B7 = 0 157 | 158 | UT = self.readRawTemp() 159 | UP = self.readRawPressure() 160 | 161 | # You can use the datasheet values to test the conversion results 162 | # dsValues = True 163 | dsValues = False 164 | 165 | if (dsValues): 166 | UT = 27898 167 | UP = 23843 168 | self._cal_AC6 = 23153 169 | self._cal_AC5 = 32757 170 | self._cal_MC = -8711 171 | self._cal_MD = 2868 172 | self._cal_B1 = 6190 173 | self._cal_B2 = 4 174 | self._cal_AC3 = -14383 175 | self._cal_AC2 = -72 176 | self._cal_AC1 = 408 177 | self._cal_AC4 = 32741 178 | self.mode = self.__BMP085_ULTRALOWPOWER 179 | if (self.debug): 180 | self.showCalibrationData() 181 | 182 | # True Temperature Calculations 183 | X1 = ((UT - self._cal_AC6) * self._cal_AC5) >> 15 184 | X2 = (self._cal_MC << 11) / (X1 + self._cal_MD) 185 | B5 = X1 + X2 186 | if (self.debug): 187 | print "DBG: X1 = %d" % (X1) 188 | print "DBG: X2 = %d" % (X2) 189 | print "DBG: B5 = %d" % (B5) 190 | print "DBG: True Temperature = %.2f C" % (((B5 + 8) >> 4) / 10.0) 191 | 192 | # Pressure Calculations 193 | B6 = B5 - 4000 194 | X1 = (self._cal_B2 * (B6 * B6) >> 12) >> 11 195 | X2 = (self._cal_AC2 * B6) >> 11 196 | X3 = X1 + X2 197 | B3 = (((self._cal_AC1 * 4 + X3) << self.mode) + 2) / 4 198 | if (self.debug): 199 | print "DBG: B6 = %d" % (B6) 200 | print "DBG: X1 = %d" % (X1) 201 | print "DBG: X2 = %d" % (X2) 202 | print "DBG: B3 = %d" % (B3) 203 | 204 | X1 = (self._cal_AC3 * B6) >> 13 205 | X2 = (self._cal_B1 * ((B6 * B6) >> 12)) >> 16 206 | X3 = ((X1 + X2) + 2) >> 2 207 | B4 = (self._cal_AC4 * (X3 + 32768)) >> 15 208 | B7 = (UP - B3) * (50000 >> self.mode) 209 | if (self.debug): 210 | print "DBG: X1 = %d" % (X1) 211 | print "DBG: X2 = %d" % (X2) 212 | print "DBG: B4 = %d" % (B4) 213 | print "DBG: B7 = %d" % (B7) 214 | 215 | if (B7 < 0x80000000): 216 | p = (B7 * 2) / B4 217 | else: 218 | p = (B7 / B4) * 2 219 | 220 | X1 = (p >> 8) * (p >> 8) 221 | X1 = (X1 * 3038) >> 16 222 | X2 = (-7375 * p) >> 16 223 | if (self.debug): 224 | print "DBG: p = %d" % (p) 225 | print "DBG: X1 = %d" % (X1) 226 | print "DBG: X2 = %d" % (X2) 227 | 228 | p = p + ((X1 + X2 + 3791) >> 4) 229 | if (self.debug): 230 | print "DBG: Pressure = %d Pa" % (p) 231 | 232 | return p 233 | 234 | def readAltitude(self, seaLevelPressure=101325): 235 | "Calculates the altitude in meters" 236 | altitude = 0.0 237 | pressure = float(self.readPressure()) 238 | altitude = 44330.0 * (1.0 - pow(pressure / seaLevelPressure, 0.1903)) 239 | if (self.debug): 240 | print "DBG: Altitude = %d" % (altitude) 241 | return altitude 242 | 243 | return 0 244 | 245 | def readMSLPressure(self, altitude): 246 | "Calculates the mean sea level pressure" 247 | pressure = float(self.readPressure()) 248 | T0 = float(altitude) / 44330 249 | T1 = math.pow(1 - T0, 5.255) 250 | mslpressure = pressure / T1 251 | return mslpressure 252 | 253 | if __name__=="__main__": 254 | bmp = BMP085() 255 | print str(bmp.readTemperature()) + " C" 256 | print str(bmp.readPressure()) + " Pa" 257 | -------------------------------------------------------------------------------- /sensors/dht22.py: -------------------------------------------------------------------------------- 1 | import sensor 2 | import dhtreader 3 | import time 4 | class DHT22(sensor.Sensor): 5 | requiredData = ["measurement","pinNumber"] 6 | optionalData = ["unit"] 7 | def __init__(self,data): 8 | dhtreader.init() 9 | dhtreader.lastDataTime = 0 10 | dhtreader.lastData = (None,None) 11 | self.sensorName = "DHT22" 12 | self.pinNum = int(data["pinNumber"]) 13 | if "temp" in data["measurement"].lower(): 14 | self.valName = "Temperature" 15 | self.valUnit = "Celsius" 16 | self.valSymbol = "C" 17 | if "unit" in data: 18 | if data["unit"]=="F": 19 | self.valUnit = "Fahrenheit" 20 | self.valSymbol = "F" 21 | elif "h" in data["measurement"].lower(): 22 | self.valName = "Relative_Humidity" 23 | self.valSymbol = "%" 24 | self.valUnit = "% Relative Humidity" 25 | return 26 | 27 | def getVal(self): 28 | tm = dhtreader.lastDataTime 29 | if (time.time()-tm)<2: 30 | t, h = dhtreader.lastData 31 | else: 32 | tim = time.time() 33 | try: 34 | t, h = dhtreader.read(22,self.pinNum) 35 | except Exception: 36 | t, h = dhtreader.lastData 37 | dhtreader.lastData = (t,h) 38 | dhtreader.lastDataTime=tim 39 | if self.valName == "Temperature": 40 | temp = t 41 | if self.valUnit == "Fahrenheit": 42 | temp = temp * 1.8 + 32 43 | return temp 44 | elif self.valName == "Relative_Humidity": 45 | return h 46 | -------------------------------------------------------------------------------- /sensors/dhtreader.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomhartley/AirPi/572bcd2ae5d76a95f22e01cef076bb483ce668ee/sensors/dhtreader.so -------------------------------------------------------------------------------- /sensors/mcp3008.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import RPi.GPIO as GPIO 4 | import sensor 5 | 6 | class MCP3008(sensor.Sensor): 7 | requiredData = [] 8 | optionalData = ["mosiPin","misoPin","csPin","clkPin"] 9 | sharedClass = None 10 | def __init__(self, data): 11 | GPIO.setmode(GPIO.BCM) 12 | GPIO.setwarnings(False) 13 | self.SPIMOSI = 23 14 | self.SPIMISO = 24 15 | self.SPICLK = 18 16 | self.SPICS = 25 17 | if "mosiPin" in data: 18 | self.SPIMOSI = data["mosiPin"] 19 | if "misoPin" in data: 20 | self.SPIMISO = data["misoPin"] 21 | if "clkPin" in data: 22 | self.SPICLK = data["clkPin"] 23 | if "csPin" in data: 24 | self.SPICS = data["csPin"] 25 | GPIO.setup(self.SPIMOSI, GPIO.OUT) 26 | GPIO.setup(self.SPIMISO, GPIO.IN) 27 | GPIO.setup(self.SPICLK, GPIO.OUT) 28 | GPIO.setup(self.SPICS, GPIO.OUT) 29 | if MCP3008.sharedClass == None: 30 | MCP3008.sharedClass = self 31 | 32 | #read SPI data from MCP3008 chip, 8 possible adc's (0 thru 7) 33 | def readADC(self,adcnum): 34 | if ((adcnum > 7) or (adcnum < 0)): 35 | return -1 36 | GPIO.output(self.SPICS, True) 37 | 38 | GPIO.output(self.SPICLK, False) # start clock low 39 | GPIO.output(self.SPICS, False) # bring CS low 40 | 41 | commandout = adcnum 42 | commandout |= 0x18 # start bit + single-ended bit 43 | commandout <<= 3 # we only need to send 5 bits here 44 | for i in range(5): 45 | if (commandout & 0x80): 46 | GPIO.output(self.SPIMOSI, True) 47 | else: 48 | GPIO.output(self.SPIMOSI, False) 49 | commandout <<= 1 50 | GPIO.output(self.SPICLK, True) 51 | GPIO.output(self.SPICLK, False) 52 | 53 | adcout = 0 54 | # read in one empty bit, one null bit and 10 ADC bits 55 | for i in range(11): 56 | GPIO.output(self.SPICLK, True) 57 | GPIO.output(self.SPICLK, False) 58 | adcout <<= 1 59 | if (GPIO.input(self.SPIMISO)): 60 | adcout |= 0x1 61 | 62 | GPIO.output(self.SPICS, True) 63 | return adcout 64 | 65 | def getVal(self): 66 | return None #not that kind of plugin, this is to be used by other plugins 67 | -------------------------------------------------------------------------------- /sensors/sensor.py: -------------------------------------------------------------------------------- 1 | class Sensor(): 2 | def __init__(data): 3 | raise NotImplementedError 4 | 5 | def getData(): 6 | raise NotImplementedError 7 | -------------------------------------------------------------------------------- /settings.cfg: -------------------------------------------------------------------------------- 1 | [Main] 2 | uploadDelay = 5 ;how long to wait before uploading again 3 | redPin=10 4 | greenPin=22 5 | --------------------------------------------------------------------------------