Purpose
To measure & monitor the room Air Quality Particulate Matter 2.5µm concentration with reasonable accuracy.
Prototype
Solution
The solution reads the Air Quality Particulate Matter 2.5µm (PM25) concentration from an IKEA Vindriktning Air Quality sensor (using a PM1006).
An ESP8266 microcontroller, WEMOS D1 Mini (D1), is connected to the PCB pins 5V, GND and REST of the IKEA Vindriktning Air Quality sensor.
The D1 reads sensor data from the serial line (via pin D7). The data is parsed into JSON format with key:value pairs id:NN, pm25:NN, level:NN.
The id is a unique device id in case several devices are used, pm25 holds the PM 2.5 concentration and the level is the air quality level.
There are three air quality levels: LOW = Alert Level 1 (GREEN) value <= 35, MEDIUM = Alert Level 2 (YELLOW) value > 35 And <= 85, RED = Alert Level 4 (RED) value > 86.
The data is regularly transmitted via WiFi to a Domoticz Home Automation System custom device.
If the air quality level is RED, then an Domoticz Alert Sensor is updated.
Communication flow and Domoticz devices.
Hardware & Software
Tested with ESP8266 modules: LOLIN (WEMOS) D1 R1, NodeMCU 0.9 (ESP-12 Module), WEMOS D1 Mini.
The WEMOS D1 Mini is used for the prototype solution.
Developed with B4R 3.9.
Wiring
Ikea VINDRIKTNING = WEMOS D1 Mini
5V = 5V; GND = GND; REST = Pin #D7 (alternate RX) via voltage divider 5v > 3v3
To measure & monitor the room Air Quality Particulate Matter 2.5µm concentration with reasonable accuracy.
Prototype
Solution
The solution reads the Air Quality Particulate Matter 2.5µm (PM25) concentration from an IKEA Vindriktning Air Quality sensor (using a PM1006).
An ESP8266 microcontroller, WEMOS D1 Mini (D1), is connected to the PCB pins 5V, GND and REST of the IKEA Vindriktning Air Quality sensor.
The D1 reads sensor data from the serial line (via pin D7). The data is parsed into JSON format with key:value pairs id:NN, pm25:NN, level:NN.
The id is a unique device id in case several devices are used, pm25 holds the PM 2.5 concentration and the level is the air quality level.
There are three air quality levels: LOW = Alert Level 1 (GREEN) value <= 35, MEDIUM = Alert Level 2 (YELLOW) value > 35 And <= 85, RED = Alert Level 4 (RED) value > 86.
The data is regularly transmitted via WiFi to a Domoticz Home Automation System custom device.
If the air quality level is RED, then an Domoticz Alert Sensor is updated.
- IKEA is a trademark of Inter-IKEA Systems B.V..
- Solution developed for personal use under the GNU GENERAL PUBLIC LICENSE Version 3.
- Use at your own risk.
Communication flow and Domoticz devices.
Hardware & Software
Tested with ESP8266 modules: LOLIN (WEMOS) D1 R1, NodeMCU 0.9 (ESP-12 Module), WEMOS D1 Mini.
The WEMOS D1 Mini is used for the prototype solution.
Developed with B4R 3.9.
Wiring
Ikea VINDRIKTNING = WEMOS D1 Mini
5V = 5V; GND = GND; REST = Pin #D7 (alternate RX) via voltage divider 5v > 3v3
B4R Code:
#Region Project Notes
'Program ikeaairqualitysensor
'Get the PM2.5 value from an IKEA Air Quality Sensor VINDRIKTNING by reading the serial data and update a Domoticz Custom Sensor.
'When starting a connection is made to the local network to be able to publish data to Domoticz via HTTP API requests.
'The serial line is read using asyncstreams and the package frame (20 bytes) received from the sensor is parsed.
'Domoticz device: IDX=97, Name=MakeLab Air Quality PM 2.5, Type=General, SubType=Custom Sensor, Data=7 ug/m3.
'Two solutions are worked out to update the Domoticz device
'Option 1 = Domoticz URL to trigger device update: http://domoticz-ip:domoticz-port/json.htm?type=command¶m=udevice&idx=#IDX#&nvalue=#NVALUE#&svalue=#SVALUE#
'Option 2 = Domoticz URL to trigger custom event: http://domoticz-ip:domoticz-port/json.htm?type=command¶m=customevent&event=ikeaairquality&data=#DATA#
'Option 2 is used. The custom event takes several actions: Update device air quality, Set alert message.
#End Region
#Region Wiring
'Tested with several ESP8266 devices
'Ikea VINDRIKTNING = ESP WeMOS D1 R1
'5V = 5V
'GND = GND
'REST = Pin #D0 RX via voltage divider 5v > 3v3
'
'Ikea VINDRIKTNING = NodeMCU 0.9
'5V = Vin
'GND = GND
'REST = Pin #RX via voltage divider 5v > 3v3
#End Region
#Region Project Attributes
#AutoFlushLogs: True
#CheckArrayBounds: True
'Required for ReplaceString used to set the URL to Domoticz HTTP API request
#StackBufferSize: 600
#End Region
'Ctrl+Click to open the C code folder: ide://run?File=%WINDIR%\System32\explorer.exe&Args=%PROJECT%\Objects\Src
Sub Process_Globals
Private VERSION As String = "IKEA Air Quality Sensor v20220220"
Private DEBUG As Boolean = True
'Communication
Private serialLine As Serial
Private serialLineBaudRate As ULong = 9600 'Import to set to 9600
Private astream As AsyncStreams 'Lib rRandomAccessFile
Private bc As ByteConverter
'WIFI
Private wifi As ESP8266WiFi 'Lib rHttpUtils2
Private WIFI_SSID As String = "SSID"
Private WIFI_PASSWORD As String = "*********"
'DOMOTICZ
'Domoticz URL HTTP API request to update the custom sensor direct via param udevice
'http://domoticz-ip:domoticz-port/json.htm?type=command¶m=udevice&idx=97&nvalue=0&svalue=28
Private URL_DOMOTICZ_97 As String = "http://domoticz-ip:domoticz-port/json.htm?type=command¶m=udevice&idx=#IDX#&nvalue=#NVALUE#&svalue=#SVALUE#"
Private IDX_DOMOTICZ_AIR_QUALITY As Int = 97 'ignore
'Domoticz URL HTTP API request to trigger the domotic custom event which updates the Domoticz custom sensor
'http://domoticz-ip:domoticz-port/json.htm?type=command¶m=customevent&event=MyEvent&data=MyData
Private URL_DOMOTICZ_CUSTOM_EVENT As String = "http://domoticz-ip:domoticz-port/json.htm?type=command¶m=customevent&event=ikeaairquality&data=#DATA#"
Private DOMOTICZ_CUSTOM_EVENT As String = "ikeaairquality"
Private SENSOR_ID As Int = 1 'Unique sensor id used for Domoticz custom event
'Sensor data
Private MESSAGE_LENGTH As Int = 20 'Sensor sends 20 byte data frame
Private currentPM25 As Int = 0 'Current PM value read from the sensor
Private previousPM25 As Int = 0 'Previous PM value which is used to calculate the difference between actual and previous sensor readings
Private offsetPM25 As Int = 2 'Offset to update the Domoticz device abs(currentPM25 - previousPM25) > Offset then update
End Sub
Private Sub AppStart
serialLine.Initialize(serialLineBaudRate) '9600
'For the WeMOS D1 Mini swap the serial line from D0 to D7
#if D1
SwapSerialMode(0)
#End If
Log(VERSION)
'Init asyncstreams required to read the sensor data via the serial line.
astream.Initialize(serialLine.Stream, "astream_NewData", "astream_Error")
'Connect to the network to be able to update the domoticz device via HTTP API request.
If wifi.Connect2(WIFI_SSID, WIFI_PASSWORD) Then
If DEBUG Then Log("[INFO] Connected to WiFi network.")
Else
Log("[ERROR] Failed to connect to WiFi network.")
Return
End If
End Sub
'Handle new data every ~20 seconds. The message buffer received from the sensor must have length of 20 bytes.
'The bytes 5 & 6 are used to calculate the PM25 concentration.
'An offset is used to update the sensor data in domoticz instead of updating with a value that has not changed.
Sub AStream_NewData (Buffer() As Byte)
If DEBUG Then Log("[INFO] AStream_NewData Received:", Buffer.Length, " = " , bc.HexFromBytes(Buffer))
'Validations
If IsValidMessageLength(Buffer) And IsValidHeader(Buffer) And IsValidCheckSum(Buffer) Then
'Calculate the PM25 value from the bytes buffer 5 & 6
currentPM25 = Buffer(5) * 256 + Buffer(6)
Log("PM25: current/previous/offset=", currentPM25, "/",previousPM25, "/",offsetPM25, " (buffer5=", Buffer(5), ", buffer6=", Buffer(6), ")")
'Update Domoticz device - Only if the difference between current and previous sample value > sample offset value
If Abs(currentPM25 - previousPM25) > offsetPM25 Then
'Update domoticz device(s) by triggering a custom event.
UpdateDomoticzCustomEvent(SENSOR_ID, currentPM25)
'NOT USED = direct device update
'Update the Domoticz custom sensor. Note that the nvalue must be 0 and the svalue is the PM25 value.
'UpdateDomoticzDevice(IDX_DOMOTICZ_AIR_QUALITY, 0, currentPM25)
End If
previousPM25 = currentPM25
End If
End Sub
Sub AStream_Error
Log("[ERROR] AStream_Error")
End Sub
#Region MESSAGECHECKS
Sub IsValidMessageLength(Buffer() As Byte) As Boolean
Dim Result As Boolean
If Buffer.Length == MESSAGE_LENGTH Then
'If DEBUG Then Log("[INFO] Message with correct length: ", Buffer.Length)
Result = True
Else
Log("[ERROR] Received message with invalid length: ", Buffer.Length)
Result = False
End If
Return Result
End Sub
'Check the message header. The first 3 bytes must be 16 11 0B
Sub IsValidHeader(Buffer() As Byte) As Boolean
Dim Result As Boolean
If (Buffer(0) == 0x16) And (Buffer(1) == 0x11) And(Buffer(2) == 0x0B) Then
'If DEBUG Then Log("[INFO] Message with correct header.")
Result = True
Else
Log("[ERROR] Received message with invalid header.")
Result = False
End If
Return Result
End Sub
'Check the checksum = All bytes must add up to 0.
Sub IsValidCheckSum(Buffer() As Byte) As Boolean
Dim Result As Boolean
Dim CheckSum As Byte = 0
'For i= 0; i < 20; i++)
For i = 0 To 19
CheckSum = CheckSum + Buffer(i)
Next
If (CheckSum == 0) Then
'If DEBUG Then Log("[INFO] Received message with correct checksum.")
Result = True
Else
Log("[ERROR] Received message with invalid checksum. Expected: 0. Actual:", CheckSum)
Result = False
End If
Return Result
End Sub
'Get the air quality level depending PM25 value.
'The IKEA sensor has 3 levels and LED indicators:
'Green LOW: 0-35=Good, Amber MEDIUM: 36-85=OK, Red (HIGH): 86-1000=NOT GOOD.
'The Domoticz Alert Sensor levels 1,2,4 are used: 0=gray, 1=green, 2=yellow, 3=orange, 4=red.
'value - PM25 value
'Returns - Level 1 = LOW (GREEN), 2 = MEDIUM (YELLOW), 4 = HIGH (RED)
Private Sub GetAirQualityLevel(value As Int) As Int
Dim Result As Int
'LOW = Alert Level 1 (GREEN)
If value <= 35 Then
Result = 1
'MEDIUM = Alert Level 2 (YELLOW)
Else if value > 35 And value <= 85 Then
Result = 2
'RED = Alert Level 4 (RED)
Else if value > 86 Then
Result = 4
End If
Return Result
End Sub
#End Region
#Region HTTP
'Update a Domoticz device by triggering a Domoticz Custom Event following the HTTP API format.
'The data must be a JSON string with escaped characters (example with PM25 value 99 and level 3): data={"id":1,"pm25":99,"level":3}.
'The HTTP response is handled by sub JobDone.
'The character " is escaped with %22.
'id - Unique id of the sensor
'value - PM2.5 value measured
Private Sub UpdateDomoticzCustomEvent(id As Int, value As String)
If DEBUG Then Log("UpdateDomoticzCustomEvent: svalue=", value)
Dim url() As Byte = URL_DOMOTICZ_CUSTOM_EVENT.GetBytes
Dim level As Int = GetAirQualityLevel(value)
Dim data As String = JoinStrings(Array As String("{%22id%22:", id, ",%22pm25%22:", value, ",%22level%22:", level, "}"))
url = ReplaceString(url, "#DATA#".GetBytes, data.GetBytes)
If DEBUG Then Log(url)
'Update the domoticz device via HTTP = init and download - see sub JobDone for response. The jobname is the custom event name.
HttpJob.Initialize(DOMOTICZ_CUSTOM_EVENT)
HttpJob.Download(url)
End Sub
'NOT USED - see Sub UpdateDomoticzCustomEvent
'Update a Domoticz device following the HTTP API format.
'The HTTP response is handled by sub JobDone.
'idx - Domoticz IDX of the custom sensor
'nvalue - For custom sensor this must be 0
'svalue - The actual PM25 value.
Private Sub UpdateDomoticzDevice(idx As String, nvalue As String, svalue As String) 'ignore
If DEBUG Then Log("UpdateDomoticzDevice: idx=",idx, ", nvalue=", nvalue, ", svalue=", svalue)
Dim url() As Byte = URL_DOMOTICZ_97.GetBytes
url = ReplaceString(url, "#IDX#".GetBytes, idx.GetBytes)
url = ReplaceString(url, "#NVALUE#".getbytes, nvalue.getbytes)
url = ReplaceString(url, "#SVALUE#".getbytes, svalue.getbytes)
If DEBUG Then Log(url)
'Update the domoticz device via HTTP = init and download - see sub JobDone for response. The jobname is the idx of the device
HttpJob.Initialize(idx)
HttpJob.Download(url)
End Sub
'Handel HTTP job done - check if the jobname has the idx of the sensor to then check the air quality red threshold = update domoticz alert sensor.
Sub JobDone (Job As JobResult)
If DEBUG Then Log("[INFO] HTTP: jobname=", Job.JobName, ", success=", Job.Success, ", status=", Job.Status)
If Not(Job.Success) Then
Log("[ERROR] HTTP: message=", Job.ErrorMessage, ", response=", Job.Response)
End If
End Sub
#End Region
#Region HELPERS
'Replaces a string - ENSURE to set the stack buffer to 500 or higher depending device used
'Thanks for providing (https://www.b4x.com/android/forum/threads/strings-and-bytes.66729/#post-435001)
Private Sub ReplaceString(Original() As Byte, SearchFor() As Byte, ReplaceWith() As Byte) As Byte()
'count number of occurrences
Dim bc2 As ByteConverter
Dim c As Int = 0
Dim i As Int
If SearchFor.Length <> ReplaceWith.Length Then
i = bc2.IndexOf(Original, SearchFor)
Do While i > -1
c = c + 1
i = bc2.IndexOf2(Original, SearchFor, i + SearchFor.Length)
Loop
End If
Dim result(Original.Length + c * (ReplaceWith.Length - SearchFor.Length)) As Byte
Dim prevIndex As Int = 0
Dim targetIndex As Int = 0
i = bc2.IndexOf(Original, SearchFor)
Do While i > -1
bc2.ArrayCopy2(Original, prevIndex, result, targetIndex, i - prevIndex)
targetIndex = targetIndex + i - prevIndex
bc2.ArrayCopy2(ReplaceWith, 0, result, targetIndex, ReplaceWith.Length)
targetIndex = targetIndex + ReplaceWith.Length
prevIndex = i + SearchFor.Length
i = bc2.IndexOf2(Original, SearchFor, prevIndex)
Loop
If prevIndex < Original.Length Then
bc2.ArrayCopy2(Original, prevIndex, result, targetIndex, Original.Length - prevIndex)
End If
Return result
End Sub
#End Region
Sub DeepSleepMode(ms As ULong) 'ignore
RunNative("deepSleep", ms * 1000)
End Sub
Sub SwapSerialMode(dummy As ULong) 'ignore
RunNative("swapSerial", dummy)
End Sub
#if C
void deepSleep(B4R::Object* o) {
ESP.deepSleep(o->toULong());
}
void swapSerial(B4R::Object* o) {
Serial.swap();
}
#end if
Domoticz Custom Event (dzVents, Lua):
local IDX_AIR_QUALITY = 97
local IDX_ALERT_MESSAGE = 26
local CUSTOM_EVENT_NAME = 'ikeaairquality'
local LOG_MARKER = 'IKEAAIRQUALITY'
return
{
on =
{
customEvents = { CUSTOM_EVENT_NAME },
},
logging =
{
level = domoticz.LOG_DEBUG,
marker = LOG_MARKER,
},
execute = function(domoticz, item)
if item.isCustomEvent then
-- Get the properties from key item.json
domoticz.log(("id=%d,pm25=%.0f,level=%d"):format(item.json.id,item.json.pm25,item.json.level))
-- Update the custom sensor
domoticz.devices(IDX_AIR_QUALITY).updateCustomSensor(item.json.pm25)
-- Update the alert sensor for the sensor with id 1 and level 4 (RED)
if item.json.id == 1 and item.json.level > 1 then
local msg = ('MakeLAB Air Quality PM2.5 %.f ug/m3.'):format(item.json.pm25)
domoticz.devices(IDX_ALERT_MESSAGE).updateAlertSensor(item.json.level, msg)
end
end
end
}
Last edited: