mirror of
				https://github.com/NotAShelf/air-quality-monitor.git
				synced 2025-10-31 11:12:38 +00:00 
			
		
		
		
	init
This commit is contained in:
		
				commit
				
					
						9df325b73e
					
				
			
		
					 10 changed files with 268 additions and 0 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | **/.DS_Store | ||||||
|  | env | ||||||
|  | *.pyc | ||||||
							
								
								
									
										7
									
								
								Dockerfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Dockerfile
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | FROM python:3.8 | ||||||
|  | WORKDIR /code | ||||||
|  | COPY src/requirements.txt . | ||||||
|  | RUN pip install -r requirements.txt | ||||||
|  | COPY src . | ||||||
|  | ENTRYPOINT [ "python" ] | ||||||
|  | CMD [ "app.py" ] | ||||||
							
								
								
									
										22
									
								
								Makefile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Makefile
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | PI_IP_ADDRESS=10.0.0.172 | ||||||
|  | PI_USERNAME=pi | ||||||
|  | 
 | ||||||
|  | .PHONY: run | ||||||
|  | run: | ||||||
|  | 	@docker-compose up | ||||||
|  | 
 | ||||||
|  | .PHONY: install | ||||||
|  | install: | ||||||
|  | 	@cd scripts && bash install.sh | ||||||
|  | 
 | ||||||
|  | .PHONY: copy | ||||||
|  | copy: | ||||||
|  | 	@rsync -a $(shell pwd) --exclude env $(PI_USERNAME)@$(PI_IP_ADDRESS):/home/$(PI_USERNAME) | ||||||
|  | 
 | ||||||
|  | .PHONY: shell | ||||||
|  | shell: | ||||||
|  | 	@ssh $(PI_USERNAME)@$(PI_IP_ADDRESS) | ||||||
|  | 
 | ||||||
|  | .PHONY: build | ||||||
|  | build: | ||||||
|  | 	@docker-compose build | ||||||
							
								
								
									
										35
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | # Raspberry Pi Air Quality Monitor | ||||||
|  | A simple air quality monitoring service for the Raspberry Pi. | ||||||
|  | 
 | ||||||
|  | ## Installation | ||||||
|  | Clone the repository and run the following: | ||||||
|  | ```bash | ||||||
|  | make install | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Running | ||||||
|  | To run, use the run command: | ||||||
|  | ```bash | ||||||
|  | make run | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Architecture | ||||||
|  | This project uses python, flask, docker-compose and redis to create a simple web server to display the latest historical values from the sensor. | ||||||
|  | 
 | ||||||
|  | ## Example Data | ||||||
|  | Some example data you can get from the sensor includes the following: | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |     "device_id": 13358, | ||||||
|  |     "pm10": 10.8, | ||||||
|  |     "pm2.5": 4.8, | ||||||
|  |     "timestamp": "2021-06-16 22:12:13.887717" | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | The sensor reads two particulate matter (PM) values. | ||||||
|  | 
 | ||||||
|  | PM10 is a measure of particles less than 10 micrometers, whereas PM 2.5 is a measurement of finer particles, less than 2.5 micrometers. | ||||||
|  | 
 | ||||||
|  | Different particles are from different sources, and can be hazardous to different parts of the respiratory system. | ||||||
							
								
								
									
										20
									
								
								docker-compose.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								docker-compose.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | version: "3.4" | ||||||
|  | services: | ||||||
|  |   redis: | ||||||
|  |     image: redis | ||||||
|  |     volumes: | ||||||
|  |       - ./data/redis:/data | ||||||
|  |   web: | ||||||
|  |     build: . | ||||||
|  |     image: pi-air-quality-monitor | ||||||
|  |     devices: | ||||||
|  |       - "/dev/ttyUSB0:/dev/ttyUSB0" | ||||||
|  |     environment: | ||||||
|  |         - REDIS_HOST=redis | ||||||
|  |         - PORT=8000 | ||||||
|  |     volumes: | ||||||
|  |       - ./src:/code | ||||||
|  |     depends_on: | ||||||
|  |       - "redis" | ||||||
|  |     ports: | ||||||
|  |       - "8000:8000" | ||||||
							
								
								
									
										13
									
								
								scripts/install.sh
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								scripts/install.sh
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | #!/bin/bash | ||||||
|  | # install.sh | ||||||
|  | 
 | ||||||
