mirror of
https://github.com/NotAShelf/air-quality-monitor.git
synced 2024-11-26 15:16:49 +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…
Reference in a new issue