Wednesday 30 July 2014

Acurite AcuLink Internet Bridge Weather / weewx

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

  1. change parser / driver to deal in metric (AcuLink Bridge reports in metric, so let's keep unit conversion to a minimum, eh?)
  2. integrate listener and parser into one file
  3. 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