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=3
Data 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=8902T
The 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:
/share/weather/acusniff.pl &
/usr/share/weewx/user/aculink.py:
#!/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
/etc/weewx/weewx.conf:
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