diff --git a/README.md b/README.md
index c6989cc..cf489d0 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,127 @@
-# mh-z16
-Lightweight python library for interactions with MH-Z16 Intelligent Infrared Gas Module CO2
+# mh-zxx
+Lightweight python library for interactions with MH-Z16, MH-Z19 and similar Intelligent Infrared Gas Module CO2
+
+# MH-ZXX CO2 Sensor API
+
+* [CO2Sensor](#mh_zxx.base.CO2Sensor)
+ * [\_\_init\_\_](#mh_zxx.base.CO2Sensor.__init__)
+ * [read\_co2](#mh_zxx.base.CO2Sensor.read_co2)
+ * [read\_temperature](#mh_zxx.base.CO2Sensor.read_temperature)
+ * [read\_status](#mh_zxx.base.CO2Sensor.read_status)
+ * [read](#mh_zxx.base.CO2Sensor.read)
+ * [zero\_calibration](#mh_zxx.base.CO2Sensor.zero_calibration)
+ * [span\_calibration](#mh_zxx.base.CO2Sensor.span_calibration)
+ * [set\_auto\_calibration](#mh_zxx.base.CO2Sensor.set_auto_calibration)
+ * [close](#mh_zxx.base.CO2Sensor.close)
+
+
+
+## CO2Sensor
+
+```python
+class CO2Sensor()
+```
+
+MH-Z16 and MH-Z19 CO2 sensor.
+
+
+
+#### \_\_init\_\_
+
+```python
+def __init__(port: str, baudrate: int = 9600, timeout: float = 0.2)
+```
+
+Setup CO2 sensor
+
+**Arguments**:
+
+- `port`: The serial port name, for example `/dev/ttyUSB0` (Linux),
+`/dev/tty.usbserial` (OS X) or `COM4` (Windows).
+- `baudrate`: Baudrate to use.
+- `timeout`: Read timeout after each command.
+
+
+
+#### read\_co2
+
+```python
+def read_co2() -> int
+```
+
+Read CO2 concentration.
+
+
+
+#### read\_temperature
+
+```python
+def read_temperature() -> float
+```
+
+Read temperature.
+
+
+
+#### read\_status
+
+```python
+def read_status() -> int
+```
+
+Read status.
+
+
+
+#### read
+
+```python
+def read() -> Tuple[int, float, int, int]
+```
+
+Read all values.
+
+**Returns**:
+
+CO2 concentration, temperature, status, co2 auto calibration point
+
+
+
+#### zero\_calibration
+
+```python
+def zero_calibration()
+```
+
+Zero calibration.
+
+
+
+#### span\_calibration
+
+```python
+def span_calibration(span: int)
+```
+
+Span calibration.
+
+
+
+#### set\_auto\_calibration
+
+```python
+def set_auto_calibration(state: bool)
+```
+
+Set auto calibration point.
+
+
+
+#### close
+
+```python
+def close()
+```
+
+Close serial port.
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..dfec6be
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,32 @@
+"""Install packages as defined in this file into the Python environment."""
+from setuptools import setup, find_namespace_packages
+
+
+# The version of this tool is based on the following steps:
+# https://packaging.python.org/guides/single-sourcing-package-version/
+VERSION = {}
+
+with open("./src/mh_zxx/version.py") as fp:
+ # pylint: disable=W0122
+ exec(fp.read(), VERSION)
+
+setup(
+ name="mh_zxx",
+ author="Filip Dobrovolny",
+ author_email="dobrovolny.filip@gmail.com",
+ url="https://github.com/fdobrovolny/mh-zxx",
+ description="Python library for interaction with MH-Z16, MH-Z19 and similar.",
+ version=VERSION.get("__version__", "0.0.0"),
+ package_dir={"": "src"},
+ packages=find_namespace_packages(where="src", exclude=["tests"]),
+ install_requires=[
+ "setuptools>=45.0",
+ "pyserial>=3.5",
+ ],
+ classifiers=[
+ "Development Status :: 1 - Planning",
+ "Programming Language :: Python :: 3.0",
+ "Topic :: Utilities",
+ "Topic :: System :: Hardware",
+ ],
+)
diff --git a/src/mh_zxx/__init__.py b/src/mh_zxx/__init__.py
new file mode 100644
index 0000000..c357ba2
--- /dev/null
+++ b/src/mh_zxx/__init__.py
@@ -0,0 +1,5 @@
+from mh_z16.base import CO2Sensor
+from mh_z16.version import __version__
+
+
+__all__ = ["CO2Sensor", "__version__"]
diff --git a/src/mh_zxx/base.py b/src/mh_zxx/base.py
new file mode 100644
index 0000000..eeae9d7
--- /dev/null
+++ b/src/mh_zxx/base.py
@@ -0,0 +1,124 @@
+import logging
+from typing import List, Tuple
+
+from serial import Serial
+
+
+logger = logging.getLogger(__name__)
+
+
+class CO2Sensor:
+ """
+ MH-Z16 and MH-Z19 CO2 sensor.
+ """
+
+ serial: Serial
+
+ def __init__(
+ self,
+ port: str,
+ baudrate: int = 9600,
+ timeout: float = 0.2,
+ ):
+ """
+ Setup CO2 sensor
+
+ :param port: The serial port name, for example `/dev/ttyUSB0` (Linux),
+ `/dev/tty.usbserial` (OS X) or `COM4` (Windows).
+ :param baudrate: Baudrate to use.
+ :param timeout: Read timeout after each command.
+ """
+ self.serial = Serial(port=port, baudrate=baudrate, timeout=timeout)
+ self.serial.open()
+
+ READ_CO2 = [0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00]
+ ZERO_CALIBRATION = [0xff, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00]
+ SPAN_CALIBRATION = [0xff, 0x01, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00]
+ AUTO_CALIBRATION = [0xff, 0x01, 0x79, 0xa0, 0x00, 0x00, 0x00, 0x00]
+ AUTO_CALIBRATION_OFF = [0xff, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00]
+
+ @staticmethod
+ def _checksum(data: bytes | List[int]) -> int:
+ """
+ Calculate _checksum of the command.
+ """
+ return 0xff - (sum(data) % 256) + 1
+
+ def _write_command(self, command: List[int]):
+ """
+ Write command to the sensor.
+ """
+ command.append(self._checksum(command))
+ self.serial.write(command)
+
+ def _read_response(self) -> bytes:
+ """
+ Read response from the sensor.
+ """
+ return self.serial.read(9)
+
+ def read_co2(self) -> int:
+ """
+ Read CO2 concentration.
+ """
+ return self.read()[0]
+
+ def read_temperature(self) -> float:
+ """
+ Read temperature.
+ """
+ return self.read()[1]
+
+ def read_status(self) -> int:
+ """
+ Read status.
+ """
+ return self.read()[2]
+
+ def read(self) -> Tuple[int, float, int, int]:
+ """
+ Read all values.
+
+ :note: last value is auto calibration point, see
+ :link: https://revspace.nl/MH-Z19#Command_set
+
+ :return: CO2 concentration, temperature, status, co2 auto calibration point
+ """
+ self._write_command(self.READ_CO2)
+ response = self._read_response()
+ if response[0] != 0xff or response[1] != 0x86:
+ raise ValueError("Invalid response")
+ elif response[8] != self._checksum(response[:8]):
+ raise ValueError("Invalid checksum")
+
+ return response[2] * 256 + response[3], response[4] - 40, response[5], response[6]
+
+ def zero_calibration(self):
+ """
+ Zero calibration.
+ """
+ self._write_command(self.ZERO_CALIBRATION)
+
+ def span_calibration(self, span: int):
+ """
+ Span calibration.
+ """
+ command = self.SPAN_CALIBRATION.copy()
+ command[3] = span // 256
+ command[4] = span % 256
+ self._write_command(command)
+
+ def set_auto_calibration(self, state: bool):
+ """
+ Set auto calibration point.
+ """
+ if state:
+ self._write_command(self.AUTO_CALIBRATION)
+ else:
+ self._write_command(self.AUTO_CALIBRATION_OFF)
+
+ def close(self):
+ """
+ Close serial port.
+ """
+ self.serial.close()
diff --git a/src/mh_zxx/version.py b/src/mh_zxx/version.py
new file mode 100644
index 0000000..f102a9c
--- /dev/null
+++ b/src/mh_zxx/version.py
@@ -0,0 +1 @@
+__version__ = "0.0.1"