sds011
#!/usr/bin/python2 # only works with python2.7
# coding=utf-8
'''
forked from A. Adamski's gist: https://gist.github.com/kadamski/92653913a53baf9dd1a8
"DATASHEET": http://cl.ly/ekot
Binh Nguyen, November 2018
adapted from kadamski script with additional features:
1. logging
2. calculate AQI (US)
3. publish to MQTT broker.
4. Schedule interval run mode
- required pyserial, install by: sudo apt install python-serial
or pip install pyserial
'''
from __future__ import print_function
import serial, struct, sys, time, json
import paho.mqtt.publish as publish
DEBUG = 1
CMD_MODE = 2
CMD_QUERY_DATA = 4
CMD_DEVICE_ID = 5
CMD_SLEEP = 6
CMD_FIRMWARE = 7
CMD_WORKING_PERIOD = 8
MODE_ACTIVE = 0
MODE_QUERY = 1
ser = serial.Serial()
ser.port = "/dev/ttyUSB0"
ser.baudrate = 9600
ser.open()
ser.flushInput()
byte, data = 0, ""
global lastTime
lastTime = 0
logFile = '/home/pi/Desktop/sambaShare/weather/sds01.csv'
AQIs = {'Good':{'aqi':[0,50], 'PM2.5':[0,12.0], 'PM10':[0,54]},
'Moderate':{'aqi':[51,100], 'PM2.5':[12.1,35.4], 'PM10':[54,154]},
'Unhealthy for Sensitive Groups':{'aqi':[101,150], 'PM2.5':[35.5,55.4], 'PM10':[155,254]},
'Unhealthy':{'aqi':[151,200], 'PM2.5':[55.5,150.4], 'PM10':[255,354]},
'Very Unhealthy':{'aqi':[201,300], 'PM2.5':[150.5,250.4], 'PM10':[355,425]},
'Hazardous':{'aqi':[301,400], 'PM2.5':[250.4,350.4], 'PM10':[425,504]},
'Very Harzadous':{'aqi':[401,500], 'PM2.5':[350.5,500.4], 'PM10':[504,604]}
}
def findRange(pollulant, conc):
'''return key of the category, PM2.5, 30 ug/m3'''
for key,value in AQIs.items():
range_ = value[pollulant]
if conc >= range_[0] and conc <= range_[1]:
return key
def calAQI(pollulant,conc):
'''return AQI based US EPA for PM2.5 and PM10, p16, 2013 Technical Asst.'''
key = findRange(pollulant, conc)
I_Lo = AQIs[key]['aqi'][0]
I_Hi = AQIs[key]['aqi'][1]
BP_Lo = AQIs[key][pollulant][0]
BP_Hi = AQIs[key][pollulant][1]
AQI_X = (I_Hi-I_Lo)*(conc-BP_Lo)/(BP_Hi-BP_Lo) + I_Lo
return round(AQI_X,1), key
def dump(d, prefix=''):
print(prefix + ' '.join(x.encode('hex') for x in d))
def construct_command(cmd, data=[]):
assert len(data) <= 12
data += [0,]*(12-len(data))
checksum = (sum(data)+cmd-2)%256
ret = "\xaa\xb4" + chr(cmd)
ret += ''.join(chr(x) for x in data)
ret += "\xff\xff" + chr(checksum) + "\xab"
if DEBUG:
dump(ret, '> ')
return ret
def process_data(d):
r = struct.unpack('<HHxxBB', d[2:])
pm25 = r[0]/10.0
pm10 = r[1]/10.0
checksum = sum(ord(v) for v in d[2:8])%256
print("PM 2.5: {} μg/m^3 PM 10: {} μg/m^3 CRC={}".format(pm25, pm10, "OK" if (checksum==r[2] and r[3]==0xab) else "NOK"))
def process_version(d):
r = struct.unpack('<BBBHBB', d[3:])
checksum = sum(ord(v) for v in d[2:8])%256
print("Y: {}, M: {}, D: {}, ID: {}, CRC={}".format(r[0], r[1], r[2], hex(r[3]), "OK" if (checksum==r[4] and r[5]==0xab) else "NOK"))
def read_response():
byte = 0
while byte != "\xaa":
byte = ser.read(size=1)
d = ser.read(size=9)
if DEBUG:
dump(d, '< ')
return byte + d
def cmd_set_mode(mode=MODE_QUERY):
ser.write(construct_command(CMD_MODE, [0x1, mode]))
read_response()
def cmd_query_data():
ser.write(construct_command(CMD_QUERY_DATA))
d = read_response()
if d[1] == "\xc0":
process_data(d)
def cmd_set_sleep(sleep=1):
mode = 0 if sleep else 1
ser.write(construct_command(CMD_SLEEP, [0x1, mode]))
read_response()
def cmd_set_working_period(period):
ser.write(construct_command(CMD_WORKING_PERIOD, [0x1, period]))
read_response()
def cmd_firmware_ver():
ser.write(construct_command(CMD_FIRMWARE))
d = read_response()
process_version(d)
def cmd_set_id(id):
id_h = (id>>8) % 256
id_l = id % 256
ser.write(construct_command(CMD_DEVICE_ID, [0]*10+[id_l, id_h]))
read_response()
def run():
''' turn on, stablize for 25 seconds before reading and then sleep'''
cmd_set_sleep(0)
cmd_set_mode(1)
cmd_firmware_ver()
time.sleep(25)
cmd_query_data()
cmd_set_mode(0)
time.sleep(5)
cmd_set_sleep()
return None
def push_MQTT(mesg):
'''push message to a local broker'''
topic = 'sensors/nova_sds011'
host = '192.168.1.xx
auth = {'username':'janeoe', 'password':'password'}
try:
publish.single(topic, mesg, hostname=host, auth=auth)
except socket.error:
pass
return None
def schedule(snapTime=600):
global lastTime
if time.time()-lastTime >=snapTime:
ts = time.strftime('%x %X', time.localtime())
print("Run script at: {}, lastRun was {}".format(ts, lastTime))
lastTime=time.time()
run()
aqi_pm25, status_pm25 = calAQI('PM2.5', pm25)
aqi_pm10, status_pm10 = calAQI('PM10', pm10)
if aqi_pm25 > aqi_pm10:
air_quality = status_pm25
air_aqi = aqi_pm25
else:
air_quality = status_pm10
air_aqi = aqi_pm10
mesg = {'sensor':'Nova_SDS011','timestamp':ts, 'pm25':pm25, 'pm10':pm10,\
'aqi25':aqi_pm25,'aqi10':aqi_pm10,'level':air_quality}
mesg = json.dumps(mesg,'UTF-8')
push_MQTT(mesg)
print('Pushed MSG {}'.format(mesg))
payload = ','.join([ts, str(pm25), str(pm10), str(aqi_pm25), str(aqi_pm10), air_quality])+ '\n'
with open(logFile, 'a+') as f:
f.write(payload)
print('Save data: {}'.format(payload))
print('Log file: {}'.format(logFile))
return None
if __name__ == "__main__":
while True:
'''keep loop running and read sensor every 5 minutes
schedule(300)