Unverified Commit ee6a1033 authored by Daniel Friesel's avatar Daniel Friesel
Browse files

initial commit

parents
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+1 −0
Original line number Diff line number Diff line
config.lua

README.md

0 → 100644
+76 −0
Original line number Diff line number Diff line
# ESP8266 Lua/NodeMCU module for MH-Z19 CO₂ sensor

This repository contains a Lua module (`mh-z19.lua`) as well as ESP8266/NodeMCU
MQTT gateway application example (`init.lua`) for the **MH-Z19** carbon dioxide
(CO₂) sensor.

## Dependencies

mh-z19.lua has been tested with Lua 5.1 on NodeMCU firmware 3.0.1
(Release 202112300746, integer build). It requires the following modules.

* struct

Most practical applications (such as the example in init.lua) also need the
following modules.

* gpio
* mqtt
* node
* softuart
* tmr
* uart
* wifi

## Setup

Connect the MH-Z19 sensor to your ESP8266/NodeMCU board as follows.

* MH-Z19 GND (black wire) → ESP8266/NodeMCU GND
* MH-Z19 Vin (red wire) → ESP8266/NodeMCU 5V
* MH-Z19 Rx (blue wire) → NodeMCU D1 (ESP8266 GPIO5)
* MH-Z19 Tx (green wire) → NodeMCU D2 (ESP8266 GPIO4)

If you use different pins for TXD and RXD, you need to adjust the
softuart.setup call in the examples provided in this repository to reflect
those changes. Keep in mind that some ESP8266 pins must have well-defined logic
levels at boot time and may therefore be unsuitable for MH-Z19 connection.

## Usage

Copy **mh-z19.lua** to your NodeMCU board and set it up as follows.

```lua
mhz19 = require("mh-z19")
port = softuart.setup(9600, 1, 2)
port:on("data", 9, uart_callback)

function uart_callback(data)
	if sds011.parse_frame(data) then
		-- mhz19.co2 contains the CO₂ concentration in ppm
	end
end

port:write(mhz19.query())
```

## Application Example

**init.lua** is an example application with HomeAssistant integration.
To use it, you need to create a **config.lua** file with WiFI and MQTT settings:

```lua
station_cfg.ssid = "..."
station_cfg.pwd = "..."
mqtt_host = "..."
```

Optionally, it can also publish readings to an InfluxDB.
To do so, configure URL and attribute:

```lua
influx_url = "..."
influx_attr = "..."
```

Readings will be stored as `mh_z19,[influx_attr] co2_ppm=...`

init.lua

0 → 100644
+118 −0
Original line number Diff line number Diff line
station_cfg = {}
publishing_mqtt = false
publishing_http = false

watchdog = tmr.create()
chip_id = string.format("%06X", node.chipid())
device_id = "esp8266_" .. chip_id
mqtt_prefix = "sensor/" .. device_id
mqttclient = mqtt.Client(device_id, 120)

dofile("config.lua")

print("ESP8266 " .. chip_id)

ledpin = 4
gpio.mode(ledpin, gpio.OUTPUT)
gpio.write(ledpin, 0)

mhz19 = require("mh-z19")

poll = tmr.create()

function log_restart()
	print("Network error " .. wifi.sta.status())
end

function setup_client()
	print("Connected")
	gpio.write(ledpin, 1)
	port = softuart.setup(9600, 1, 2)
	port:on("data", 9, uart_callback)
	publishing_mqtt = true
	mqttclient:publish(mqtt_prefix .. "/state", "online", 0, 1, function(client)
		publishing_mqtt = false
		query_data()
		poll:start()
	end)
end

function connect_mqtt()
	print("IP address: " .. wifi.sta.getip())
	print("Connecting to MQTT " .. mqtt_host)
	mqttclient:on("connect", hass_register)
	mqttclient:on("offline", log_restart)
	mqttclient:lwt(mqtt_prefix .. "/state", "offline", 0, 1)
	mqttclient:connect(mqtt_host)
end