|  | cd ../ | ||||||
|  | 
 | ||||||
|  | sudo apt-get update | ||||||
|  | sudo apt-get install -y python3 python3-pip python3-dev libffi-dev libssl-dev | ||||||
|  | 
 | ||||||
|  | curl -sSL https://get.docker.com | sh | ||||||
|  | sudo usermod -aG docker ${USER} | ||||||
|  | sudo pip3 install docker-compose | ||||||
|  | sudo systemctl enable docker | ||||||
|  | newgrp docker | ||||||
							
								
								
									
										28
									
								
								src/AirQualityMonitor.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/AirQualityMonitor.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import time | ||||||
|  | from sds011 import SDS011 | ||||||
|  | import redis | ||||||
|  | 
 | ||||||
|  | redis_client = redis.StrictRedis(host=os.environ.get('REDIS_HOST'), port=6379, db=0) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AirQualityMonitor(): | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         self.sds = SDS011(port='/dev/ttyUSB0') | ||||||
|  |         self.sds.set_working_period(rate=1) | ||||||
|  | 
 | ||||||
|  |     def get_measurement(self): | ||||||
|  |         return { | ||||||
|  |             'time': int(time.time()), | ||||||
|  |             'measurement': self.sds.read_measurement(), | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     def save_measurement_to_redis(self): | ||||||
|  |         """Saves measurement to redis db""" | ||||||
|  |         redis_client.lpush('measurements', json.dumps(self.get_measurement(), default=str)) | ||||||
|  | 
 | ||||||
|  |     def get_last_n_measurements(self, n=10): | ||||||
|  |         """Returns the last n measurements in the list""" | ||||||
|  |         return [json.loads(x) for x in redis_client.lrange('measurements', -n, -1)] | ||||||
							
								
								
									
										75
									
								
								src/app.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/app.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | ||||||
|  | import os | ||||||
|  | import time | ||||||
|  | from flask import Flask, request, jsonify, render_template | ||||||
|  | from AirQualityMonitor import AirQualityMonitor | ||||||
|  | from apscheduler.schedulers.background import BackgroundScheduler | ||||||
|  | import redis | ||||||
|  | import atexit | ||||||
|  | from flask_cors import CORS, cross_origin | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | app = Flask(__name__) | ||||||
|  | cors = CORS(app) | ||||||
|  | app.config['CORS_HEADERS'] = 'Content-Type' | ||||||
|  | aqm = AirQualityMonitor() | ||||||
|  | 
 | ||||||
|  | scheduler = BackgroundScheduler() | ||||||
|  | scheduler.add_job(func=aqm.save_measurement_to_redis, trigger="interval", seconds=60) | ||||||
|  | scheduler.start() | ||||||
|  | atexit.register(lambda: scheduler.shutdown()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def reconfigure_data(measurement): | ||||||
|  |     """Reconfigures data for chart.js""" | ||||||
|  |     current = int(time.time()) | ||||||
|  |     measurement = measurement.reverse() | ||||||
|  |     return { | ||||||
|  |         'labels': [int((current - (x['time'])) / 60) for x in measurement], | ||||||
|  |         'pm10': { | ||||||
|  |             'label': 'pm10', | ||||||
|  |             'data': [x['measurement']['pm10'] for x in measurement], | ||||||
|  |             'backgroundColor': '#cc0000', | ||||||
|  |             'borderColor': '#cc0000', | ||||||
|  |             'borderWidth': 3, | ||||||
|  |         }, | ||||||
|  |         'pm2': { | ||||||
|  |             'label': 'pm2.5', | ||||||
|  |             'data': [x['measurement']['pm2.5'] for x in measurement], | ||||||
|  |             'backgroundColor': '#42C0FB', | ||||||
|  |             'borderColor': '#42C0FB', | ||||||
|  |             'borderWidth': 3, | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | @app.route('/') | ||||||
|  | def index(): | ||||||
|  |     """Index page for the application""" | ||||||
|  |     context = { | ||||||
|  |         'historical': reconfigure_data(aqm.get_last_n_measurements()), | ||||||
|  |     } | ||||||
|  |     return render_template('index.html', context=context) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.route('/api/') | ||||||
|  | @cross_origin() | ||||||
|  | 
 | ||||||
|  | def api(): | ||||||
|  |     """Returns historical data from the sensor""" | ||||||
|  |     context = { | ||||||
|  |         'historical': reconfigure_data(aqm.get_last_n_measurements()), | ||||||
|  |     } | ||||||
|  |     return jsonify(context) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.route('/api/now/') | ||||||
|  | def api_now(): | ||||||
|  |     """Returns latest data from the sensor""" | ||||||
|  |     context = { | ||||||
|  |         'current': aqm.get_measurement(), | ||||||
|  |     } | ||||||
|  |     return jsonify(context) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     app.run(debug=True, use_reloader=False, host='0.0.0.0', port=int(os.environ.get('PORT', '8000'))) | ||||||
							
								
								
									
										6
									
								
								src/requirements.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/requirements.txt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | sds011==0.2.0 | ||||||
|  | ipython | ||||||
|  | flask | ||||||
|  | redis | ||||||
|  | APScheduler==3.7.0 | ||||||
|  | flask-cors | ||||||
							
								
								
									
										59
									
								
								src/templates/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/templates/index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,59 @@ | ||||||
|  | <!doctype html> | ||||||
|  | <html lang="en"> | ||||||
|  | 
 | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  |     <meta name="description" content=""> | ||||||
|  |     <title>Raspberry Pi Air Quality Monitor</title> | ||||||
|  | 
 | ||||||
|  |     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" | ||||||
|  |         integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> | ||||||
|  | 
 | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     <div class="container"> | ||||||
|  |         <div class="row"> | ||||||
|  |             <div class="col-12"> | ||||||
|  |                 <h1>Raspberry Pi Air Quality Monitor</h1> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="row"> | ||||||
|  |             <div class="col-12"> | ||||||
|  |                 <canvas id="historicalChart" width="400" height="200"></canvas> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" | ||||||
|  |         integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"> | ||||||
|  |     </script> | ||||||
|  |     <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> | ||||||
|  | 
 | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.3.2/chart.min.js" integrity="sha512-VCHVc5miKoln972iJPvkQrUYYq7XpxXzvqNfiul1H4aZDwGBGC0lq373KNleaB2LpnC2a/iNfE5zoRYmB4TRDQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> | ||||||
|  |     <script> | ||||||
|  |          | ||||||
|  |         $.getJSON('http://10.0.0.172:8000/api/', function(data) { | ||||||
|  |             var ctx = document.getElementById('historicalChart').getContext('2d'); | ||||||
|  |             var historicalChart = new Chart(ctx, { | ||||||
|  |                 type: 'line', | ||||||
|  |                 data: { | ||||||
|  |                     labels: data.historical.labels, | ||||||
|  |                     datasets: [data.historical.pm10, data.historical.pm2] | ||||||
|  |                 }, | ||||||
|  |                 options: { | ||||||
|  |                     scales: { | ||||||
|  |                         y: { | ||||||
|  |                             beginAtZero: true | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }) | ||||||
|  |          | ||||||
|  |          | ||||||
|  |         </script> | ||||||
|  |          | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Ryder Damen
				Ryder Damen