[EDGE] Awair Air Quality Sensor using Local API: Connection Closed issue (link to driver in post #39)

yea air care thats what its called

Addendum>
correction: This problem is not related to http chunked, but related with sending request into two separate chunks.

Original Post>

HTTP library which is built in the ST Edge Driver has error parsing http chunked response data.
Awair Local API responds using http chunked, so that’s why many people failed to build the Awair Edge driver.

@nayelyz, Below is the raw TCP socket data from Awair device. You might fix the error in the bulit in http libaray using this data.

think@utility:~$ curl http://192.168.254.114/air-data/latest --trace out.txt
{"timestamp":"2023-01-20T02:27:31.749Z","score":76,"dew_point":13.15,"temp":26.11,"humid":44.70,"abs_humid":10.93,"co2":1683,"co2_est":545,"co2_est_baseline":35210,"voc":682,"voc_baseline":37976,"voc_h2_raw":25,"voc_ethanol_raw":36,"pm25":5,"pm10_est":6}
think@utility:~$ cat out.txt
== Info:   Trying 192.168.254.114:80...
== Info: Connected to 192.168.254.114 (192.168.254.114) port 80 (#0)
=> Send header, 94 bytes (0x5e)
0000: 47 45 54 20 2f 61 69 72 2d 64 61 74 61 2f 6c 61 GET /air-data/la
0010: 74 65 73 74 20 48 54 54 50 2f 31 2e 31 0d 0a 48 test HTTP/1.1..H
0020: 6f 73 74 3a 20 31 39 32 2e 31 36 38 2e 32 35 34 ost: 192.168.254
0030: 2e 31 31 34 0d 0a 55 73 65 72 2d 41 67 65 6e 74 .114..User-Agent
0040: 3a 20 63 75 72 6c 2f 37 2e 37 34 2e 30 0d 0a 41 : curl/7.74.0..A
0050: 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a 0d 0a       ccept: */*....
== Info: Mark bundle as not supporting multiuse
<= Recv header, 17 bytes (0x11)
0000: 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d HTTP/1.1 200 OK.
0010: 0a                                              .
<= Recv header, 32 bytes (0x20)
0000: 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 61 70 Content-Type: ap
0010: 70 6c 69 63 61 74 69 6f 6e 2f 6a 73 6f 6e 0d 0a plication/json..
<= Recv header, 79 bytes (0x4f)
0000: 43 61 63 68 65 2d 43 6f 6e 74 72 6f 6c 3a 20 6e Cache-Control: n
0010: 6f 2d 73 74 6f 72 65 2c 20 6e 6f 2d 63 61 63 68 o-store, no-cach
0020: 65 2c 20 6d 75 73 74 2d 72 65 76 61 6c 69 64 61 e, must-revalida
0030: 74 65 2c 20 70 6f 73 74 2d 63 68 65 63 6b 3d 30 te, post-check=0
0040: 2c 20 70 72 65 2d 63 68 65 63 6b 3d 30 0d 0a    , pre-check=0..
<= Recv header, 18 bytes (0x12)
0000: 50 72 61 67 6d 61 3a 20 6e 6f 2d 63 61 63 68 65 Pragma: no-cache
0010: 0d 0a                                           ..
<= Recv header, 24 bytes (0x18)
0000: 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 4b 65 65 70 Connection: Keep
0010: 2d 41 6c 69 76 65 0d 0a                         -Alive..
<= Recv header, 32 bytes (0x20)
0000: 41 63 63 65 73 73 2d 43 6f 6e 74 72 6f 6c 2d 41 Access-Control-A
0010: 6c 6c 6f 77 2d 4f 72 69 67 69 6e 3a 20 2a 0d 0a llow-Origin: *..
<= Recv header, 28 bytes (0x1c)
0000: 54 72 61 6e 73 66 65 72 2d 45 6e 63 6f 64 69 6e Transfer-Encodin
0010: 67 3a 20 63 68 75 6e 6b 65 64 0d 0a             g: chunked..
<= Recv header, 2 bytes (0x2)
0000: 0d 0a                                           ..
<= Recv data, 260 bytes (0x104)
0000: 66 65 0d 0a 7b 22 74 69 6d 65 73 74 61 6d 70 22 fe..{"timestamp"
0010: 3a 22 32 30 32 33 2d 30 31 2d 32 30 54 30 32 3a :"2023-01-20T02:
0020: 32 37 3a 33 31 2e 37 34 39 5a 22 2c 22 73 63 6f 27:31.749Z","sco
0030: 72 65 22 3a 37 36 2c 22 64 65 77 5f 70 6f 69 6e re":76,"dew_poin
0040: 74 22 3a 31 33 2e 31 35 2c 22 74 65 6d 70 22 3a t":13.15,"temp":
0050: 32 36 2e 31 31 2c 22 68 75 6d 69 64 22 3a 34 34 26.11,"humid":44
0060: 2e 37 30 2c 22 61 62 73 5f 68 75 6d 69 64 22 3a .70,"abs_humid":
0070: 31 30 2e 39 33 2c 22 63 6f 32 22 3a 31 36 38 33 10.93,"co2":1683
0080: 2c 22 63 6f 32 5f 65 73 74 22 3a 35 34 35 2c 22 ,"co2_est":545,"
0090: 63 6f 32 5f 65 73 74 5f 62 61 73 65 6c 69 6e 65 co2_est_baseline
00a0: 22 3a 33 35 32 31 30 2c 22 76 6f 63 22 3a 36 38 ":35210,"voc":68
00b0: 32 2c 22 76 6f 63 5f 62 61 73 65 6c 69 6e 65 22 2,"voc_baseline"
00c0: 3a 33 37 39 37 36 2c 22 76 6f 63 5f 68 32 5f 72 :37976,"voc_h2_r
00d0: 61 77 22 3a 32 35 2c 22 76 6f 63 5f 65 74 68 61 aw":25,"voc_etha
00e0: 6e 6f 6c 5f 72 61 77 22 3a 33 36 2c 22 70 6d 32 nol_raw":36,"pm2
00f0: 35 22 3a 35 2c 22 70 6d 31 30 5f 65 73 74 22 3a 5":5,"pm10_est":
0100: 36 7d 0d 0a                                     6}..
<= Recv data, 5 bytes (0x5)
0000: 30 0d 0a 0d 0a                                  0....
== Info: Connection #0 to host 192.168.254.114 left intact
think@utility:~$

@felixowns : The driver is originally made by @jido1517. Because of the error in built in http library, his original Awair Edge driver used Edge Bridge made by @TAustin.
toddaustin07/edgebridge: Forwarding Bridge Server for SmartThings Edge drivers (github.com)

I personally contacted him to get the source code of the Awair edge driver using Edge Bridge, and then I’ve modified to use raw TCP socket method instead of the built in http library.

I can open my part of the source code : the tcp socket part.
I hope SmartThings fix the error of the built in library, so that whole source code could be much more concise.

local socket = require "cosock.socket"
local log = require "log"


local function validate_address(addr)
  local ip, port = addr:match('http://([^:]+):?(.*)$')
  port = (port == '') and 80 or tonumber(port)
  if ip == nil or type(port) ~= 'number' then
    return nil
  end
  
  local chunks = {ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")}
  if #chunks ~= 4 then return nil end
  for _, v in pairs(chunks) do
    if tonumber(v) > 255 then return nil end
  end
  if (port < 1) or (port > 65535) then
    return nil
  end
  
  return ip, port
end

local http_get = socket.protect(function(url)
  local host, path = string.match(url, "http://([^/]+)(/.*)")
  socket.try(host, 'invalid url: enter valid uri in the config')
  local ip, port = validate_address('http://'..host)
  socket.try(ip, 'invalid url: enter valid uri in the config')
  
  local c = socket.try(socket.connect(ip, port))
  --log.info("connected")
  
  local ret, chunked, length
  local try = socket.newtry(function() c:close() end)
  
  try(c:send("GET "..path.." HTTP/1.1\r\nHost: "..host.."\r\nConnection: close\r\n\r\n"))
  --log.info("sent headers")
  
  ret = try(c:receive())
  if ret ~= "HTTP/1.1 200 OK" then
    log.warn("[http] http response: "..ret)
  end
  --log.debug("< "..ret)
  
  while ret ~= '' do
    ret = try(c:receive())
    --log.debug("< "..ret)
    ret = string.lower(ret)
    if ret == 'transfer-encoding: chunked' then
      chunked = true
    else
      local _, _, value = ret:find("^content%-length:%s*(.*)")
      if value ~= nil then 
        length = tonumber(value)
      end
    end
  end
  --log.info("response header received")
  
  if chunked ~= true and length == nil then
    try(nil, 'failed to receive content length from header')
  end
  
  local body = ''
  while true do
    if chunked then
      ret = try(c:receive('*l'))
      length = tonumber(ret, 16)
      --log.info("chunk size received "..length)
      if length == 0 then break end
    end
    
    ret = try(c:receive(length))
    --log.debug("< "..ret)
    body = body..ret
    
    if chunked then
      try(c:receive(2)) -- skip \r\n after every chunk
    else
      break
    end
  end
  
  c:close()
  return body
end)


-- below is how you use this http_get function.
local air_json, err = http_get(device.preferences.url..'/air-data/latest')

2 Likes

Thanks so much for posting this driver.
I followed the video and installation was straight forward!

I am curious what you mean here, is this cosock.socket.http or Luncheon?

If it is cosock.socket.http, can you provide the invocation that is causing the problem?

1 Like

Yes. I get same error as the original post when I use cosock http

Below is the code, which fails to parse the Awair’s http response

local cosock = require "cosock"
local socket = require "cosock.socket"
local http = cosock.asyncify "socket.http"
local ltn12 = require "ltn12"
local json = require "st.json"
local utils = require "st.utils"
local log = require "log"

local function http_get(url)
	local host, path = string.match(url, "http://([^/]+)(/.*)")
	local res_body = {}
	local _, code = http.request({
			method="GET",
			url=url,
			sink=ltn12.sink.table(res_body),
			headers={
			["HOST"] = host,
			["Connection"] = "close"
		},
	})
	log.debug('[http response code] '..code)
	log.debug('[http response body] '..utils.stringify_table(res_body))
	return res_body, code
end

-- below is how you use this http_get function.
local air_json, err = http_get(device.preferences.url..'/air-data/latest')

and below is the hub log (with hub version 45.11)

2023-01-21T03:51:31.745627422+09:00 DEBUG Awair http  [http response code] [string "socket"]:1553: closed
2023-01-21T03:51:31.756460737+09:00 DEBUG Awair http  [http response body] {}

I’ve already posted raw TCP data from the Awair device, which fails to be parsed with the built in cosock http libaray.

Looking at the luasocket’s API for handling chunk encoding, it seems like it will call receive a couple of times for each chunk, returning the error if one is encountered before the full chunk has been read. Since you are providing the header ["Connection"] = "close", the peer is closing the connection before this series of receives has completed. Does the above code work if you remove that header?

Hi, @iquix

The team checked more details about this issue and they noticed it’s due to a bug in luasocket and it’s not specific to the implementation of SmartThings, but, they can work on a fix because they use a fork of it.

Thank you for bringing this to our attention!

Yes, it seems like http.lua part of the luasocket has error. Thank you for your help.

I removed ["Connection"] = "close" header, but it generates same error.

  1. As you can see from my raw level socket code, it also sends connection close header, but it works fine.

  2. I also checked with curl with Connection: close header, and it seems Awair device ignores Connection: close header, and leaves connection with keep-alive header response, which means Awair never closes the socket first.

think@utility:~$ curl -H "Connection: close" http://192.168.254.114/air-data/latest --trace out.txt
{"timestamp":"2023-01-20T23:29:33.472Z","score":74,"dew_point":13.76,"temp":25.08,"humid":49.47,"abs_humid":11.41,"co2":2346,"co2_est":765,"co2_est_baseline":35106,"voc":786,"voc_baseline":37960,"voc_h2_raw":24,"voc_ethanol_raw":35,"pm25":2,"pm10_est":3}
think@utility:~$ cat out.txt
== Info:   Trying 192.168.254.114:80...
== Info: Connected to 192.168.254.114 (192.168.254.114) port 80 (#0)
=> Send header, 113 bytes (0x71)
0000: 47 45 54 20 2f 61 69 72 2d 64 61 74 61 2f 6c 61 GET /air-data/la
0010: 74 65 73 74 20 48 54 54 50 2f 31 2e 31 0d 0a 48 test HTTP/1.1..H
0020: 6f 73 74 3a 20 31 39 32 2e 31 36 38 2e 32 35 34 ost: 192.168.254
0030: 2e 31 31 34 0d 0a 55 73 65 72 2d 41 67 65 6e 74 .114..User-Agent
0040: 3a 20 63 75 72 6c 2f 37 2e 37 34 2e 30 0d 0a 41 : curl/7.74.0..A
0050: 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a 43 6f 6e 6e ccept: */*..Conn
0060: 65 63 74 69 6f 6e 3a 20 63 6c 6f 73 65 0d 0a 0d ection: close...
0070: 0a                                              .
== Info: Mark bundle as not supporting multiuse
<= Recv header, 17 bytes (0x11)
0000: 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d HTTP/1.1 200 OK.
0010: 0a                                              .
<= Recv header, 32 bytes (0x20)
0000: 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 61 70 Content-Type: ap
0010: 70 6c 69 63 61 74 69 6f 6e 2f 6a 73 6f 6e 0d 0a plication/json..
<= Recv header, 79 bytes (0x4f)
0000: 43 61 63 68 65 2d 43 6f 6e 74 72 6f 6c 3a 20 6e Cache-Control: n
0010: 6f 2d 73 74 6f 72 65 2c 20 6e 6f 2d 63 61 63 68 o-store, no-cach
0020: 65 2c 20 6d 75 73 74 2d 72 65 76 61 6c 69 64 61 e, must-revalida
0030: 74 65 2c 20 70 6f 73 74 2d 63 68 65 63 6b 3d 30 te, post-check=0
0040: 2c 20 70 72 65 2d 63 68 65 63 6b 3d 30 0d 0a    , pre-check=0..
<= Recv header, 18 bytes (0x12)
0000: 50 72 61 67 6d 61 3a 20 6e 6f 2d 63 61 63 68 65 Pragma: no-cache
0010: 0d 0a                                           ..
<= Recv header, 24 bytes (0x18)
0000: 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 4b 65 65 70 Connection: Keep
0010: 2d 41 6c 69 76 65 0d 0a                         -Alive..
<= Recv header, 32 bytes (0x20)
0000: 41 63 63 65 73 73 2d 43 6f 6e 74 72 6f 6c 2d 41 Access-Control-A
0010: 6c 6c 6f 77 2d 4f 72 69 67 69 6e 3a 20 2a 0d 0a llow-Origin: *..
<= Recv header, 28 bytes (0x1c)
0000: 54 72 61 6e 73 66 65 72 2d 45 6e 63 6f 64 69 6e Transfer-Encodin
0010: 67 3a 20 63 68 75 6e 6b 65 64 0d 0a             g: chunked..
<= Recv header, 2 bytes (0x2)
0000: 0d 0a                                           ..
<= Recv data, 260 bytes (0x104)
0000: 66 65 0d 0a 7b 22 74 69 6d 65 73 74 61 6d 70 22 fe..{"timestamp"
0010: 3a 22 32 30 32 33 2d 30 31 2d 32 30 54 32 33 3a :"2023-01-20T23:
0020: 32 39 3a 33 33 2e 34 37 32 5a 22 2c 22 73 63 6f 29:33.472Z","sco
0030: 72 65 22 3a 37 34 2c 22 64 65 77 5f 70 6f 69 6e re":74,"dew_poin
0040: 74 22 3a 31 33 2e 37 36 2c 22 74 65 6d 70 22 3a t":13.76,"temp":
0050: 32 35 2e 30 38 2c 22 68 75 6d 69 64 22 3a 34 39 25.08,"humid":49
0060: 2e 34 37 2c 22 61 62 73 5f 68 75 6d 69 64 22 3a .47,"abs_humid":
0070: 31 31 2e 34 31 2c 22 63 6f 32 22 3a 32 33 34 36 11.41,"co2":2346
0080: 2c 22 63 6f 32 5f 65 73 74 22 3a 37 36 35 2c 22 ,"co2_est":765,"
0090: 63 6f 32 5f 65 73 74 5f 62 61 73 65 6c 69 6e 65 co2_est_baseline
00a0: 22 3a 33 35 31 30 36 2c 22 76 6f 63 22 3a 37 38 ":35106,"voc":78
00b0: 36 2c 22 76 6f 63 5f 62 61 73 65 6c 69 6e 65 22 6,"voc_baseline"
00c0: 3a 33 37 39 36 30 2c 22 76 6f 63 5f 68 32 5f 72 :37960,"voc_h2_r
00d0: 61 77 22 3a 32 34 2c 22 76 6f 63 5f 65 74 68 61 aw":24,"voc_etha
00e0: 6e 6f 6c 5f 72 61 77 22 3a 33 35 2c 22 70 6d 32 nol_raw":35,"pm2
00f0: 35 22 3a 32 2c 22 70 6d 31 30 5f 65 73 74 22 3a 5":2,"pm10_est":
0100: 33 7d 0d 0a                                     3}..
<= Recv data, 5 bytes (0x5)
0000: 30 0d 0a 0d 0a                                  0....
== Info: Connection #0 to host 192.168.254.114 left intact

@robert.masen, @nayelyz

I did some debug, by editing built in luasocket http.lua source code.
It turned out that it was not related to http chunked. Sorry for giving you wrong information

It was related to the problem when request and header were sent into two separate chunks. It seems that Awair can’t handle it in this way.

When I modified the code by merging those two separate calls into one function, it worked fine.

like below, to send reqestline and headers with single self.c:send() call.

function metat.__index:sendrequestline_and_headers(method, uri, tosend)
    local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri)

    local canonic = headers.canonic
    local h = "\r\n"
    for f, v in base.pairs(tosend) do
        h = (canonic[f] or f) .. ": " .. v .. "\r\n" .. h
    end
    self.try(self.c:send(reqline..h))
    return 1
end

and L357-L358 into below

h:sendrequestline_and_headers(nreqt.method, nreqt.uri, nreqt.headers)

Hi

anyone here would be able to modify this edge driver so it can supports AWAIR OMNI

It basically works fine on this edge driver but some parameters are missing like:
LUX
NOISE LEVEL
BATERRY LEVEL
POWER STATUS

{“timestamp”:“2023-01-26T22:37:31.541Z”,“score”:94,“temp”:22.21,“humid”:40.30,“co2”:637,“voc”:187,“pm25”:2,“lux”:8.3,“spl_a”:53.6}

{“device_uuid”:“awair-omni_15752”,“wifi_mac”:“70:88:6B:12:3B:7F”,“ip”:“192.168.1.187”,“netmask”:“255.255.255.0”,“gateway”:“192.168.1.1”,“fw_version”:“1.6.1”,“timezone”:“Europe/London”,“display”:“temp”,“led”:{“mode”:“auto”,“brightness”:25},“power-status”:{“battery”:100,“plugged”:true}}

Displaying power status requires a lot of modifications.

I’ve modified my driver and added ‘Awair Omni’ in the device type settings to show only these data.

You need to update the edge driver by uninstalling and reinstalling the edge driver.

Actually it worked without uninstalling or readding anything, new option appeared in the driver and I changed to OMNI, It works great much appreciated my friend

Actually it worked without uninstalling or readding anything, new option appeared in the driver and I changed to OMNI, It works great much appreciated my friend

I believe driver stopped working, its not refreshing any longer, anyone experiencing it ?

Both of my Awair Elements are still updating. The device type in my driver settings is set to R2.

back to normal now, seems like there was some glitch ( it wasn’t updating temperature and humidity only ) other parameters were being updated. i didn’t do anything. Anyway its all good for now.

Sorry, I use a screen reader, and I can’t follow all the code in this thread. Is there a link to a channel that has a working edge driver for Awair models?

Thanks!

Here’s a link to the post in this thread that contains the link to the channel.

And here’s a direct link to the channel if my link to a post of a link was too meta.

1 Like

@iquix / @TylerDurden Is this driver open source?

I saw references to code modifications, but didn’t see any links to the repo. I am shopping around for air quality sensors and this driver piqued my interest. If it is open source, I am willing to contribute/fork the repo and build it out.