Using your AcuRite AcuLink Internet Bridge with weewx
Gratitude:
Major thanks to http://nincehelser.com/ipwx/ for the starting point for this.Further thanks to Matthew Wall for his huge assistance with the aculink.py driver.
Preamble
This is a work in progress. There are a bunch of components that still need to be tweaked, and a bunch more that need to be made more user-friendly.You will also need to start with George Nincehelser's instructions (linked above) on setting up your ethernet bridge.
Your mileage may vary.
Technical
AcuLink connects to www.acu-link.com, submits a "POST /messages/ HTTP/1.1" command with the data resembling these lines:POST /messages/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded Host: www.acu-link.com Content-Length: Connection: close id=24C86E014F4C&sensor=01438&mt=tower&humidity=A0160&temperature=A028900000&battery=normal&rssi=3Data lines may look like one of the following:
id=24C86E014F4C&sensor=01086&mt=5N1x31&windspeed=A001660000&winddir=6&rainfall=A0000000&battery=normal&rssi=2 id=24C86E014F4C&sensor=01086&mt=5N1x38&windspeed=A003270000&humidity=A0480&temperature=A024222222&battery=normal&rssi=2 id=24C86E014F4C&sensor=01438&mt=tower&humidity=A0160&temperature=A019800000&battery=normal&rssi=2 id=24C86E014F4C&sensor=11583&mt=tower&humidity=A0440&temperature=A026100000&battery=normal&rssi=2 id=24C86E014F4C&mt=pressure&C1=442D&C2=0C77&C3=010E&C4=027D&C5=87E7&C6=178F&C7=09C4&A=07&B=19&C=06&D=09&PR=95EF&TR=8902TThe response given is:
HTTP/1.1 200 OK Server: nginx/1.0.10 + Phusion Passenger 3.0.12 (mod_rails/mod_rack) Content-Type: application/json Status: 200 Transfer-Encoding: chunked Connection: close X-Powered-By: Phusion Passenger (mod_rails/mod_rack) 3.0.12 { "success": 1, "checkversion": "126" }
To do list
change parser / driver to deal in metric (AcuLink Bridge reports in metric, so let's keep unit conversion to a minimum, eh?)integrate listener and parser into one file- write script to act as alternate listener (and responder) in cases when www.acu-link.com is unreachable
The Goods
acusniff.pl:#!/usr/bin/perl # Copyright 2014 by Kris Benson # Based on ipwx tool from George D. Nincehelser ######################### # Settings # at 690m AMSL, it seems that 1195 is about right for correction # 1195 = 119.5 mbar $barocorrection = 1195; $dropfile = "/share/weather/wxdata2"; $verbose = 1; $macaddr = '24C86E014F4C'; $sensor1 = '01086'; # 5-in-1 $sensor2 = '11583'; # front porch $sensor3 = '01438'; # upstairs ######################### $temp1 = '9999'; $humid1 = '9999'; $temp2 = '9999'; $humid2 = '9999'; $temp3 = '9999'; $humid3 = '9999'; $windspeed = '9999'; $winddir = '9999'; $rainfall = '9999'; $raintotal = '9999'; $pressure = '9999'; $nestinfo = '9999'; $battery1 = '9999'; $battery2 = '9999'; $battery3 = '9999'; $rssi1 = '9999'; $rssi2 = '9999'; $rssi3 = '9999'; open(TCPDUMP, "-|", "/usr/sbin/tcpdump -A -n -p -i eth2 -s0 -w - tcp dst port 80 2>/dev/null | /usr/bin/stdbuf -oL /usr/bin/strings -n8") or die "can't open tcpdump: $!"; while ($line = <TCPDUMP>) { if($line =~ /id=$macaddr/) { if($line =~ /sensor=$sensor1/) { # 5-in-1 # # id=24C86E014F4C&sensor=01086&mt=5N1x31&windspeed=A001660000&winddir=6&rainfall=A0000000&battery=normal&rssi=2 # id=24C86E014F4C&sensor=01086&mt=5N1x38&windspeed=A003270000&humidity=A0480&temperature=A024222222&battery=normal&rssi=2 # # two different data lines # may have # windspeed, winddir, rainfall, battery and rssi # or # windspeed, humidity, temperature, battery and rssi if($line =~ /windspeed=A(\d+)&winddir=([^&]+)&rainfall=A(\d+)&battery=([^&]+)&rssi=(\d{1})/) { # wind speed is in 100's of mm/s - convert to m/s $windspeed = $1 / 1000000; # see below for decoding $winddir_l = $2; # rainfall is in thousands of mm $rainfall = $3 / 1000; $battery1 = $4; $rssi1 = $5; # Wind direction is reported as a single hex digit # Seems to be using a non-standard Gray Code if ($winddir_l eq '5' ) { $winddir = 0 } elsif ($winddir_l eq '7' ) { $winddir = 22.5 } elsif ($winddir_l eq '3' ) { $winddir = 45 } elsif ($winddir_l eq '1' ) { $winddir = 67.5 } elsif ($winddir_l eq '9' ) { $winddir = 90 } elsif ($winddir_l eq 'B' ) { $winddir = 112.5 } elsif ($winddir_l eq 'F' ) { $winddir = 135 } elsif ($winddir_l eq 'D' ) { $winddir = 157.5 } elsif ($winddir_l eq 'C' ) { $winddir = 180 } elsif ($winddir_l eq 'E' ) { $winddir = 202.5 } elsif ($winddir_l eq 'A' ) { $winddir = 225 } elsif ($winddir_l eq '8' ) { $winddir = 247.5 } elsif ($winddir_l eq '0' ) { $winddir = 270 } elsif ($winddir_l eq '2' ) { $winddir = 292.5 } elsif ($winddir_l eq '6' ) { $winddir = 315 } elsif ($winddir_l eq '4' ) { $winddir = 337.5 } } elsif ($line =~ /windspeed=A(\d+)&humidity=A(\d+)&temperature=A(\d+)&battery=([^&]+)&rssi=(\d{1})/) { # wind speed is in 100's of mm/s - convert to m/s $windspeed = $1 / 1000000; $humid1 = $2 / 10; $temp1 = $3 / 1000000; $battery1 = $4; $rssi1 = $5; } else { print "51 " . $line; } } elsif($line =~ /sensor=$sensor2/) { # front porch # # id=24C86E014F4C&sensor=11583&mt=tower&humidity=A0440&temperature=A026100000&battery=normal&rssi=2 # # we hope to get temperature, humidity, battery and rssi from this one if($line =~ /humidity=A(\d+)&temperature=A(\d+)&battery=([^&]+)&rssi=(\d{1})/) { $humid2 = $1 / 10; $temp2 = $2 / 1000000; $battery2 = $3; $rssi2 = $4; } else { print "S2 " . $line; } } elsif($line =~ /sensor=$sensor3/) { # upstairs # # id=24C86E014F4C&sensor=01438&mt=tower&humidity=A0160&temperature=A019800000&battery=normal&rssi=2 # # we hope to get temperature, battery and rssi from this one # it gives us a humidity value as well, but it's fixed at 16% # (cheaper sensor) if($line =~ /humidity=A(\d+)&temperature=A(\d+)&battery=([^&]+)&rssi=(\d{1})/) { $humid3 = $1 / 10; $temp3 = $2 / 1000000; $battery3 = $3; $rssi3 = $4; } else { print "S3 " . $line; } } elsif($line =~ /mt=pressure/) { # bridge, giving barometric pressure # # id=24C86E014F4C&mt=pressure&C1=442D&C2=0C77&C3=010E&C4=027D&C5=87E7&C6=178F&C7=09C4&A=07&B=19&C=06&D=09&PR=95EF&TR=8902T # # print "BR " . $line; # this should be able to be accomplished more elegantly with regexp # but for now, this works... $c1 = hex(substr $line, index ($line, 'C1=') + 3, 4); $c2 = hex(substr $line, index ($line, 'C2=') + 3, 4); $c3 = hex(substr $line, index ($line, 'C3=') + 3, 4); $c4 = hex(substr $line, index ($line, 'C4=') + 3, 4); $c5 = hex(substr $line, index ($line, 'C5=') + 3, 4); $c6 = hex(substr $line, index ($line, 'C6=') + 3, 4); $c7 = hex(substr $line, index ($line, 'C7=') + 3, 4); $a = hex(substr $line, index ($line, 'A=') + 2, 2); $b = hex(substr $line, index ($line, 'B=') + 2, 2); $c = hex(substr $line, index ($line, 'C=') + 2, 2); $d = hex(substr $line, index ($line, 'D=') + 2, 2); $pr = hex(substr $line, index ($line, 'PR=') + 3, 4); $tr = hex(substr $line, index ($line, 'TR=') + 3, 4); if ($tr >= $c5) { $dut = $tr - $c5 - (($tr-$c5) / 2**7) * (($tr-$c5) / 2**7)* $a / 2**$c; } else { $dut = $tr - $c5 - (($tr-$c5) / 2**7) * (($tr-$c5) / 2**7)* $b / 2**$c; } $off = ($c2 + ($c4 - 1024) * $dut / 2**14) * 4; $sens = $c1 + $c3 * $dut / 2**10; $x = $sens * ($pr - 7168) / 2**14 - $off; $p = $x * 10 / 2**5 + $c7; $t = 250 + $dut * $c6 / 2**16 - $dut / 2 ** $d; $p = $p + $barocorrection; $pressure = $p / 10; } else { print "?? " . $line; } # write out the file open (NESTFILE, '/tmp/nest_current-metric') or die "can't open nestfile: $!"; open (DROPFILE, ">$dropfile") or die "can't open dropfile: $!"; print DROPFILE "outTemp = $temp1\n" if ($temp1 != '9999'); print DROPFILE "outHumidity = $humid1\n" if ($humid1 != '9999'); print DROPFILE "barometer = $pressure\n" if ($pressure != '9999'); print DROPFILE "windSpeed = $windspeed\n" if ($windspeed != '9999'); print DROPFILE "windDir = $winddir\n" if ($winddir != '9999'); print DROPFILE "rain = $rainfall\n" if ($rainfall != '9999'); print DROPFILE "extraTemp1 = $temp2\n" if ($temp2 != '9999'); print DROPFILE "extraHumid1 = $humid2\n" if ($humid2 != '9999'); print DROPFILE "extraTemp2 = $temp3\n" if ($temp3 != '9999'); print DROPFILE "extraHumid2 = $humid3\n" if ($humid3 != '9999'); print DROPFILE "rssi1 = $rssi1\n" if ($rssi1 != '9999'); print DROPFILE "rssi2 = $rssi2\n" if ($rssi2 != '9999'); print DROPFILE "rssi3 = $rssi3\n" if ($rssi3 != '9999'); foreach $nestline (<NESTFILE>) { print DROPFILE $nestline; } close(DROPFILE); close(NESTFILE); } } close(TCPDUMP);
usage:
/usr/share/weewx/user/aculink.py:/share/weather/acusniff.pl &
/etc/weewx/weewx.conf:#!/usr/bin/python # $Id: aculink.py 958 2014-07-12 19:53:28Z mwall $ # Copyright 2014 Matthew Wall # # weewx driver for AcuRite AcuLink Internet Bridge # based on the original aculink hack by george.nincehelser # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. # # See http://www.gnu.org/licenses/ # This driver will read weather data from a file. The file is specified using # the wxdata_path parameter. Each line of the file is a name=value pair, for # example: # # temp=18 # humi=54 # in_temp=20 # # To use this driver, make the following changes to weewx.conf: # # [Station] # station_type = AcuLink # [AcuLink] # loop_interval = 2 # number of seconds # wxdata_path = /var/tmp/wxdata # location of data file # driver = user.aculink from __future__ import with_statement import syslog import time import weewx.abstractstation import weewx.wxformulas DRIVER_VERSION = '0.5' def logmsg(dst, msg): syslog.syslog(dst, 'aculink: %s' % msg) def logdbg(msg): logmsg(syslog.LOG_DEBUG, msg) def loginf(msg): logmsg(syslog.LOG_INFO, msg) def logcrt(msg): logmsg(syslog.LOG_CRIT, msg) def logerr(msg): logmsg(syslog.LOG_ERR, msg) def _get_as_float(d, s): v = None if s in d: try: v = float(d[s]) except ValueError, e: logerr("cannot read value for '%s': %s" % (s, e)) return v def loader(config_dict, engine): station = AcuLink(**config_dict['AcuLink']) return station class AcuLink(weewx.abstractstation.AbstractStation): """weewx driver for the AcuLink Internet Bridge""" def __init__(self, **stn_dict): # where to find the weather data file self.wxdata_path = stn_dict.get('wxdata_path', '/var/tmp/wxdata') # how often to poll the weather data file, seconds self.loop_interval = float(stn_dict.get('loop_interval', 2.5)) self.last_rain_ts = None loginf("data file is %s" % self.wxdata_path) loginf("polling interval is %s" % self.loop_interval) def genLoopPackets(self): while True: # read whatever values we can get from the file wxdata = {} try: with open(self.wxdata_path) as f: for line in f: eq_index = line.find('=') name = line[:eq_index].strip() value = line[eq_index + 1:].strip() wxdata[name] = value except Exception, e: logerr("read failed: %s" % e) # map the data into a weewx loop packet _packet = {'dateTime': int(time.time()+0.5), 'usUnits' : weewx.METRICWX } _packet['outTemp'] = _get_as_float(wxdata, 'outTemp') _packet['outHumidity'] = _get_as_float(wxdata, 'outHumidity') _packet['inTemp'] = _get_as_float(wxdata, 'inTemp') _packet['inHumidity'] = _get_as_float(wxdata, 'inHumidity') _packet['inDewpoint'] = _get_as_float(wxdata, 'inDewpoint') _packet['barometer'] = _get_as_float(wxdata, 'barometer') _packet['altimeter'] = _get_as_float(wxdata, 'barometer') _packet['windSpeed'] = _get_as_float(wxdata, 'windSpeed') _packet['windDir'] = _get_as_float(wxdata, 'windDir') _packet['extraTemp1'] = _get_as_float(wxdata, 'extraTemp1') _packet['extraHumid1'] = _get_as_float(wxdata, 'extraHumid1') _packet['extraTemp2'] = _get_as_float(wxdata, 'extraTemp2') _packet['extraHumid2'] = _get_as_float(wxdata, 'extraHumid2') _packet['rssi1'] = _get_as_float(wxdata, 'rssi1') _packet['rssi2'] = _get_as_float(wxdata, 'rssi2') _packet['rssi3'] = _get_as_float(wxdata, 'rssi3') _packet['heatingTemp'] = _get_as_float(wxdata, 'heatingTemp') _packet['heatingHumid'] = _get_as_float(wxdata, 'heatingHumid') _packet['heatOn'] = _get_as_float(wxdata, 'heatOn') _packet['fanOn'] = _get_as_float(wxdata, 'fanOn') _packet['humidOn'] = _get_as_float(wxdata, 'humidOn') _packet['timeToTarget'] = _get_as_float(wxdata, 'timeToTarget') _packet['heatingVoltage'] = _get_as_float(wxdata, 'heatingVoltage') _packet['inTempBatteryStatus'] = _get_as_float(wxdata, 'inTempBatteryStatus') _packet['inTempConnection'] = _get_as_float(wxdata, 'inTempConnection') _packet['rain'] = _get_as_float(wxdata, 'rain') if _packet['rain']: _packet['rain'] = _packet['rain'] / 14.4 _packet['windGust'] = None _packet['windGustDir'] = None # there is no wind direction when wind speed is zero or None if not _packet['windSpeed']: _packet['windDir'] = None # calculate the rain rate _packet['rainRate'] = weewx.wxformulas.calculate_rain_rate( _packet['rain'], _packet['dateTime'], self.last_rain_ts) self.last_rain_ts = _packet['dateTime'] # calculate derived readings _packet['dewpoint'] = weewx.wxformulas.dewpointC(_packet['outTemp'], _packet['outHumidity']) _packet['windchill'] = weewx.wxformulas.windchillC(_packet['outTemp'], _packet['windSpeed']) _packet['heatindex'] = weewx.wxformulas.heatindexC(_packet['outTemp'], _packet['outHumidity']) yield _packet time.sleep(self.loop_interval) @property def hardware_name(self): return "AcuLink" # To test this driver, do the following: # cd /home/weewx # PYTHONPATH=bin python bin/user/aculink.py if __name__ == "__main__": import weeutil.weeutil station = AcuLink() for packet in station.genLoopPackets(): print weeutil.weeutil.timestamp_to_string(packet['dateTime']), packet
station_type = AcuLink ... [AcuLink] # This section for the AcuLink # The time (in seconds) between LOOP packets. loop_interval = 2 wxdata_path = /share/weather/wxdata2 driver = user.aculink