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