Saturday 17 December 2016

Current Approach

As I mentioned in my Connected by TCP post, I'd be doing things a little differently if I started my home automation implementation with what I know now.

Basically, here's the approach I'd take:

  1. start with the Vera Plus controller
  2. use Z-Wave switches and dimmers to control whole light circuits or outlets
  3. add Philips Hue gateway and bulbs (White or White Ambiance) to fixtures that need separate control from others on the same circuit; integrate them with Vera

Once I had lighting controlled, I'd start with some additional implementations:

  1. install the Nest Thermostat; integrate with Vera
  2. add controllable door locks; integrate with either Vera or home security system
  3. add Z-Wave water leak detection sensors
  4. add Aeon Minimote scene controllers to control specific actions
  5. install Ring Doorbell

Connected by TCP Lighting

When I started drafting this post, it would have had a very different bent, as I was impressed by the price point of the Connected by TCP system and had implemented it in a few areas of my home.

Unfortunately, development all but ceased in 2014, and support for the product ended in summer 2016.

TCP's Connected line is an example of bait & switch - they're now selling their products through Nimbus9 - which requires the purchase of a new gateway device as well as adding a $5 monthly subscription fee.  TCP's selling feature was that the system was without monthly charges and would continue to work indefinitely.

Thankfully, there have been some enterprising individuals who have managed to integrate the original TCP gateway with connected products like Vera or on your own Linux server (see http://home.stockmopar.com/updated-connected-by-tcp-api/ and https://github.com/stockmopar/connectedbytcp)

That said, Philips has added the "Hue White" line that has a similar price point as the TCP bulbs, not to mention being supported by a company that seems to stand behind their product.  Not to mention, a protocol that works more quickly and efficiently than the proprietary one TCP uses.

Additionally, there are many Z-Wave options that have come down in price and increased in accessibility.  If I was to do things over again with my home automation system, I'd certainly take some different approaches than I had in the past.  I'll be writing more about that over the next little while.

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