function connect_wifi()
	print("WiFi MAC: " .. wifi.sta.getmac())
	print("Connecting to ESSID " .. station_cfg.ssid)
	wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, connect_mqtt)
	wifi.eventmon.register(wifi.eventmon.STA_DHCP_TIMEOUT, log_restart)
	wifi.eventmon.register(wifi.eventmon.STA_DISCONNECTED, log_restart)
	wifi.setmode(wifi.STATION)
	wifi.sta.config(station_cfg)
	wifi.sta.connect()
end

function uart_callback(data)
	if not mhz19.parse_frame(data) then
		print("Invalid MH-Z19 frame")
		return
	end

	local json_str = string.format('{"rssi_dbm":%d,"co2_ppm":"%d"}', wifi.sta.getrssi(), mhz19.co2)
	local influx_str = string.format("co2_ppm=%d", mhz19.co2)

	if not publishing_mqtt then
		watchdog:start(true)
		publishing_mqtt = true
		gpio.write(ledpin, 0)
		mqttclient:publish(mqtt_prefix .. "/data", json_str, 0, 0, function(client)
			publishing_mqtt = false
			if influx_url and influx_attr and influx_str then
				publish_influx(influx_str)
			else
				gpio.write(ledpin, 1)
				collectgarbage()
			end
		end)
	end
end

function publish_influx(payload)
	if not publishing_http then
		publishing_http = true
		http.post(influx_url, influx_header, "mh_z19" .. influx_attr .. " " .. payload, function(code, data)
			publishing_http = false
			gpio.write(ledpin, 1)
			collectgarbage()
		end)
	end
end

function query_data()
	port:write(mhz19.query())
end

function hass_register()
	local hass_device = string.format('{"connections":[["mac","%s"]],"identifiers":["%s"],"model":"ESP8266 + MH-Z19","name":"MH-Z19 %s","manufacturer":"derf"}', wifi.sta.getmac(), device_id, chip_id)
	local hass_entity_base = string.format('"device":%s,"state_topic":"%s/data","expire_after":120', hass_device, mqtt_prefix)
	local hass_co2 = string.format('{%s,"name":"CO₂","object_id":"%s_co2","unique_id":"%s_co2","device_class":"carbon_dioxide","unit_of_measurement":"ppm","value_template":"{{value_json.co2_ppm}}"}', hass_entity_base, device_id, device_id)
	local hass_rssi = string.format('{%s,"name":"RSSI","object_id":"%s_rssi","unique_id":"%s_rssi","device_class":"signal_strength","unit_of_measurement":"dBm","value_template":"{{value_json.rssi_dbm}}","entity_category":"diagnostic"}', hass_entity_base, device_id, device_id)

	mqttclient:publish("homeassistant/sensor/" .. device_id .. "/co2/config", hass_co2, 0, 1, function(client)
		mqttclient:publish("homeassistant/sensor/" .. device_id .. "/rssi/config", hass_rssi, 0, 1, function(client)
			collectgarbage()
			setup_client()
		end)
	end)
end

watchdog:register(180 * 1000, tmr.ALARM_SEMI, node.restart)
poll:register(20 * 1000, tmr.ALARM_AUTO, query_data)
watchdog:start()

connect_wifi()

mh-z19.lua

0 → 100644
+28 −0
Original line number Diff line number Diff line
local mhz19 = {}

local c_read = string.char(0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79)

mhz19.co2 = nil
mhz19.temp = nil
mhz19.tt = nil
mhz19.ss = nil
mhz19.uu = nil

function mhz19.query()
	return c_read
end

function mhz19.parse_frame(data)
	local head, cmd, co2h, co2l, temp, tt, ss, uh, ul = struct.unpack("BBBBBBBBB", data)
	if head ~= 0xff or cmd ~= 0x86 then
		return false
	end
	mhz19.co2 = co2h * 256 + co2l
	mhz19.temp = temp - 40
	mhz19.tt = tt
	mhz19.ss = ss
	mhz19.uu = uh * 256 + ul
	return true
end

return mhz19