mirror of
https://github.com/NotAShelf/air-quality-monitor.git
synced 2024-11-22 13:20:48 +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