kizniche / Mycodo

An environmental monitoring and regulation system
http://kylegabriel.com/projects/
GNU General Public License v3.0
2.96k stars 494 forks source link

Sensor Addition: MH-Z16 CO2 Sensor #281

Closed Magnum-Pl closed 7 years ago

Magnum-Pl commented 7 years ago

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

kizniche commented 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.

Magnum-Pl commented 7 years ago

I can't get the MH-Z16 to display any sensor data.

mycodo.log.zip

kizniche commented 7 years ago

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.

kizniche commented 7 years ago

wow, I had added support for the MH-Z19 sensor, not the MH-Z16. Let me duplicate and add support for that one

kizniche commented 7 years ago

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.

Magnum-Pl commented 7 years ago

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'

mycodo.log.zip

kizniche commented 7 years ago

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.

kizniche commented 7 years ago

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
Magnum-Pl commented 7 years ago

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'

mycodo.log.zip

kizniche commented 7 years ago

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 ;)

Magnum-Pl commented 7 years ago

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.

mycodo.log.zip

kizniche commented 7 years ago

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.

Magnum-Pl commented 7 years ago

Ok, I made the edit and this is what I'm getting in the log:

mycodo.log.zip

kizniche commented 7 years ago

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
Magnum-Pl commented 7 years ago

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.

mycodo.log.zip

kizniche commented 7 years ago

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)
Magnum-Pl commented 7 years ago

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.

mycodo.log 2.zip

kizniche commented 7 years ago

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.

Magnum-Pl commented 7 years ago

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.

kizniche commented 7 years ago

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.

Magnum-Pl commented 7 years ago

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

kizniche commented 7 years ago

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.

kizniche commented 7 years ago

How's the sensor module been working?

Magnum-Pl commented 7 years ago

No issues so far.

I'm going to switch over to UART when I get home and let that run for a few days.

Magnum-Pl commented 7 years ago

The MH-Z16 has been running great through UART.

No errors at all.

kizniche commented 7 years ago

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.

Magnum-Pl commented 7 years ago

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.

samannazz commented 2 years ago

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

kizniche commented 2 years ago

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.

samannazz commented 2 years ago

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?