Share My Creation Air Quality IKEA Vindriktning

Purpose
To measure & monitor the room Air Quality Particulate Matter 2.5µm concentration with reasonable accuracy.

a1.png

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.
a2.png

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&param=udevice&idx=#IDX#&nvalue=#NVALUE#&svalue=#SVALUE#
'Option 2 = Domoticz URL to trigger custom event: http://domoticz-ip:domoticz-port/json.htm?type=command&param=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&param=udevice&idx=97&nvalue=0&svalue=28
    Private URL_DOMOTICZ_97 As String         = "http://domoticz-ip:domoticz-port/json.htm?type=command&param=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&param=customevent&event=MyEvent&data=MyData
    Private URL_DOMOTICZ_CUSTOM_EVENT As String = "http://domoticz-ip:domoticz-port/json.htm?type=command&param=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:
Top