Closed Magnum-Pl closed 7 years ago
I included support for this sensor in the latest release (v5.1.0). Please upgrade and let me know if it works. Thanks.
I can't get the MH-Z16 to display any sensor data.
I just made a commit to fix this issue and released v5.1.5. Let me know if there's still an issue after an update. Thanks.
wow, I had added support for the MH-Z19 sensor, not the MH-Z16. Let me duplicate and add support for that one
Okay, that was weird. I had actually added the MH-Z19 right before you created this issue, and I just thought it was fortuitous. We both overlooked that version number.
Well, there now should be I2C and UART support for the MH-Z16 CO2 sensor in the latest release, v5.1.6. If you could test both interfaces, it would be appreciated. I'm about to leave my home, so I'm not sure how much I can help if there's an issue, but I'll be around to check this issue. Thanks.
I didn't catch the difference until I tried to add the sensor on the inputs page! I thought it might work because they're part of the same family.
The MH-Z16 works great with UART, but I can't get any data to display with I2C.
I'm able to use the NDIR.py and example.py scripts to test the sensor and get data that way through I2C, but I can't get the sensor to display anything in Mycodo.
The MH-Z16 datasheet says to use the address 0x9A, but that didn't work. I used "i2cdetect -y 1" to determine that the MH-Z16 is using the I2C address 0x4D, but that didn't work either.
Im able to add the sensor on the Inputs page. When I try to activate it I get a message in an orange box that says:
Error: Could not activate Sensor controller with ID 6: 'SensorController' object has no attribute 'i2c_address'
That seems like an easy fix if that's the error. Let me check the code and get back to you with what to edit and retest the I2C.
It appears I forgot to add MH_Z16_I2C
to the list of I2C devices at line 158 of ~/Mycodo/mycodo/config.py
Line 158: LIST_DEVICES_I2C = [
If you add it, so it looks like the following, it should work.
LIST_DEVICES_I2C = [
'ADS1x15',
'AM2315',
'ATLAS_PH_I2C',
'ATLAS_PT1000_I2C',
'BH1750',
'BME280',
'BMP',
'BMP180',
'BMP280',
'CHIRP',
'HTU21D',
'MH_Z16_I2C',
'MCP342x',
'SHT2x',
'TMP006',
'TSL2561',
'TSL2591'
]
Make sure to save, then reload the daemon
sudo service mycodo restart
I made that edit and got another error while activating the sensor.
Error: Could not activate Sensor controller with ID 6: 'MHZ16Sensor' object has no attribute 'interface'
In ~/Mycodo/mycodo/sensors/mh_z16.py, move line 28 (self.interface = interface
) to after line 19, so it looks like this:
super(MHZ16Sensor, self).__init__()
self.interface = interface
if self.interface == 'UART':
self.logger = logging.getLogger(
"mycodo.sensors.mhz16.{dev}".format(dev=device_loc.replace('/', '')))
elif self.interface == 'I2C':
self.logger = logging.getLogger(
"mycodo.sensors.mhz16.{dev}".format(dev=i2c_address))
self.k30_lock_file = None
self._co2 = 0
Apologies, I made this module in a hurry before leaving the house and didn't really look at the code closer than pulling the UART and I2C code from the sources and dropping it in. We at least got the UART part working on the first try ;)
No worries. I appreciate all your effort and I'm glad to help get things sorted.
The sensor data is showing up on the Live Measurements page and I'm no longer getting any errors on the Inputs page after that edit, but Im still getting this error in the log:
2017-08-11 22:08:05,602 - mycodo.sensors.mhz16.77 - ERROR - MHZ16Sensor raised an exception when taking a reading: 121 2017-08-11 22:08:05,603 - mycodo.sensor_6 - ERROR - StopIteration raised. Possibly could not read sensor. Ensure it's connected properly and detected.
I'm not sure what that is, so we'll have to change the logging line from error to exception. In ~/Mycodo/mycodo/sensors/mh_z16.py change line 169 from error
to exception
, like so:
self.logger.exception(
"{cls} raised an exception when taking a reading: "
"{err}".format(cls=type(self).__name__, err=e))
This will print the entire exception traceback.
Ok, I made the edit and this is what I'm getting in the log:
Okay. Here's what I would do to fix this, I'm not sure why there are read issues, but I modified the module to be more like the example script. There are too many edits to list them all, so here's the whole ~/Mycodo/mycodo/sensors/mh_z16.py module to test:
# coding=utf-8
from lockfile import LockFile
import logging
import serial
import smbus
import struct
import time
from .base_sensor import AbstractSensor
from sensorutils import is_device
class MHZ16Sensor(AbstractSensor):
""" A sensor support class that monitors the MH-Z16's CO2 concentration """
def __init__(self, interface, device_loc=None, baud_rate=None,
i2c_address=None, i2c_bus=None):
super(MHZ16Sensor, self).__init__()
self.interface = interface
if self.interface == 'UART':
self.logger = logging.getLogger(
"mycodo.sensors.mhz16.{dev}".format(dev=device_loc.replace('/', '')))
elif self.interface == 'I2C':
self.logger = logging.getLogger(
"mycodo.sensors.mhz16.{dev}".format(dev=i2c_address))
self.k30_lock_file = None
self._co2 = 0
if self.interface == 'UART':
# Check if device is valid
self.serial_device = is_device(device_loc)
if self.serial_device:
try:
self.ser = serial.Serial(self.serial_device,
baudrate=baud_rate,
timeout=1)
self.k30_lock_file = "/var/lock/sen-mhz16-{}".format(device_loc.replace('/', ''))
except serial.SerialException:
self.logger.exception('Opening serial')
else:
self.logger.error(
'Could not open "{dev}". '
'Check the device location is correct.'.format(
dev=device_loc))
elif self.interface == 'I2C':
self.cmd_measure = [0xFF, 0x01, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63]
self.IOCONTROL = 0X0E << 3
self.FCR = 0X02 << 3
self.LCR = 0X03 << 3
self.DLL = 0x00 << 3
self.DLH = 0X01 << 3
self.THR = 0X00 << 3
self.RHR = 0x00 << 3
self.TXLVL = 0X08 << 3
self.RXLVL = 0X09 << 3
self.i2c_address = i2c_address
self.i2c = smbus.SMBus(i2c_bus)
self.begin()
def __repr__(self):
""" Representation of object """
return "<{cls}(co2={co2})>".format(
cls=type(self).__name__,
co2="{0:.2f}".format(self._co2))
def __str__(self):
""" Return CO2 information """
return "CO2: {co2}".format(co2="{0:.2f}".format(self._co2))
def __iter__(self): # must return an iterator
""" MH-Z16 iterates through live CO2 readings """
return self
def next(self):
""" Get next CO2 reading """
if self.read(): # raised an error
raise StopIteration # required
return dict(co2=float('{0:.2f}'.format(self._co2)))
def info(self):
conditions_measured = [
("CO2", "co2", "float", "0.00", self._co2, self.co2)
]
return conditions_measured
@property
def co2(self):
""" CO2 concentration in ppmv """
if not self._co2: # update if needed
self.read()
return self._co2
def get_measurement(self):
""" Gets the MH-Z16's CO2 concentration in ppmv via UART"""
self._co2 = None
if self.interface == 'UART':
self.ser.flushInput()
time.sleep(1)
self.ser.write("\xff\x01\x86\x00\x00\x00\x00\x00\x79")
time.sleep(.01)
resp = self.ser.read(9)
if len(resp) != 0:
high_level = struct.unpack('B', resp[2])[0]
low_level = struct.unpack('B', resp[3])[0]
co2 = high_level * 256 + low_level
return co2
elif self.interface == 'I2C':
self.write_register(self.FCR, 0x07)
self.send(self.cmd_measure)
try:
co2 = self.parse(self.receive())
except:
co2 = None
return co2
return None
def read(self):
"""
Takes a reading from the MH-Z16 and updates the self._co2 value
:returns: None on success or 1 on error
"""
if self.interface == 'UART':
lock = LockFile(self.k30_lock_file)
try:
if self.interface == 'UART':
if not self.serial_device: # Don't measure if device isn't validated
return None
# Acquire lock on MHZ16 to ensure more than one read isn't
# being attempted at once.
while not lock.i_am_locking():
try: # wait 60 seconds before breaking lock
lock.acquire(timeout=60)
except Exception as e:
self.logger.error(
"{cls} 60 second timeout, {lock} lock broken: "
"{err}".format(
cls=type(self).__name__,
lock=self.k30_lock_file,
err=e))
lock.break_lock()
lock.acquire()
self._co2 = self.get_measurement()
lock.release()
elif self.interface == 'I2C':
self._co2 = self.get_measurement()
if self._co2 is None:
return 1
return # success - no errors
except Exception as e:
self.logger.error(
"{cls} raised an exception when taking a reading: "
"{err}".format(cls=type(self).__name__, err=e))
if self.interface == 'UART':
lock.release()
return 1
def begin(self):
try:
self.write_register(self.IOCONTROL, 0x08)
except IOError:
pass
self.write_register(self.FCR, 0x07)
self.write_register(self.LCR, 0x83)
self.write_register(self.DLL, 0x60)
self.write_register(self.DLH, 0x00)
self.write_register(self.LCR, 0x03)
@staticmethod
def parse(response):
checksum = 0
if len(response) < 9:
return None
for i in range(0, 9):
checksum += response[i]
if response[0] == 0xFF:
if response[1] == 0x9C:
if checksum % 256 == 0xFF:
return (response[2] << 24) + (response[3] << 16) + (response[4] << 8) + response[5]
return None
def read_register(self, reg_addr):
time.sleep(0.001)
return self.i2c.read_byte_data(self.i2c_address, reg_addr)
def write_register(self, reg_addr, val):
time.sleep(0.001)
self.i2c.write_byte_data(self.i2c_address, reg_addr, val)
def send(self, command):
if self.read_register(self.TXLVL) >= len(command):
self.i2c.write_i2c_block_data(self.i2c_address, self.THR, command)
def receive(self):
n = 9
buf = []
start = time.clock()
while n > 0:
rx_level = self.read_register(self.RXLVL)
if rx_level > n:
rx_level = n
buf.extend(self.i2c.read_i2c_block_data(self.i2c_address, self.RHR, rx_level))
n = n - rx_level
if time.clock() - start > 0.2:
break
return buf
The only error I'm getting in the log now is:
2017-08-11 22:55:27,876 - mycodo.sensor_6 - ERROR - StopIteration raised. Possibly could not read sensor. Ensure it's connected properly and detected.
Everything else is working fine. I'm still getting data on the Live Measurements page.
Try changing line 154 (and after) so it looks like this:
elif self.interface == 'I2C':
for _ in range(2):
self._co2 = self.get_measurement()
if self._co2:
return # success - no errors
time.sleep(1)
I'm still getting the same error in the log, but not as often and it took a few minutes for it to show up.
2017-08-11 23:31:07,325 - mycodo.sensor_6 - ERROR - StopIteration raised. Possibly could not read sensor. Ensure it's connected properly and detected.
Strange. I suspect it may be the time.sleep()s being too short in read_register() (line 201) and write_register() (line 205). I also think if the test script had try/except tests it would also error the same as this module.
That's all for me, I'm going to sleep. Feel free to experiment to see if you can reduce or get rid of the errors that occasionally occur.
It seems like you were right about the time.sleeps()s being too short.
I changed time.sleep(0.001) in lines 202 and 206 to time.sleep(0.01) and haven't had any errors.
Thanks for the help.
I did a double-take just now because I've been scouring the code looking for what could be causing the intermittent errors, made a few commits, and was going to ask if you could test the code, then I looked back at your last comment and saw you edited it to say you found the issue.
Could you please test my latest mh_z16.py code (in I2C mode) to see if it runs without errors? I changed the timers to what you said works and removed the 3 tries to acquire a measurement. If this works well, I'll add it to the next release. Thanks.
I've been running the new code for 30 minutes with zero errors.
I'm comparing it to the K30 sensor and the data is accurate.
I'll let you know if any errors pop up.
Should I reopen this until you get it into the next release, or leave it closed?
Thanks again
Thanks for testing. Let's leave it closed unless you come across any issue. I'd let it run a few days before confirming everything is working, for good measure.
How's the sensor module been working?
No issues so far.
I'm going to switch over to UART when I get home and let that run for a few days.
The MH-Z16 has been running great through UART.
No errors at all.
Great! Thanks for testing. If there are any other sensors you have, now or in the future, let me know and I'll try to support them.
Sounds great, I appreciate it.
I just ordered a 433mhz transceiver and a PH module.
I'll try to find some code and I'll let you know when I get them.
Hello Magnum-PI, i just get stuck with the MH-Z16 CO2 sensor while measuring via I2C pins.
I run the sandBox electronics code for python but it caused an error sensor not found, so I used i2cdetect -y 1 to determine if mh-z16 use I2C but it's not connected. I have another mh-z16 same response with this.
Now I want to use uart, but I can't find your code in Mycode/mycode, sensor folder is not available please guide me where can i find it.
As this matter is urgent, I would appreciate a reply as soon as possible. thanks
The MH-Z16 Mycodo input supports both I2C and UART. Type "MH-Z16" in the Input search box on the Mycodo Input page and you should see both I2C and UART inputs.
okay, so i got your code but its very confusing if i download the repository https://github.com/kizniche/Mycodo.git and run the mh_z16.py code under Mycode/mycode/inputs/mh_z16.py it says ModuleNotFoundError: No module named 'mycodo' so i cut the mh_z16.py code and paste it into same directory where abstract_base_controller.py is and remove mycode directory refrence but this abstract_base_controller.py have other file where it import classes so till know i am unable to run your code ... is there any simple methode to run this code?
Hi Kyle,
Can you add support for the MH-Z16 CO2 sensor from Sandbox Electronics?
I've attached a Python script from Sandbox.
Also included are links to their GitHub repo and the sensor page on their site.
I tested the script and was able to view the sensor data.
https://github.com/SandboxElectronics/NDIR
http://sandboxelectronics.com/?product=mh-z16-ndir-co2-sensor-with-i2cuart-5v3-3v-interface-for-arduinoraspeberry-pi
NDIR_RasPi_Python.zip