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
Wonderful work on this. Do you have any updates to have the aculink.py work with weewx 3.4? Its complaining right now about No Module Named Abstraction. Thanks.
ReplyDeleteI've not yet updated to WeeWx 3.4. I'm pretty sure I'm still in the 2.x tree and it's working well enough for me. Didn't see any compelling reason to make the jump.
DeleteThat said, I have updated this for the new Acurite firmware - I'll be posting that in a bit.
DeleteWow :)
ReplyDeleteThis is an incredible collection of ideas!
Waiting for more helpful pieces.
You would amazing to read a similar one here-
besttoolsbrand