From f6cb0258e7e0027073a3cbcf76f23a2ad64e5bc6 Mon Sep 17 00:00:00 2001 From: Steffen Klee Date: Wed, 10 Apr 2024 23:52:37 +0200 Subject: [PATCH 1/3] feat: add MH-Z19 sensor module This adds support for NDIR CO2 sensor MH-Z19. The sensor module supports reading CO2 ppm value and configuring the ppm range over the UART/serial interface. --- mqtt_io/modules/sensor/mhz19.py | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 mqtt_io/modules/sensor/mhz19.py diff --git a/mqtt_io/modules/sensor/mhz19.py b/mqtt_io/modules/sensor/mhz19.py new file mode 100644 index 00000000..1ce73831 --- /dev/null +++ b/mqtt_io/modules/sensor/mhz19.py @@ -0,0 +1,61 @@ +""" +MH-Z19 NDIR CO2 sensor +""" + +from mqtt_io.modules.sensor import GenericSensor +from mqtt_io.types import CerberusSchemaType, ConfigType, SensorValueType + +REQUIREMENTS = ("pyserial",) +CONFIG_SCHEMA: CerberusSchemaType = { + "device": dict(type="string", required=True, empty=False), + "range": dict(type="integer", required=False, empty=False, default=5000, allowed=[2000, 5000, 10000]), +} + +class Sensor(GenericSensor): + """ + Implementation of Sensor class for the MH-Z19 CO2 sensor using UART/serial. + """ + + @staticmethod + def _calc_checksum(data: bytes) -> bytes: + v = sum(data[1:]) % 0x100 + v = (0xff - v + 1) % 0x100 + return v.to_bytes(1, "big") + + @staticmethod + def _check_checksum(data: bytes) -> bool: + return Sensor._calc_checksum(data[:-1]) == data[-1:] + + @staticmethod + def _add_checksum(data: bytes) -> bytes: + return data + Sensor._calc_checksum(data) + + def setup_module(self) -> None: + # pylint: disable=import-error,import-outside-toplevel + import serial # type: ignore + + self.ser = serial.Serial( + port=self.config["device"], + baudrate=9600, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + ) + + # setup detection range + cmd = Sensor._add_checksum(b"\xff\x01\x99\x00\x00\x00" + self.config["range"].to_bytes(2, "big")) + self.ser.write(cmd) + # no response + + def cleanup(self) -> None: + self.ser.close() + + def get_value(self, sens_conf: ConfigType) -> SensorValueType: + self.ser.write(Sensor._add_checksum(b"\xff\x01\x86\x00\x00\x00\x00\x00")) + resp = self.ser.read(9) + + if len(resp) == 9: + if resp[0:2] == b"\xff\x86" and Sensor._check_checksum(resp): + return int.from_bytes(resp[2:4], "big") + + return None From 2bfb30d2d08d171b77bf592f03842a162d323cfe Mon Sep 17 00:00:00 2001 From: Steffen Klee Date: Fri, 28 Jun 2024 21:47:13 +0200 Subject: [PATCH 2/3] doc: add MH-Z19 sensor example configuration --- config.example.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config.example.yml b/config.example.yml index 5ead074e..b23e37e2 100644 --- a/config.example.yml +++ b/config.example.yml @@ -34,6 +34,11 @@ sensor_modules: type: DS18S20 address: 000803702e49 + - name: mhz19 + module: mhz19 + device: "/dev/ttyS1" + range: 5000 + digital_inputs: - name: button module: raspberrypi @@ -76,3 +81,10 @@ sensor_inputs: module: ds18b22 interval: 10 digits: 2 + + - name: co2_mhz19 + module: mhz19 + interval: 30 + ha_discovery: + name: CO2 MH-Z19 + device_class: carbon_dioxide From 0f6fe5a0a135fd5ba45aca2efe3d107978a42bbd Mon Sep 17 00:00:00 2001 From: Steffen Klee Date: Thu, 4 Jul 2024 00:52:22 +0200 Subject: [PATCH 3/3] fix: linting mhz19 --- mqtt_io/modules/sensor/mhz19.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mqtt_io/modules/sensor/mhz19.py b/mqtt_io/modules/sensor/mhz19.py index 1ce73831..04f30ead 100644 --- a/mqtt_io/modules/sensor/mhz19.py +++ b/mqtt_io/modules/sensor/mhz19.py @@ -8,7 +8,8 @@ REQUIREMENTS = ("pyserial",) CONFIG_SCHEMA: CerberusSchemaType = { "device": dict(type="string", required=True, empty=False), - "range": dict(type="integer", required=False, empty=False, default=5000, allowed=[2000, 5000, 10000]), + "range": dict(type="integer", required=False, empty=False, default=5000, + allowed=[2000, 5000, 10000]), } class Sensor(GenericSensor): @@ -18,9 +19,9 @@ class Sensor(GenericSensor): @staticmethod def _calc_checksum(data: bytes) -> bytes: - v = sum(data[1:]) % 0x100 - v = (0xff - v + 1) % 0x100 - return v.to_bytes(1, "big") + value = sum(data[1:]) % 0x100 + value = (0xff - value + 1) % 0x100 + return value.to_bytes(1, "big") @staticmethod def _check_checksum(data: bytes) -> bool: @@ -43,7 +44,8 @@ def setup_module(self) -> None: ) # setup detection range - cmd = Sensor._add_checksum(b"\xff\x01\x99\x00\x00\x00" + self.config["range"].to_bytes(2, "big")) + cmd = Sensor._add_checksum(b"\xff\x01\x99\x00\x00\x00" + + self.config["range"].to_bytes(2, "big")) self.ser.write(cmd) # no response