Removed extra files not related to eagle (sorry Ted) numpy was causing docker builds to take forever, now down to 8 seconds for eagle only stuff

This commit is contained in:
2018-09-02 23:26:23 -07:00
parent d880f44ca6
commit 30043c9b4f
66 changed files with 1 additions and 7370 deletions

View File

@@ -9,13 +9,9 @@ __doc__ = """T-Home Python package
#===========================================================================
from . import acurite
from . import broker
from . import eagle
from . import sma
from . import thermostat
from . import util
from . import weatherUnderground
#===========================================================================

View File

@@ -1,30 +0,0 @@
#===========================================================================
#
# Sensor configuration class
#
#===========================================================================
#===========================================================================
class Sensor:
"""Sensor msg processer.
Set humidity to False for temp only sensors to clear the humidity
field. The bridge reports humidity of 16% for these sensors which
is incorrrect.
"""
def __init__( self, id, location, humidity=True ):
self.id = id
self.location = location
self.hasHumidity = humidity
#------------------------------------------------------------------------
def process( self, msg ):
assert( self.id == msg.id )
msg.location = self.location
# Remove the humidity attribute if the sensor doesn't suppor it.
if not self.hasHumidity and hasattr( msg, "humidity" ):
del msg.humidity
#===========================================================================

View File

@@ -1,72 +0,0 @@
#===========================================================================
#
# Acurite weather station package.
#
#===========================================================================
__doc__ = """T-Home Acurite weather station package.
Used to intercept Acurite post commands from an Acurite bridge being
posted to the Acurite web sites. The bridge reads radio traffic from
the sensors and posts them.
This package assumes the code is run on a system in-line with the
bridge. See: http://www.bobshome.net/weather/ for details.
Designed for use w/ a Raspberry Pi. Use a USB network adaptor and
plug the bridge into the adaptor. Run the code on the pi with the USB
network adaptor set up as a bridge to the regular network.
On the PI, install bridge and TCP monitoring utilities:
apt-get install bridge-utils tcpdump tcpflow
Then edit /etc/network/interfaces to configure the USB network as a
bridge to the main network.
--------------
auto lo
iface lo inet loopback
iface eth0 inet dhcp
allow-hotplug wlan0
iface wlan0 inet manual
wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
iface default inet dhcp
auto eth1 br0
iface br0 inet dhcp
iface eth1 inet manual
bridge_ports eth0 eth1
--------------
The restart the network:
sudo service networking restart
To access the data, use tcpflow (see bin/acurite-read.sh for this
script) to intercept data from the USB link and pass it to the script.
--------------
#!/bin/sh
SCRIPT=$HOME/tHome/bin/acurite-send.py
LOG=/var/log/tHome/acurite-send.log
(/usr/bin/tcpflow -c -i eth1 -s tcp dst port 80 | $SCRIPT) 2>> $LOG &
--------------
"""
#===========================================================================
from . import cmdLine
from . import config
from .decode import decode
from . import mqtt
from .Sensor import Sensor
#===========================================================================

View File

@@ -1,68 +0,0 @@
#===========================================================================
#
# Acurite bridge parser
#
#===========================================================================
import argparse
import json
import sys
from .. import broker
from . import config
from .decode import decode
from . import mqtt
#===========================================================================
def process( config, text, sensorMap ):
idx = text.find( "id=" )
if idx == -1:
return []
# Parse the data from the HTTP command.
data = decode( text[idx:], sensorMap )
if not data:
return []
# Convert to a list of MQTT (topic, payload) tuples.
return mqtt.convert( config, data )
#===========================================================================
def run( args, input=sys.stdin ):
"""DEPRECATED
This function is used when intercepting bridge traffic. The new
way redirects bridge traffic which bin/tHome-acurite.py handles so
this function isn't needed any more.
"""
p = argparse.ArgumentParser( prog=args[0],
description="Acurite decoder" )
p.add_argument( "-c", "--configDir", metavar="configDir",
default="/var/config/tHome",
help="T-Home configuration directory." )
p.add_argument( "-l", "--log", metavar="logFile",
default=None, help="Logging file to use. Input 'stdout' "
"to log to the screen." )
c = p.parse_args( args[1:] )
# Parse the acurite config file.
cfg = config.parse( c.configDir )
log = config.log( cfg, c.log )
sensorMap = {}
for s in cfg.sensors:
sensorMap[s.id] = s
# Create the MQTT client and connect it to the broker.
client = broker.connect( c.configDir, log )
while True:
line = sys.stdin.readline()
msgs = process( cfg, line, sensorMap )
for topic, data in msgs:
log.info( "Publish: %s: %s" % ( topic, payload ) )
payload = json.dumps( data )
client.publish( topic, payload )
#===========================================================================

View File

@@ -1,46 +0,0 @@
#===========================================================================
#
# Config file
#
#===========================================================================
__doc__ = """Config file parsing.
"""
from .. import util
from ..util import config as C
#===========================================================================
# Config file section name and defaults.
configEntries = [
# ( name, converter function, default value )
C.Entry( "logFile", util.path.expand ),
C.Entry( "logLevel", int, 20 ), # INFO
C.Entry( "sensors", list ),
C.Entry( "mqttBattery", str ),
C.Entry( "mqttRssi", str ),
C.Entry( "mqttHumidity", str ),
C.Entry( "mqttTemp", str ),
C.Entry( "mqttWindSpeed", str ),
C.Entry( "mqttWindDir", str ),
C.Entry( "mqttBarometer", str ),
C.Entry( "mqttRain", str ),
]
#===========================================================================
def parse( configDir, configFile='acurite.py' ):
m = C.readAndCheck( configDir, configFile, configEntries )
return m
#===========================================================================
def log( config, logFile=None ):
if not logFile:
logFile = config.logFile
return util.log.get( "acurite", config.logLevel, logFile )
#===========================================================================

View File

@@ -1,172 +0,0 @@
#===========================================================================
#
# Acurite sensor data class
#
#===========================================================================
import StringIO
import time
from ..util import Data
#------------------------------------------------------------------------
windMap = {
'5' : 0,
'7' : 22.5,
'3' : 45,
'1' : 67.5,
'9' : 90,
'B' : 112.5,
'F' : 135,
'D' : 157.5,
'C' : 180,
'E' : 202.5,
'A' : 225,
'8' : 247.5,
'0' : 270,
'2' : 292.5,
'6' : 315,
'4' : 337.5,
}
#------------------------------------------------------------------------
batteryMap = {
"normal" : 1.0,
"low" : 0.1,
}
#===========================================================================
def decode( text, sensorMap ):
"""Decode a sensor post from the Acurite bridge.
Input is a line of text sent by the bridge to the Acurite server.
Return valeue is a tHome.util.Data object (dict) with the parsed
values.
"""
# Skip lines that aren't the sensor information.
idx = text.find( "id=" )
if idx == -1:
return
text = text[idx:]
# Split the input into fields.
elems = text.split( "&" )
# Get the key/value pairs for each element.
items = {}
for e in elems:
k, v = e.split( "=" )
items[k] = v
# Create an empty data object (dict) to store the results.
data = Data()
# No time field in the data - record the current time as the time
# stamp.
data.time = time.time()
# Call the handler function for each element.
for k, v in items.iteritems():
func = handlers.get( k, None )
if func:
func( data, items, k, v )
# Use the sensor map to process the data. This primarily sets a
# location label given the sensor ID field.
s = sensorMap.get( data.id, None )
if s:
s.process( data )
else:
data.location = "Unknown"
return data
#===========================================================================
def _readSensor( data, items, key, value ):
data.id = value
#===========================================================================
def _readPressure( data, items, key, value ):
if value != "pressure":
return
# Convert hex strings to integer values
c1 = int( items["C1"], 16 )
c2 = int( items["C2"], 16 )
c3 = int( items["C3"], 16 )
c4 = int( items["C4"], 16 )
c5 = int( items["C5"], 16 )
c6 = int( items["C6"], 16 )
c7 = int( items["C7"], 16 )
a = int( items["A"], 16 )
b = int( items["B"], 16 )
c = int( items["C"], 16 )
d = int( items["D"], 16 )
pr = int( items["PR"], 16 )
tr = int( items["TR"], 16 )
if tr >= c5:
dut = tr - c5 - (tr-c5)/128.0 * (tr-c5)/128.0 * a/2**c
else:
dut = tr - c5 - (tr-c5)/128.0 * (tr-c5)/128.0 * b/2**c
off = (c2 + (c4 - 1024) * dut / 16384.0) * 4
sens = c1 + c3 * dut / 1024.0
x = sens * (pr - 7168) / 16384.0 - off
p = x * 10 / 32 + c7 + 760.0
data.id = items.get( "id", "Unknown" )
data.pressure = round( p / 338.637526, 2 ) # Convert to HgIn
#===========================================================================
def _readSignal( data, items, key, value ):
data.signal = float( value ) / 4.0
#===========================================================================
def _readBattery( data, items, key, value ):
data.battery = batteryMap.get( value, 0 )
#===========================================================================
def _readWindDir( data, items, key, value ):
data.windDir = windMap.get( value, None )
#===========================================================================
def _readWindSpeed( data, items, key, value ):
""" A0aaaabbbbb == aaaa.bbbbb cm/sec
"""
cmPerSec = float( value[2:6] ) + float( value[6:10] ) / 1e4
data.windSpeed = round( cmPerSec / 44.704, 2 )
#===========================================================================
def _readTemp( data, items, key, value ):
""" Aaaabbbbbb == aaa.bbbbbb deg C
"""
degC = float( value[1:4] ) + float( value[4:10] ) / 1e6
data.temperature = round( degC * 1.8 + 32, 1 )
#===========================================================================
def _readHumidity( data, items, key, value ):
""" Aaaabbbbbb == aaa.bbbbbb percentage
"""
data.humidity = float( value[1:4] ) + float( value[4:10] ) / 1e6
#===========================================================================
def _readRainfall( data, items, key, value ):
""" A0aabbbb == aa.bbbb cm in the last 36 seconds
"""
cm = float( value[2:4] ) + float( value[4:8] ) / 1e4
data.rainfall = round( cm / 25.4, 3 )
#===========================================================================
handlers = {
"sensor" : _readSensor,
"mt" : _readPressure,
"windspeed" : _readWindSpeed,
"winddir" : _readWindDir,
"temperature" : _readTemp,
"humidity" : _readHumidity,
"rainfall" : _readRainfall,
"battery" : _readBattery,
"rssi" : _readSignal,
}
#===========================================================================

View File

@@ -1,79 +0,0 @@
#===========================================================================
#
# Convert decoded data to MQTT messages.
#
#===========================================================================
#===========================================================================
def convert( config, data ):
# List of tuples of ( topic, payload ) where payload is a dictionary.
msgs = []
if hasattr( data, "battery" ):
topic = config.mqttBattery % data.location
payload = {
"time" : data.time,
"battery" : data.battery,
}
msgs.append( ( topic, payload ) )
if hasattr( data, "signal" ):
topic = config.mqttRssi % data.location
payload = {
"time" : data.time,
# Input is 0->1, convert to 0->100
"rssi" : data.signal * 100,
}
msgs.append( ( topic, payload ) )
if hasattr( data, "humidity" ):
topic = config.mqttHumidity % data.location
payload = {
"time" : data.time,
"humidity" : data.humidity,
}
msgs.append( ( topic, payload ) )
if hasattr( data, "temperature" ):
topic = config.mqttTemp % data.location
payload = {
"time" : data.time,
"temperature" : data.temperature,
}
msgs.append( ( topic, payload ) )
if hasattr( data, "windSpeed" ):
topic = config.mqttWindSpeed % data.location
payload = {
"time" : data.time,
"speed" : data.windSpeed,
}
msgs.append( ( topic, payload ) )
if hasattr( data, "windDir" ):
topic = config.mqttWindDir % data.location
payload = {
"time" : data.time,
"direction" : data.windDir,
}
msgs.append( ( topic, payload ) )
if hasattr( data, "pressure" ):
topic = config.mqttBarometer % data.location
payload = {
"time" : data.time,
"pressure" : data.pressure,
}
msgs.append( ( topic, payload ) )
if hasattr( data, "rainfall" ):
topic = config.mqttRain % data.location
payload = {
"time" : data.time,
"rain" : data.rainfall,
}
msgs.append( ( topic, payload ) )
return msgs
#===========================================================================

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env python
from tHome import acurite
sensorMap = {}
for l in open( "/home/ted/weather.log", "r" ):
p = acurite.decode( l, sensorMap )
if p:
print p

View File

@@ -1,29 +0,0 @@
#!/usr/bin/env python
from tHome import acurite
sensors = [
acurite.Sensor( "08260", "Garage" ),
acurite.Sensor( "09096", "Kitchen" ),
acurite.Sensor( "00414", "Backyard" ),
acurite.Sensor( "24C86E0449A0", "Bridge" ),
acurite.Sensor( "05250", "Courtyard", humidity=False ),
acurite.Sensor( "16039", "Rec Room", humidity=False ),
acurite.Sensor( "02717", "Front Bedroom", humidity=False ),
acurite.Sensor( "05125", "Den", humidity=False ),
acurite.Sensor( "08628", "Garage 2", humidity=False ),
acurite.Sensor( "09338", "Side Bedroom", humidity=False ),
acurite.Sensor( "01948", "Master Closet", humidity=False ),
acurite.Sensor( "15116", "Attic", humidity=False ),
acurite.Sensor( "05450", "Master Bath", humidity=False ),
]
sensorMap = {}
for s in sensors:
sensorMap[s.id] = s
for l in open( "/home/ted/weather.log", "r" ):
r = acurite.cmdLine.process( l, sensorMap )
if r:
print r

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env python
from tHome import acurite
acurite.run()

View File

@@ -1,127 +0,0 @@
#===========================================================================
#
# Log on/off packets
#
#===========================================================================
import logging
import socket
import time
# Import the base header and the struct type codes we're using.
from .Header import *
from .. import util
#===========================================================================
class LogOn ( Header ):
_fields = Header._fields + [
( uint4, 'command' ),
( uint4, 'group' ),
( uint4, 'timeout' ),
( uint4, 'time' ),
( uint4, 'unknown1' ),
( '12s', "password" ),
( uint4, 'trailer' ),
]
struct = util.NamedStruct( 'LITTLE_ENDIAN', _fields )
#------------------------------------------------------------------------
def __init__( self, group, password ):
assert( len( password ) <= 12 )
assert( group == "USER" or group == "INSTALLER" )
Header.__init__( self )
if group == "USER":
self.group = 0x00000007
passOffset = 0x88
else: # installer
self.group = 0x0000000A
passOffset = 0xBB
self.password = ""
# Loop over each character and encode it as hex and offset by
# the group code offset.
for i in range( 12 ):
if i < len( password ):
c = int( password[i].encode( 'hex' ), 16 ) + passOffset
# Pad out to 12 bytes w/ the group code offset.
else:
c = passOffset
# Turn the hex code back to a character.
self.password += chr( c )
self.destCtrl = 0x0100
self.srcCtrl = 0x0100
self.command = 0xFFFD040C
self.timeout = 0x00000384 # 900 sec
self.time = int( time.time() )
self.unknown1 = 0x00
self.trailer = 0x00
#------------------------------------------------------------------------
def send( self, sock ):
# Pack ourselves into the message structure.
bytes = self.struct.pack( self )
if self._log.isEnabledFor( logging.DEBUG ):
self._log.debug( "Send: LogOn packet\n" + util.hex.dump( bytes ) )
try:
# Send the message and receive the response back.
sock.send( bytes )
bytes = sock.recv( 4096 )
except socket.timeout as e:
msg = "Can't log on - time out error"
self._log.error( msg )
util.Error.raiseException( e, msg )
if self._log.isEnabledFor( logging.DEBUG ):
self._log.debug( "Recv: LogOn reply\n" + util.hex.dump( bytes ) )
# Overwrite our fields w/ the reply data.
self.struct.unpack( self, bytes )
if self.error:
raise util.Error( "Error trying to log on to the SMA inverter. "
"Group/password failed." )
#------------------------------------------------------------------------
#===========================================================================
class LogOff ( Header ):
_fields = Header._fields + [
( uint4, 'command' ),
( uint4, 'unknown1' ),
( uint4, 'trailer' ),
]
struct = util.NamedStruct( 'LITTLE_ENDIAN', _fields )
#------------------------------------------------------------------------
def __init__( self ):
Header.__init__( self )
self.destCtrl = 0x0300
self.srcCtrl = 0x0300
self.command = 0xFFFD010E
self.unknown1 = 0xFFFFFFFF
self.trailer = 0x00
#------------------------------------------------------------------------
def send( self, sock ):
bytes = self.struct.pack( self )
if self._log.isEnabledFor( logging.DEBUG ):
self._log.debug( "Send: LogOff packet\n" + util.hex.dump( bytes ) )
sock.send( bytes )
#------------------------------------------------------------------------
#===========================================================================

View File

@@ -1,92 +0,0 @@
#===========================================================================
#
# Common data packet structures. Used for requests and replies.
#
#===========================================================================
from .. import util
#===========================================================================
# Struct type codes
uint1 = "B"
uint2 = "H"
uint4 = "I"
uint8 = "Q"
int1 = "b"
int2 = "h"
int4 = "i"
int8 = "q"
#==============================================================================
class Header:
_fields = [
# Header fields
( uint4, 'hdrMagic' ),
( uint4, 'hdrUnknown1' ),
( uint4, 'hdrUnknown2' ),
( uint1, 'packetHi' ), # packet length in little endian hi word
( uint1, 'packetLo' ), # packet length in little endian low word
( uint4, 'signature' ),
( uint1, 'wordLen' ), # int( packetLen / 4 )
( uint1, 'hdrUnknown3' ),
# Common packet fields
( uint2, 'destId', ),
( uint4, 'destSerial', ),
( uint2, 'destCtrl', ),
( uint2, 'srcId', ),
( uint4, 'srcSerial', ),
( uint2, 'srcCtrl', ),
( uint2, 'error', ),
( uint2, 'fragmentId', ),
( uint1, 'packetId', ),
( uint1, 'baseUnknown', ),
]
_hdrSize = 20 # bytes for the header fields.
_nextPacketId = 0
#------------------------------------------------------------------------
def __init__( self ):
assert( self.struct )
self.hdrMagic = 0x00414D53
self.hdrUnknown1 = 0xA0020400
self.hdrUnknown2 = 0x01000000
self.signature = 0x65601000
self.hdrUnknown3 = 0xA0
# NOTE: self.struct must be created by the derived class. That
# allows this to compute the correct packet length and encode it.
packetLen = len( self.struct ) - self._hdrSize
self.packetHi = ( packetLen >> 8 ) & 0xFF
self.packetLo = packetLen & 0xFF
self.wordLen = int( packetLen / 4 )
# Any destination - we send to a specific IP address so this
# isn't important.
self.destId = 0xFFFF
self.destSerial = 0xFFFFFFFF
self.destCtrl = 0x00
self.srcId = 0x7d # TODO change this?
self.srcSerial = 0x334657B0 # TODO change this?
self.srcCtrl = 0x00
self.error = 0
self.fragmentId = 0
self.baseUnknown = 0x80
# Packet id is 1 byte so roll over at 256.
self._nextPacketId += 1
if self._nextPacketId == 256:
self._nextPacketId = 1
self.packetId = self._nextPacketId
self._log = util.log.get( "sma" )
#------------------------------------------------------------------------
def bytes( self ):
return self.struct.pack( self )
#------------------------------------------------------------------------
#===========================================================================

View File

@@ -1,240 +0,0 @@
#===========================================================================
#
# Primary SMA API.
#
#===========================================================================
import socket
from .. import util
from . import Auth
from . import Reply
from . import Request
#==============================================================================
class Link:
"""SMA WebConnection link
Units: Watt, Watt-hours, C, seconds
l = Link( '192.168.1.14' )
print l.acTotalEnergy()
See also: report for common requests.
"""
def __init__( self, ip, port=9522, group="USER", password="0000",
connect=True, timeout=120, decode=True, raw=False ):
if group != "USER" and group != "INSTALLER":
raise util.Error( "Invalid group '%s'. Valid groups are 'USER' "
"'INSTALLER'." % group )
self.ip = ip
self.port = port
self.group = group
self.password = password
self.timeout = timeout
self.decode = decode
self.raw = raw
self.socket = None
if connect:
self.open()
#---------------------------------------------------------------------------
def info( self ):
p = Request.Data( command=0x58000200, first=0x00821E00, last=0x008220FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.StringItem( "name", 40, timeVar="timeWake" ),
Reply.AttrItem( "type", 40 ),
Reply.AttrItem( "model", 40 ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def status( self ):
p = Request.Data( command=0x51800200, first=0x00214800, last=0x002148FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.AttrItem( "status", 32, timeVar="time" ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def gridRelayStatus( self ):
p = Request.Data( command=0x51800200, first=0x00416400, last=0x004164FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.AttrItem( "gridStatus", 32, timeVar="timeOff" ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def temperature( self ):
"""Return the inverter temp in deg C (or 0 if unavailable)."""
p = Request.Data( command=0x52000200, first=0x00237700, last=0x002377FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.I32Item( "temperature", 16, mult=0.01 ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def version( self ):
"""Return the inverter software version string."""
p = Request.Data( command=0x58000200, first=0x00823400, last=0x008234FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.VersionItem( "version" ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def acTotalEnergy( self ):
p = Request.Data( command=0x54000200, first=0x00260100, last=0x002622FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.I64Item( "totalEnergy", 16, mult=1.0, timeVar="timeLast" ),
Reply.I64Item( "dailyEnergy", 16, mult=1.0 ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def acTotalPower( self ):
p = Request.Data( command=0x51000200, first=0x00263F00, last=0x00263FFF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.I32Item( "acPower", 28, mult=1.0, timeVar="timeOff" ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def acPower( self ):
p = Request.Data( command=0x51000200, first=0x00464000, last=0x004642FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.I32Item( "acPower1", 28, mult=1.0, timeVar="timeOff" ),
Reply.I32Item( "acPower2", 28, mult=1.0 ),
Reply.I32Item( "acPower3", 28, mult=1.0 ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def acMaxPower( self ):
p = Request.Data( command=0x51000200, first=0x00411E00, last=0x004120FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.U32Item( "acMaxPower1", 28, mult=1.0, timeVar="time" ),
Reply.U32Item( "acMaxPower2", 28, mult=1.0 ),
Reply.U32Item( "acMaxPower3", 28, mult=1.0 ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def operationTime( self ):
p = Request.Data( command=0x54000200, first=0x00462E00, last=0x00462FFF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.I64Item( "operationTime", 16, mult=1.0, timeVar="timeLast" ),
Reply.I64Item( "feedTime", 16, mult=1.0 ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def dcPower( self ):
p = Request.Data( command=0x53800200, first=0x00251E00, last=0x00251EFF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.I32Item( "dcPower1", 28, mult=1.0, timeVar="timeOff" ),
Reply.I32Item( "dcPower2", 28, mult=1.0 ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def dcVoltage( self ):
p = Request.Data( command=0x53800200, first=0x00451F00, last=0x004521FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.I32Item( "dcVoltage1", 28, mult=0.01, timeVar="timeOff" ),
Reply.I32Item( "dcVoltage2", 28, mult=0.01 ),
Reply.I32Item( "dcCurrent1", 28, mult=0.001 ),
Reply.I32Item( "dcCurrent2", 28, mult=0.001 ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def acVoltage( self ):
p = Request.Data( command=0x51000200, first=0x00464800, last=0x004652FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.U32Item( "acVoltage1", 28, mult=0.01, timeVar="timeOff" ),
Reply.U32Item( "acVoltage2", 28, mult=0.01 ),
Reply.U32Item( "acVoltage3", 28, mult=0.01 ),
Reply.U32Item( "acGridVoltage", 28, mult=0.01 ),
Reply.U32Item( "unknown1", 28, mult=0.01 ),
Reply.U32Item( "unknown2", 28, mult=0.01 ),
Reply.U32Item( "acCurrent1", 28, mult=0.001 ),
Reply.U32Item( "acCurrent2", 28, mult=0.001 ),
Reply.U32Item( "acCurrent3", 28, mult=0.001 ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def gridFrequency( self ):
p = Request.Data( command=0x51000200, first=0x00465700, last=0x004657FF )
bytes = p.send( self.socket )
decoder = Reply.Value( [
Reply.U32Item( "frequency", 28, mult=0.01, timeVar="timeOff" ),
] )
return self._return( bytes, decoder )
#---------------------------------------------------------------------------
def __del__( self ):
self.close()
#---------------------------------------------------------------------------
def open( self ):
if self.socket:
return
self.socket = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )
self.socket.settimeout( self.timeout )
try:
self.socket.connect( ( self.ip, self.port ) )
p = Auth.LogOn( self.group, self.password )
p.send( self.socket )
except:
if self.socket:
self.socket.close()
self.socket = None
raise
#---------------------------------------------------------------------------
def close( self ):
if not self.socket:
return
p = Auth.LogOff()
try:
p.send( self.socket )
finally:
self.socket.close()
self.socket = None
#---------------------------------------------------------------------------
def __enter__( self ):
return self
#---------------------------------------------------------------------------
def __exit__( self, type, value, traceback ):
self.close()
#---------------------------------------------------------------------------
def _return( self, bytes, decoder ):
if self.decode:
return decoder.decode( bytes, self.raw )
else:
return ( bytes, decoder )
#==============================================================================

View File

@@ -1,225 +0,0 @@
#===========================================================================
#
# Data reply packets
#
#===========================================================================
import struct
from .. import util
# Import the base header and the struct type codes we're using.
from .Header import *
from . import tags
#===========================================================================
class Value ( Header, util.Data ):
_fields = Header._fields + [
( uint4, 'command' ),
( uint4, 'first' ),
( uint4, 'last' ),
]
struct = util.NamedStruct( 'LITTLE_ENDIAN', _fields )
def __init__( self, decoders ):
self.values = []
self._decoders = decoders
Header.__init__( self )
util.Data.__init__( self )
def __call__( self, bytes, raw=False, obj=None ):
return self.decode( bytes, raw, obj )
def decode( self, bytes, raw=False, obj=None ):
# Unpack the base data.
self.struct.unpack( self, bytes )
offset = len( self.struct )
for d in self._decoders:
offset += d.decodeItem( self, bytes, offset )
if raw:
return self
r = obj if obj is not None else util.Data()
for item in self.values:
for varFrom, varTo in item._variables:
value = getattr( item, varFrom )
setattr( r, varTo, value )
return r
#==============================================================================
class BaseItem ( util.Data ):
_fields = [
( uint1, 'mppNum' ),
( uint2, 'msgType' ),
( uint1, 'dataType' ),
( uint4, 'time' ),
]
_baseSize = 8 # bytes
def __init__( self, var, size, timeVar=None ):
self.variable = var
self._size = size
self._variables = []
if timeVar:
self._variables.append( ( 'time', timeVar ) )
util.Data.__init__( self )
def decodeBase( self, obj, attrVal ):
setattr( obj, self.variable, attrVal )
obj.values.append( self )
return self._size
#==============================================================================
class StringItem ( BaseItem ):
def __init__( self, var, size, timeVar=None ):
BaseItem.__init__( self, var, size, timeVar )
self._fields = BaseItem._fields + [
( '%ds' % ( size - BaseItem._baseSize ), "bytes" ),
]
self._struct = util.NamedStruct( 'LITTLE_ENDIAN', self._fields )
self._variables.append( ( 'value', var ) )
def decodeItem( self, obj, bytes, offset ):
self._struct.unpack( self, bytes, offset )
s = self.bytes
idx = s.find( "\0" )
if idx != -1:
s = s[:idx]
self.value = s
return BaseItem.decodeBase( self, obj, self.value )
#==============================================================================
class AttrItem ( BaseItem ):
struct = util.NamedStruct( 'LITTLE_ENDIAN', BaseItem._fields )
def __init__( self, var, size, timeVar=None ):
BaseItem.__init__( self, var, size, timeVar )
self._numAttr = ( size - BaseItem._baseSize ) / 4
self._variables.append( ( 'value', var ) )
def decodeItem( self, obj, bytes, offset ):
self.struct.unpack( self, bytes, offset )
offset += len( self.struct )
# Little endian, 4 individual unsigned bytes
s = struct.Struct( '<BBBB' )
for i in range( self._numAttr ):
# x[0], x[1], x[2] are the attribute code
# x[3] == 1 if the attribute is active or 0 if not
# Only 1 active attribute per block
x = s.unpack_from( bytes, offset )
if x[3] == 1:
self.code = x[2]*65536 + x[1]*256 + x[0]
self.value = tags.values.get( self.code, "Unknown" )
break
offset += s.size
return BaseItem.decodeBase( self, obj, self.value )
#==============================================================================
class BaseInt ( BaseItem ):
def __init__( self, var, size, mult=1.0, timeVar=None ):
BaseItem.__init__( self, var, size, timeVar )
self._mult = mult
self._variables.append( ( 'value', var ) )
def decodeItem( self, obj, bytes, offset ):
self.struct.unpack( self, bytes, offset )
if self.value == self.nan:
self.value = 0
else:
self.value *= self._mult
return BaseItem.decodeBase( self, obj, self.value )
#==============================================================================
class I32Item ( BaseInt ):
_fields = BaseInt._fields + [
( int4, 'value' ),
]
struct = util.NamedStruct( 'LITTLE_ENDIAN', _fields )
nan = -0x80000000 # SMA int32 NAN value
#==============================================================================
class U32Item ( BaseInt ):
_fields = BaseInt._fields + [
( uint4, 'value' ),
]
struct = util.NamedStruct( 'LITTLE_ENDIAN', _fields )
nan = 0xFFFFFFFF # SMA uint32 NAN value
#==============================================================================
class I64Item ( BaseInt ):
_fields = BaseInt._fields + [
( int8, 'value' ),
]
struct = util.NamedStruct( 'LITTLE_ENDIAN', _fields )
nan = -0x8000000000000000 # SMA int64 NAN value
#==============================================================================
class U64Item ( BaseInt ):
_fields = BaseInt._fields + [
( uint8, 'value' ),
]
struct = util.NamedStruct( 'LITTLE_ENDIAN', _fields )
def __init__( self, size, var, mult=1.0 ):
# 0xFFFFFFFFFFFFFFFF == SMA uint64 NAN value
BaseInt.__init__( self, 0xFFFFFFFFFFFFFFFF, size, var, mult )
#==============================================================================
class VersionItem ( BaseItem ):
_fields = BaseItem._fields + [
( uint4, 'value' ),
( uint4, 'value2' ),
( uint4, 'value3' ),
( uint4, 'value4' ),
( uint1, 'verType' ),
( uint1, 'verBuild' ),
( uint1, 'verMinor' ),
( uint1, 'verMajor' ),
]
struct = util.NamedStruct( 'LITTLE_ENDIAN', _fields )
def __init__( self, var ):
BaseItem.__init__( self, var, 24 )
self._variables.append( ( 'version', var ) )
def decodeItem( self, obj, bytes, offset ):
self.struct.unpack( self, bytes, offset )
if self.verType > 5:
relType = str( self.verType )
else:
relType = "NEABRS"[self.verType]
self.version = '%02x.%02x.%02x.%s' % ( self.verMajor, self.verMinor,
self.verBuild, relType )
return BaseItem.decodeBase( self, obj, self.version )
#==============================================================================

View File

@@ -1,58 +0,0 @@
#===========================================================================
#
# Data request packet
#
#===========================================================================
import logging
import socket
# Import the base header and the struct type codes we're using.
from .Header import *
from .. import util
#===========================================================================
class Data ( Header ):
_fields = Header._fields + [
( uint4, 'command' ),
( uint4, 'first' ),
( uint4, 'last' ),
( uint4, 'trailer' ),
]
struct = util.NamedStruct( 'LITTLE_ENDIAN', _fields )
#------------------------------------------------------------------------
def __init__( self, command, first, last ):
Header.__init__( self )
self.command = command
self.first = first
self.last = last
self.trailer = 0x00
#------------------------------------------------------------------------
def send( self, sock ):
bytes = self.struct.pack( self )
if self._log.isEnabledFor( logging.DEBUG ):
self._log.debug( "Send: Request.Data packet\n" +
util.hex.dump( bytes ) )
# Send the request and read the reply back in.
try:
sock.send( bytes )
reply = sock.recv( 4096 )
except socket.timeout as e:
msg = "Data request failed - time out error"
self._log.error( msg )
util.Error.raiseException( e, msg )
if self._log.isEnabledFor( logging.DEBUG ):
self._log.debug( "Recv: Reply packet\n" + util.hex.dump( reply ) )
return reply
#------------------------------------------------------------------------
#===========================================================================

View File

@@ -1,28 +0,0 @@
#===========================================================================
#
# SMA inverter module
#
#===========================================================================
__doc__ = """Read SMA Solar inverter data.
See Link and report for the main programming interfaces.
See cmdLine for a main program to poll the inverter and send out MQTT
messges.
"""
#===========================================================================
from . import Auth
from . import cmdLine
from . import config
from .Header import Header
from .Link import Link
from . import Reply
from . import report
from . import Request
from . import start
from . import tags
#===========================================================================

View File

@@ -1,49 +0,0 @@
#===========================================================================
#
# Command line processing
#
#===========================================================================
import argparse
from .. import broker
from . import config
from . import start
#===========================================================================
def run( args ):
"""Parse command line arguments to poll the inverter.
= INPUTS
- args [str]: List of command line arguments. [0] should be the
program name.
"""
p = argparse.ArgumentParser( prog=args[0],
description="SMA inverter reader" )
p.add_argument( "-c", "--configDir", metavar="configDir",
default="/var/config/tHome",
help="T-Home configuration directory." )
p.add_argument( "-l", "--log", metavar="logFile",
default=None, help="Logging file to use. Input 'stdout' "
"to log to the screen." )
p.add_argument( "--debug", default=False, action="store_true",
help="Debugging - no sending, just print" )
c = p.parse_args( args[1:] )
if c.debug:
c.log = "stdout"
# Parse the sma and broker config files.
cfg = config.parse( c.configDir )
log = config.log( cfg, c.log )
if c.debug:
log.setLevel( 10 )
# Create the MQTT client and connect it to the broker.
client = broker.connect( c.configDir, log )
start.start( cfg, client, debug=c.debug )
#===========================================================================

View File

@@ -1,49 +0,0 @@
#===========================================================================
#
# Config file
#
#===========================================================================
__doc__ = """Config file parsing.
"""
from .. import util
from ..util import config as C
#===========================================================================
# Config file section name and defaults.
configEntries = [
# ( name, converter function, default value )
C.Entry( "host", str ),
C.Entry( "port", int, 9522 ),
C.Entry( "group", str, "USER" ),
C.Entry( "password", str, "0000" ),
C.Entry( "logFile", util.path.expand ),
C.Entry( "logLevel", int, 20 ), # INFO
C.Entry( "pollPower", int, 10 ),
C.Entry( "pollEnergy", int, 600 ), # 10 minutes
C.Entry( "pollFull", int, 0 ), # disabled
C.Entry( "mqttEnergy", str, "elec/solar/meter" ),
C.Entry( "mqttPower", str, "elec/solar/instant" ),
C.Entry( "mqttFull", str, "elec/solar/detail" ),
C.Entry( "lat", float ),
C.Entry( "lon", float ),
C.Entry( "timePad", float, 600 ), # 10 minutes
]
#===========================================================================
def parse( configDir, configFile='sma.py' ):
return C.readAndCheck( configDir, configFile, configEntries )
#===========================================================================
def log( config, logFile=None ):
if not logFile:
logFile = config.logFile
return util.log.get( "sma", config.logLevel, logFile )
#===========================================================================

View File

@@ -1,99 +0,0 @@
#===========================================================================
#
# Report functions
#
#===========================================================================
import time
from ..util import Data
from .Link import Link
#===========================================================================
def power( *args, **kwargs ):
"""Return instantaneous AC and DC power generation.
Inputs are the same as Link() constructor:
obj = report.instant( '192.168.1.15' )
print obj
"""
with Link( *args, **kwargs ) as link:
link.decode = False
link.raw = False
dcBytes, dc = link.dcPower()
acBytes, ac = link.acTotalPower()
now = time.time()
obj = dc.decode( dcBytes )
obj.update( ac.decode( acBytes ) )
obj.time = now
obj.dcPower = obj.dcPower1 + obj.dcPower2
return obj
#===========================================================================
def energy( *args, **kwargs ):
"""Return instantaneous power and total energy status.
Get instantaneous AC and DC power generation and energy created for
the day.
Inputs are the same as Link() constructor:
obj = report.energy( '192.168.1.15' )
print obj
"""
with Link( *args, **kwargs ) as link:
link.decode = False
dcBytes, dc = link.dcPower()
acBytes, ac = link.acTotalPower()
totBytes, total = link.acTotalEnergy()
now = time.time()
obj = dc.decode( dcBytes )
obj.update( ac.decode( acBytes ) )
obj.update( total.decode( totBytes ) )
obj.time = now
obj.dcPower = obj.dcPower1 + obj.dcPower2
return obj
#===========================================================================
def full( *args, **kwargs ):
"""Return all possible fields.
Inputs are the same as Link() constructor:
obj = report.full( '192.168.1.15' )
print obj
"""
funcs = [
Link.info,
Link.status,
Link.gridRelayStatus,
Link.temperature,
Link.version,
Link.acTotalEnergy,
Link.acTotalPower,
Link.acPower,
Link.acMaxPower,
Link.operationTime,
Link.dcPower,
Link.dcVoltage,
Link.acVoltage,
Link.gridFrequency,
]
with Link( *args, **kwargs ) as link:
link.decode = False
results = [ f( link ) for f in funcs ]
now = time.time()
obj = Data()
for bytes, decoder in results:
obj.update( decoder.decode( bytes ) )
obj.time = now
obj.dcPower = obj.dcPower1 + obj.dcPower2
return obj
#===========================================================================

View File

@@ -1,207 +0,0 @@
#===========================================================================
#
# Main report processing
#
#===========================================================================
import astral
import calendar
import datetime
import json
import time
from .. import util
from . import report
#===========================================================================
def start( config, client, debug=False ):
fromts = datetime.datetime.fromtimestamp
log = util.log.get( "sma" )
linkArgs = { "ip" : config.host, "port" : config.port,
"group" : config.group, "password" : config.password, }
reports = []
if config.pollFull > 0:
reports.append( util.Data( lbl="full", func=msgFull,
poll=config.pollFull, nextT=0 ) )
if config.pollEnergy > 0:
reports.append( util.Data( lbl="energy", func=msgEnergy,
poll=config.pollEnergy, nextT=0 ) )
if config.pollPower > 0:
reports.append( util.Data( lbl="power", func=msgPower,
poll=config.pollPower, nextT=0 ) )
if not reports:
log.error( "No reports specified, all poll times <= 0" )
return
t0 = time.time()
useRiseSet = ( config.lat is not None and config.lon is not None )
log.info( "Startup time : %s" % fromts( t0 ) )
if useRiseSet:
timeRise = sunRise( config.lat, config.lon )
timeSet = sunSet( config.lat, config.lon )
log.info( "Today's sun rise: %s" % fromts( timeRise ) )
log.info( "Today's sun set : %s" % fromts( timeSet ) )
else:
timeRise = 0
timeSet = 3e9 # jan, 2065
# Current time is before todays sun rise. Start reporting at the
# rise time and stop reporting at the set time.
if t0 < timeRise:
timeBeg = timeRise - config.timePad
timeEnd = timeSet + config.timePad
log.info( "Before sun rise, sleeping until %s" % fromts( timeBeg ) )
# Current time is after todays sun set. Start reporting at
# tomorrows rise time and stop reporting at tomorrows set time.
elif t0 > timeSet:
timeBeg = sunRise( config.lat, config.lon, +1 ) - config.timePad
timeEnd = sunSet( config.lat, config.lon, +1 ) + config.timePad
log.info( "After sun set, sleeping until %s" % fromts( timeBeg ) )
# Current time is between todays rise and set. Start reporting
# immediately and stop reporting at the set time.
else:
timeBeg = t0
timeEnd = timeSet + config.timePad
log.info( "Sun up, run until sunset %s" % fromts( timeEnd ) )
_initTimes( reports, timeBeg )
while True:
nextReport = min( reports, key=lambda x: x.nextT )
dt = nextReport.nextT - t0
if dt > 0:
log.info( "Sleeping %s sec for report %s" % ( dt, nextReport.lbl ) )
time.sleep( dt )
log.info( "Running report : %s" % nextReport.lbl )
try:
nextReport.func( client, linkArgs, config, log )
except:
log.exception( "Report failed to run" )
nextReport.nextT += nextReport.poll
t0 = time.time()
# Time is now after the end time for today.
if t0 > timeEnd:
# If the last report wasn't the largest one, run the largest
# report one last time. This gives us a final tally of
# energy production for example.
if nextReport != reports[0]:
reports[0].func( client, linkArgs, config, log )
timeBeg = sunRise( config.lat, config.lon, +1 ) - config.timePad
timeEnd = sunSet( config.lat, config.lon, +1 ) + config.timePad
_initTimes( reports, timeBeg )
log.info( "After sun set, sleeping until %s" % fromts( timeBeg ) )
#===========================================================================
def sunRise( lat, lon, dayOffset=0 ):
t = datetime.date.today() + datetime.timedelta( days=dayOffset )
a = astral.Astral()
utc = a.sunrise_utc( t, lat, lon )
# Convert the UTC datetime to a UNIX time stamp.
return calendar.timegm( utc.timetuple() )
#===========================================================================
def sunSet( lat, lon, dayOffset=0 ):
t = datetime.date.today() + datetime.timedelta( days=dayOffset )
a = astral.Astral()
utc = a.sunset_utc( t, lat, lon )
# Convert the UTC datetime to a UNIX time stamp.
return calendar.timegm( utc.timetuple() )
#===========================================================================
def msgPower( client, linkArgs, config, log ):
data = report.power( **linkArgs )
msg = _buildPowerMsg( data, log )
payload = json.dumps( msg )
log.info( "Publish: %s: %s" % ( config.mqttPower, payload ) )
client.publish( config.mqttPower, payload )
#===========================================================================
def msgEnergy( client, linkArgs, config, log ):
data = report.energy( **linkArgs )
msgs = [
# ( topic name, msg dict )
( config.mqttPower, _buildPowerMsg( data, log ) ),
( config.mqttEnergy, _buildEnergyMsg( data, log ) ),
]
for topic, data in msgs:
payload = json.dumps( data )
log.info( "Publish: %s: %s" % ( topic, payload ) )
client.publish( topic, payload )
#===========================================================================
def msgFull( client, linkArgs, config, log ):
data = report.full( **linkArgs )
msgs = [
# ( topic name, msg dict )
( config.mqttPower, _buildPowerMsg( data, log ) ),
( config.mqttEnergy, _buildEnergyMsg( data, log ) ),
( config.mqttFull, _buildFullMsg( data, log ) ),
]
for topic, data in msgs:
payload = json.dumps( data )
client.publish( topic, payload )
#===========================================================================
def _buildPowerMsg( data, log ):
msg = {
"time" : data["time"],
"acPower" : data["acPower"], # in W
"dcPower" : data["dcPower"], # in W
}
log.info( "AC power: %(acPower)s W", msg )
return msg
#===========================================================================
def _buildEnergyMsg( data, log ):
msg = {
"time" : data["time"],
"dailyEnergy" : data["dailyEnergy"] / 1000.0, # Wh -> kWh
"totalEnergy" : data["totalEnergy"] / 1000.0, # Wh -> kWh
}
log.info( "Daily energy: %(dailyEnergy)s kWh", msg )
return msg
#===========================================================================
def _buildFullMsg( data, log ):
return data.__dict__
#===========================================================================
def _initTimes( reports, timeBeg ):
# Set the first time to be the begin time + the polling interval.
for r in reports:
r.nextT = timeBeg + r.poll
# Run the biggest priority report once right at startup.
reports[0].nextT = timeBeg
#===========================================================================

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
class FakeSocket:
def __init__( self, reply ):
self.sent = None
self.reply = reply
self.closed = False
def send( self, bytes ):
self.sent = bytes
def recv( self, bufLen ):
return self.reply
def close( self ):
self.closed = True

View File

@@ -1,51 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestAcMaxPower ( T.util.test.Case ) :
def test_acMaxPower( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 7A 00 10 60 65 1E 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
0B 80 01 02 00 51 01 00 00 00
03 00 00 00 01 1E 41 00 82 22
AF 53 88 13 00 00 88 13 00 00
88 13 00 00 88 13 00 00 01 00
00 00 01 1F 41 00 82 22 AF 53
88 13 00 00 88 13 00 00 00 00
00 00 88 13 00 00 00 00 00 00
01 20 41 00 82 22 AF 53 88 13
00 00 88 13 00 00 00 00 00 00
88 13 00 00 00 00 00 00 00 00
00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.acMaxPower()
l.decode = False
buf, decoder = l.acMaxPower()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
acMaxPower1 = 5000.0,
acMaxPower2 = 5000.0,
acMaxPower3 = 5000.0,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,51 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestAcPower ( T.util.test.Case ) :
def test_acPower( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 7A 00 10 60 65 1E 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
10 80 01 02 00 51 07 00 00 00
09 00 00 00 01 40 46 40 86 22
AF 53 B5 07 00 00 B5 07 00 00
B5 07 00 00 B5 07 00 00 01 00
00 00 01 41 46 40 86 22 AF 53
B5 07 00 00 B5 07 00 00 B5 07
00 00 B5 07 00 00 01 00 00 00
01 42 46 40 86 22 AF 53 00 00
00 80 00 00 00 80 00 00 00 80
00 00 00 80 01 00 00 00 00 00
00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.acPower()
l.decode = False
buf, decoder = l.acPower()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
acPower1 = 1973.0,
acPower2 = 1973.0,
acPower3 = 0.0,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,44 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestAcTotalEnergy ( T.util.test.Case ) :
def test_acTotalEnergy( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 46 00 10 60 65 11 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
0C 80 01 02 00 54 00 00 00 00
01 00 00 00 01 01 26 00 85 22
AF 53 D0 6A 09 00 00 00 00 00
01 22 26 00 82 22 AF 53 2F 3B
00 00 00 00 00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.acTotalEnergy()
l.decode = False
buf, decoder = l.acTotalEnergy()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
totalEnergy = 617168.0,
dailyEnergy = 15151.0,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,43 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestAcTotalPower ( T.util.test.Case ) :
def test_acTotalPower( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 42 00 10 60 65 10 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
12 80 01 02 00 51 00 00 00 00
00 00 00 00 01 3F 26 40 86 22
AF 53 6A 0F 00 00 6A 0F 00 00
6A 0F 00 00 6A 0F 00 00 01 00
00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.acTotalPower()
l.decode = False
buf, decoder = l.acTotalPower()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
acPower = 3946.0,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,73 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestAcVoltage ( T.util.test.Case ) :
def test_acVoltage( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 01 22 00 10 60 65 48 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
11 80 01 02 00 51 0A 00 00 00
12 00 00 00 01 48 46 00 86 22
AF 53 A6 2F 00 00 A6 2F 00 00
A6 2F 00 00 A6 2F 00 00 01 00
00 00 01 49 46 00 86 22 AF 53
9B 2F 00 00 9B 2F 00 00 9B 2F
00 00 9B 2F 00 00 01 00 00 00
01 4A 46 00 86 22 AF 53 FF FF
FF FF FF FF FF FF FF FF FF FF
FF FF FF FF 01 00 00 00 01 4B
46 00 86 22 AF 53 43 5F 00 00
43 5F 00 00 43 5F 00 00 43 5F
00 00 01 00 00 00 01 4C 46 00
86 22 AF 53 FF FF FF FF FF FF
FF FF FF FF FF FF FF FF FF FF
01 00 00 00 01 4D 46 00 86 22
AF 53 FF FF FF FF FF FF FF FF
FF FF FF FF FF FF FF FF 01 00
00 00 01 50 46 00 86 22 AF 53
35 3F 00 00 35 3F 00 00 35 3F
00 00 35 3F 00 00 01 00 00 00
01 51 46 00 86 22 AF 53 35 3F
00 00 35 3F 00 00 35 3F 00 00
35 3F 00 00 01 00 00 00 01 52
46 00 86 22 AF 53 FF FF FF FF
FF FF FF FF FF FF FF FF FF FF
FF FF 01 00 00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.acVoltage()
l.decode = False
buf, decoder = l.acVoltage()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
acVoltage1 = 121.98,
acVoltage2 = 121.87,
acVoltage3 = 0,
acGridVoltage = 243.87,
unknown1 = 0,
unknown2 = 0,
acCurrent1 = 16.181,
acCurrent2 = 16.181,
acCurrent3 = 0,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,47 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestDcPower ( T.util.test.Case ) :
def test_dcPower( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 5E 00 10 60 65 17 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
0E 80 01 02 80 53 00 00 00 00
01 00 00 00 01 1E 25 40 85 22
AF 53 13 08 00 00 13 08 00 00
13 08 00 00 13 08 00 00 01 00
00 00 02 1E 25 40 85 22 AF 53
21 08 00 00 21 08 00 00 21 08
00 00 21 08 00 00 01 00 00 00
00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.dcPower()
l.decode = False
buf, decoder = l.dcPower()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
dcPower1 = 2067.0,
dcPower2 = 2081.0,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,54 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestDcVoltage ( T.util.test.Case ) :
def test_dcVoltage( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 96 00 10 60 65 25 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
0F 80 01 02 80 53 02 00 00 00
05 00 00 00 01 1F 45 40 85 22
AF 53 B1 5E 00 00 B1 5E 00 00
B1 5E 00 00 B1 5E 00 00 01 00
00 00 02 1F 45 40 85 22 AF 53
D5 5E 00 00 D5 5E 00 00 D5 5E
00 00 D5 5E 00 00 01 00 00 00
01 21 45 40 85 22 AF 53 51 21
00 00 51 21 00 00 51 21 00 00
51 21 00 00 01 00 00 00 02 21
45 40 85 22 AF 53 7D 21 00 00
7D 21 00 00 7D 21 00 00 7D 21
00 00 01 00 00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.dcVoltage()
l.decode = False
buf, decoder = l.dcVoltage()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
dcVoltage1 = 242.41,
dcVoltage2 = 242.77,
dcCurrent1 = 8.529,
dcCurrent2 = 8.573,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,43 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestGridFrequency ( T.util.test.Case ) :
def test_gridFrequency( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 42 00 10 60 65 10 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
13 80 01 02 00 51 13 00 00 00
13 00 00 00 01 57 46 00 86 22
AF 53 6C 17 00 00 6C 17 00 00
6C 17 00 00 6C 17 00 00 01 00
00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.gridFrequency()
l.decode = False
buf, decoder = l.gridFrequency()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
frequency = 59.96,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,44 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestGridRelayStatus ( T.util.test.Case ) :
def test_GridRelayStatus( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 4E 00 10 60 65 13 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
0A 80 01 02 80 51 06 00 00 00
06 00 00 00 01 64 41 08 85 22
AF 53 33 00 00 01 37 01 00 00
FD FF FF 00 FE FF FF 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.gridRelayStatus()
l.decode = False
buf, decoder = l.gridRelayStatus()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
gridStatus = 'Closed',
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,58 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestInfo ( T.util.test.Case ) :
def test_info( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 C6 00 10 60 65 31 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
05 80 01 02 00 58 00 00 00 00
03 00 00 00 01 1E 82 10 82 19
AF 53 53 4E 3A 20 31 39 31 33
30 30 36 30 34 38 00 00 10 00
00 00 10 00 00 00 00 00 00 00
00 00 00 00 01 1F 82 08 82 19
AF 53 41 1F 00 01 42 1F 00 00
FE FF FF 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 01 20 82 08 82 19
AF 53 EE 23 00 00 EF 23 00 00
F0 23 00 00 F1 23 00 01 F2 23
00 00 F3 23 00 00 F4 23 00 00
F5 23 00 00 01 20 82 08 82 19
AF 53 FE FF FF 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False, raw=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.info()
l.decode = False
buf, decoder = l.info()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
name = 'SN: 1913006048',
model = 'SB 5000TLUS-22',
type = 'Solar Inverter',
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,44 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestOperationTime ( T.util.test.Case ) :
def test_OperationTime( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 46 00 10 60 65 11 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
0D 80 01 02 00 54 03 00 00 00
04 00 00 00 01 2E 46 00 85 22
AF 53 00 FA 0E 00 00 00 00 00
01 2F 46 00 85 22 AF 53 42 97
0E 00 00 00 00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.operationTime()
l.decode = False
buf, decoder = l.operationTime()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
operationTime = 981504.0,
feedTime = 956226.0,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env python
import tHome as T
#T.util.log.get( 'sma', 10, 'stdout' )
r = T.sma.report.energy( ip="192.168.1.14" )
print "=================="
print r

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env python
import tHome as T
#T.util.log.get( 'sma', 10, 'stdout' )
r = T.sma.report.full( ip="192.168.1.14" )
print "=================="
print r

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env python
import tHome as T
#T.util.log.get( 'sma', 10, 'stdout' )
r = T.sma.report.power( ip="192.168.1.14" )
print "=================="
print r

View File

@@ -1,44 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestStatus ( T.util.test.Case ) :
def test_status( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 4E 00 10 60 65 13 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
08 80 01 02 80 51 00 00 00 00
00 00 00 00 01 48 21 08 82 22
AF 53 23 00 00 00 2F 01 00 00
33 01 00 01 C7 01 00 00 FE FF
FF 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.status()
l.decode = False
buf, decoder = l.status()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
status = 'Ok',
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,43 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestTemp ( T.util.test.Case ) :
def test_temp( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 42 00 10 60 65 10 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
09 80 01 02 00 52 00 00 00 00
00 00 00 00 01 77 23 40 44 22
AF 53 AC 1B 00 00 B6 1B 00 00
B2 1B 00 00 B2 1B 00 00 01 00
00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.temperature()
l.decode = False
buf, decoder = l.temperature()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
temperature = 70.84,
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,44 +0,0 @@
import unittest
from FakeSocket import FakeSocket
import tHome as T
#===========================================================================
#===========================================================================
class TestVersion ( T.util.test.Case ) :
def test_version( self ):
reply = """
53 4D 41 00 00 04 02 A0 00 00
00 01 00 4E 00 10 60 65 13 90
7D 00 AB 94 40 3B 00 A0 F7 00
E0 27 06 72 00 00 00 00 00 00
04 80 01 02 00 58 07 00 00 00
07 00 00 00 01 34 82 00 94 19
AF 53 00 00 00 00 00 00 00 00
FE FF FF FF FE FF FF FF 04 06
60 02 04 06 60 02 00 00 00 00
00 00 00 00 00 00 00 00
"""
l = T.sma.Link( "fake", connect=False )
try:
l.socket = FakeSocket( T.util.hex.toBytes( reply ) )
o1 = l.version()
l.decode = False
buf, decoder = l.version()
o2 = decoder( buf )
finally:
l.socket = None
right = T.util.Data(
version = '02.60.06.R',
)
print o1
for k in right.keys():
r = right[k]
self.eq( getattr( o1, k ), r, k )
self.eq( getattr( o2, k ), r, k )
#===========================================================================

View File

@@ -1,149 +0,0 @@
#===========================================================================
#
# Thermostat class
#
#===========================================================================
import json
import requests
import time
from .. import util
#===========================================================================
class Thermostat:
# Temperature mode
tmode = { 0 : "off", 1 : "heat", 2 : "cool", 3 : "auto" }
# Active heating/cooling flag.
tstate = { 0 : "off", 1 : "heat", 2 : "cool" }
# Fan mode
fmode = { 0 : "auto", 1 : "circulate", 2 : "on" }
# Fan on/off flag.
fstate = { 0 : "off", 1 : "on" }
#------------------------------------------------------------------------
def __init__( self, label, host, mqttTempTopic, mqttModeTopic,
mqttStateTopic, mqttSetTopic ):
self.label = label
self.host = host
self.mqttTempTopic = mqttTempTopic
self.mqttModeTopic = mqttModeTopic
self.mqttStateTopic = mqttStateTopic
self.mqttSetTopic = mqttSetTopic
self._log = util.log.get( "thermostat" )
self._lastStatus = None
#------------------------------------------------------------------------
def status( self ):
self._log.info( "%s:%s Getting status" % ( self.host, self.label ) )
url = "http://%s/tstat" % self.host
r = requests.get( url )
self._log.info( "%s:%s Received status %s" % ( self.host, self.label,
r.status_code ) )
if r.status_code != requests.codes.ok:
self._lastStatus = None
e = util.Error( r.text )
e.add( "Error requesting status from the thermostat '%s' at %s" %
( self.label, self.host ) )
raise e
d = r.json()
tempControl = 'auto'
if d["override"]:
tempControl = 'override'
elif d['hold']:
tempControl = 'hold'
m = util.Data(
# Unix time.
time = time.time(),
# Current temperature at thermostat.
temperature = d["temp"],
# Thermostat mode (off, heating, cooling
tempMode = self.tmode[ d["tmode" ] ],
# Is the hvac currently running?
tempState = self.tstate[ d["tstate" ] ],
# Fan mode (auto, on)
fanMode = self.fmode[ d["fmode" ] ],
# Is the fan current on?
fanState = self.fstate[ d["fstate" ] ],
# program or override mode
tempControl = tempControl,
)
if "t_heat" in d:
m.target = d["t_heat"]
else:
m.target = d["t_cool"]
self._log.info( "%s:%s Received: %s" % ( self.host, self.label, m ) )
self._lastStatus = m
return m
#------------------------------------------------------------------------
def messages( self, data=None ):
data = data if data is not None else self._lastStatus
if not data:
return []
s = self._lastStatus
msgs = [
# tuple of ( topic, message )
( self.mqttTempTopic, {
'time' : s.time,
'temp' : s.temperature,
} ),
( self.mqttModeTopic, {
'time' : s.time,
'sys' : s.tempMode,
'fan' : s.fanMode,
'temp' : s.tempControl,
} ),
( self.mqttStateTopic, {
'time' : s.time,
'active' : s.tempState,
'fan' : s.fanState,
'target' : s.target,
'temp' : s.temperature,
} ),
]
return msgs
#------------------------------------------------------------------------
def processSet( self, client, msg ):
data = json.loads( msg.payload )
replyTopic = msg.topic + "/" + data['id']
status = None
if '/temp' in msg.topic:
value = data['temp']
# TODO: set temperature
status = 0
msg = ""
elif '/mode' in msg.topic:
sysMode = data['sys'] # off/heat/cool/auto
fanMode = data['fan'] # on/auto
# TODO: set mode
status = 0
msg = ""
if status is not None:
reply = { time : time.time(), error : status, msg : msg }
payload = json.dumps( reply )
client.publish( replyTopic, payload )
#------------------------------------------------------------------------
def __str__( self ):
return """Thermostat(
Label = '%s',
Host = '%s',
)""" % ( self.label, self.host )
#------------------------------------------------------------------------

View File

@@ -1,22 +0,0 @@
#===========================================================================
#
# Radio thermostat module.
#
#===========================================================================
__doc__ = """Radio Thermostat package.
Used for reading radio-thermostat brand WIFI thermostats.
Logging object name: tHome.thermostat
"""
#===========================================================================
#===========================================================================
from . import config
from .Thermostat import Thermostat
#===========================================================================

View File

@@ -1,64 +0,0 @@
#===========================================================================
#
# Config file
#
#===========================================================================
__doc__ = """Config file parsing.
"""
from .. import util
from ..util import config as C
from .Thermostat import Thermostat
#===========================================================================
# Config file section name and defaults.
configEntries = [
# ( name, converter function, default value )
C.Entry( "logFile", util.path.expand ),
C.Entry( "logLevel", int, 20 ), # INFO
C.Entry( "thermostats", list ),
]
thermostatEntries = [
# ( name, converter function, default value )
C.Entry( "host", str ),
C.Entry( "label", str ),
C.Entry( "mqttTempTopic", str ),
C.Entry( "mqttModeTopic", str ),
C.Entry( "mqttStateTopic", str ),
C.Entry( "mqttSetTopic", str ),
]
#===========================================================================
def parse( configDir, configFile='thermostat.py' ):
m = C.readAndCheck( configDir, configFile, configEntries )
# Replace the thermostat dict inputs with Thermostat objecdts.
m.thermostats = parseThermostats( m.thermostats )
return m
#===========================================================================
def parseThermostats( entries ):
assert( len( entries ) > 0 )
thermostats = []
for e in entries:
C.check( e, thermostatEntries )
thermostats.append( Thermostat( **e ) )
return thermostats
#===========================================================================
def log( config, logFile=None ):
if not logFile:
logFile = config.logFile
return util.log.get( "thermostat", config.logLevel, logFile )
#===========================================================================

View File

@@ -1,19 +0,0 @@
#===========================================================================
#
# Weather Underground web site access
#
#===========================================================================
__doc__ = """Communicate with the Weather Underground web site.
Allows upload of weather data to a personal weather station.
"""
#===========================================================================
from . import cmdLine
from . import config
from . import start
#===========================================================================

View File

@@ -1,52 +0,0 @@
#===========================================================================
#
# Command line processing
#
#===========================================================================
import argparse
import numpy as np
from .. import broker
from . import config
from . import start
#===========================================================================
def run( args ):
"""Parse command line arguments to upload weather data.
= INPUTS
- args [str]: List of command line arguments. [0] should be the
program name.
"""
p = argparse.ArgumentParser( prog=args[0],
description="SMA inverter reader" )
p.add_argument( "-c", "--configDir", metavar="configDir",
default="/var/config/tHome",
help="T-Home configuration directory." )
p.add_argument( "-l", "--log", metavar="logFile",
default=None, help="Logging file to use. Input 'stdout' "
"to log to the screen." )
p.add_argument( "--debug", default=False, action="store_true",
help="Debugging - no sending, just print" )
c = p.parse_args( args[1:] )
if c.debug:
c.log = "stdout"
# Parse the sma and broker config files.
cfg = config.parse( c.configDir )
log = config.log( cfg, c.log )
if c.debug:
log.setLevel( 10 )
# Create the MQTT client and connect it to the broker.
client = broker.connect( c.configDir, log )
# Numpy reports invalid errors when dealing w/ nans which don't
# matter to this algorithm.
with np.errstate( invalid='ignore' ):
start.start( cfg, client, debug=c.debug )
#===========================================================================

View File

@@ -1,47 +0,0 @@
#===========================================================================
#
# Config file
#
#===========================================================================
__doc__ = """Config file parsing.
"""
from .. import util
from ..util import config as C
#===========================================================================
# Config file section name and defaults.
configEntries = [
# ( name, converter function, default value )
C.Entry( "uploadUrl", str ),
C.Entry( "id", str ),
C.Entry( "password", str ),
C.Entry( "poll", int, 120 ),
C.Entry( "maxRate", int, 10 ),
C.Entry( "digits", int, 2 ),
C.Entry( "mqttWind", str, None ),
C.Entry( "mqttTemp", list, [] ),
C.Entry( "mqttRain", str, None ),
C.Entry( "mqttBarometer", str, None ),
C.Entry( "mqttHumidity", str, None ),
C.Entry( "logFile", util.path.expand ),
C.Entry( "logLevel", int, 20 ), # INFO
]
#===========================================================================
def parse( configDir, configFile='weatherUnderground.py' ):
return C.readAndCheck( configDir, configFile, configEntries )
#===========================================================================
def log( config, logFile=None ):
if not logFile:
logFile = config.logFile
return util.log.get( "weatherUnderground", config.logLevel, logFile )
#===========================================================================

View File

@@ -1,403 +0,0 @@
#===========================================================================
#
# Main report processing
#
#===========================================================================
import logging
import requests
import datetime
import threading
import numpy as np
import time
from StringIO import StringIO
from .. import util
#===========================================================================
class CircularTimeBuf:
"""Circular buffer class.
This stores data in a numpy array as a circular buffer that covers
a set amount of time. When data is added (with a time tag), it
will automatically erase any data older than the input time length.
This allows for fast and easy computations involving the last n
seconds of data regardless of the rate that data is seen.
"""
def __init__( self, timeLen, maxRate, label=None, log=None ):
self.label = label if label is not None else ""
self.log = log
numEntries = int( timeLen / maxRate )
self._dt = timeLen
self._len = numEntries
# Index of the last entry added to the buffer. -1 is used to
# indicate that no data is present.
self._lastIdx = -1
# Create an array of NaN values. v[0] is the array of times,
# v[1] is the array of values.
self.v = np.full( ( 2, self._len ), np.nan )
#--------------------------------------------------------------------------
def __nonzero__( self ):
return self._lastIdx != -1
#--------------------------------------------------------------------------
def append( self, t, v ):
# Remove any data older than t-self._dt
self.updateTo( t )
idx = self._nextIdx( self._lastIdx )
self.v[0][idx] = t
self.v[1][idx] = v
self._lastIdx = idx
# Debugging output
if self.log and self.log.isEnabledFor( logging.DEBUG ):
s = StringIO()
print >> s, "%s record time: %.1f\n" % ( self.label, t )
if self._lastIdx != -1:
i = self._lastIdx
while not np.isnan( self.v[0][i] ):
print >> s, " %.1f %.1f " % ( self.v[0][i], self.v[1][i] )
i = self._prevIdx( i )
self.log.debug( s.getvalue().strip() )
#--------------------------------------------------------------------------
def mean( self, t=None ):
# Remove any data older than t-self._dt
if t:
self.updateTo( t )
# No data in the buffer.
if self._lastIdx == -1:
return None
# Compute the mean value and ignore any nans.
return np.nanmean( self.v[1] )
#--------------------------------------------------------------------------
def sum( self, t=None ):
# Remove any data older than t-self._dt
if t:
self.updateTo( t )
# No data in the buffer.
if self._lastIdx == -1:
return None
# Sum all the values and ignore any nans.
return np.nansum( self.v[1] )
#--------------------------------------------------------------------------
def max( self, t=None ):
# Remove any data older than t-self._dt
if t:
self.updateTo( t )
# No data in the buffer.
if self._lastIdx == -1:
return None
# Compute the max value and ignore any nans.
return np.nanmax( self.v[1] )
#--------------------------------------------------------------------------
def min( self, t=None ):
# Remove any data older than t-self._dt
if t:
self.updateTo( t )
# No data in the buffer.
if self._lastIdx == -1:
return None
# Compute the max value and ignore any nans.
return np.nanmin( self.v[1] )
#--------------------------------------------------------------------------
def updateTo( self, time ):
if self._lastIdx == -1:
return
# Delete any entries that are more than self._dt before the
# input time.
tBeg = time - self._dt
# Find the indeces where the time is too old. This returns a
# tuple of len 1 so it can be used as an array index below.
i = np.where( self.v[0] < tBeg )
# Reset those values to nan
if i:
# Get an index of the current nan values.
nans = np.where( np.isnan( self.v[0] ) == True )
self.v[0][i] = np.nan
self.v[1][i] = np.nan
# If the number of existing nan values and old values is the
# length of the array, reset lastIdx to indicate there is no
# data. This eliminates warnings about all-nan axis when
# doing computations.
if ( len( nans[0] ) + len( i[0] ) ) == self._len:
self._lastIdx = -1
#--------------------------------------------------------------------------
def _nextIdx( self, index ):
nextIdx = index + 1
if nextIdx == self._len:
nextIdx = 0
return nextIdx
#--------------------------------------------------------------------------
def _prevIdx( self, index ):
prevIdx = index - 1
if prevIdx < 0:
prevIdx = self._len - 1
return prevIdx
#--------------------------------------------------------------------------
#===========================================================================
class Reader:
def __init__( self, config, log ):
self.config = config
self.log = log
# Fields that use config.poll are used to return the average
# value over the poll interval. That decouples the interval we
# get messages in and the upload reporting interval. Some
# fields have fixed intervals that weather underground requires
# (like rain).
self.temps = []
self.tempKeys = []
for i in range( len( config.mqttTemp ) ):
self.temps.append( CircularTimeBuf( config.poll, config.maxRate,
"Temp %d" % i, self.log ) )
if i == 0:
self.tempKeys.append( 'tempf' )
else:
self.tempKeys.append( 'temp%df' % i )
self.humidity = CircularTimeBuf( config.poll, config.maxRate,
"Humidity", self.log )
self.barometer = CircularTimeBuf( config.poll, config.maxRate,
"Barometer", self.log )
# Uploaded wind speed/dir are the average over the poll interval
# Gust values will be the maximum value over the poll interval.
self.windSpeed = CircularTimeBuf( config.poll, config.maxRate,
"Wind Speed", self.log )
# Average direction has to be computed by averaging the vector
# directions and then turning that back to an angle. See:
# http://www.webmet.com/met_monitoring/622.html
# A weighted version is shown here - but I'm not doing that
# because the speed and direction messages are separate - so
# this is just the averaged direction.
# east = speed * sin( dir )
# north = speed * cos( dir )
# ve = -1/n sum( east )
# vn = -1/n sum( north )
# Average dir = arctan( ve/vn ) +/- 180
self.windEast = CircularTimeBuf( config.poll, config.maxRate,
"Wind Dir East", self.log )
self.windNorth = CircularTimeBuf( config.poll, config.maxRate,
"Wind Dir North", self.log )
# Accumulate rain for the day. Store the current date when the
# first reading shows up. If the date changes, then we reset
# the accumulation.
self.rainDate = None
self.rainDayTotal = 0.0
# Accumulate rain for the last hour and allow at least 15
# seconds between messages (acurite publishes every 36 seconds
# so this is fine).
self.rainHour = CircularTimeBuf( 3600.0, 15, "Rain Hour", self.log )
# MQTT will be pushing data to use and we'll be publishing it
# out so we need to lock when accessing the numpy matrix.
self.lock = threading.Lock()
#--------------------------------------------------------------------------
def readMsg( self, client, userData, msg ):
with self.lock:
topic = msg.topic
data = util.json.loads( msg.payload )
self.log.info( "Read %s: %s" % ( topic, data ) )
time = data['time']
for i in range( len( self.config.mqttTemp ) ):
if topic == self.config.mqttTemp[i]:
self.temps[i].append( time, data['temperature' ] )
break
else:
if topic == self.config.mqttHumidity:
self.humidity.append( time, data['humidity' ] )
elif topic == self.config.mqttBarometer:
self.barometer.append( time, data['pressure' ] )
elif topic == self.config.mqttRain:
today = datetime.date.today()
# Reset the rain if the local day changes.
if self.rainDate != today:
self.rainDayTotal = 0.0
self.rainDate = today
self.log.info( "Update rain date to %s" % today )
self.rainDayTotal += data['rain']
self.rainHour.append( time, data['rain'] )
elif topic == self.config.mqttWindSpeed:
self.windSpeed.append( time, data['speed'] )
elif topic == self.config.mqttWindDir:
# Note: if we multiple the trig terms by the speed, it
# would give us a weighted direction. But that
# requires matching the direction msg time with the
# speed msg time and requires they both exist. So it's
# easier to just average the directions.
angle = np.deg2rad( data['direction'] )
self.windEast.append( time, np.sin( angle ) )
self.windNorth.append( time, np.cos( angle ) )
#--------------------------------------------------------------------------
def updatePayload( self, payload ):
with self.lock:
t = time.time()
dt = datetime.datetime.utcnow()
payload['dateutc'] = dt.strftime( '%Y-%m-%d %H:%M:%S' )
# Only update the payload dictionary if the value from the
# circular buffer isn't None. That can happen if we don't
# have any data for the requested interval. Round the result
# to a few digits to keep the upload message smaller.
def updateDict( key, value, digits=self.config.digits ):
if value is not None:
payload[key] = round( value, digits )
haveData = False
if self.windSpeed:
haveData = True
updateDict( 'windspeedmph', self.windSpeed.mean( t ) )
updateDict( 'windgustmph', self.windSpeed.max( t ) )
if self.windEast:
# Average the wind direction using vector math.
# See: http://www.webmet.com/met_monitoring/622.html
avgEast = - self.windEast.mean( t )
avgNorth = - self.windNorth.mean( t )
windDir = np.rad2deg( np.arctan2( avgEast, avgNorth ) )
# Shift to the correct quadrant
if windDir < 180:
windDir += 180
else:
windDir -= 180
updateDict( 'winddir', windDir )
if self.humidity:
haveData = True
updateDict( 'humidity', self.humidity.mean( t ) )
if self.barometer:
haveData = True
updateDict( 'baromin', self.barometer.mean( t ) )
if self.rainDate is not None:
haveData = True
updateDict( 'dailyrainin', self.rainDayTotal, 3 )
updateDict( 'rainin', self.rainHour.sum( t ), 3 )
for i in range( len( self.temps ) ):
if self.temps[i]:
haveData = True
updateDict( self.tempKeys[i], self.temps[i].mean( t ) )
return haveData
#--------------------------------------------------------------------------
#===========================================================================
def start( config, client, debug=False ):
fromts = datetime.datetime.fromtimestamp
log = util.log.get( "weatherUnderground" )
# URL arguments to send.
payloadBase = {
'action' : 'updateraw',
'ID' : config.id,
'PASSWORD' : config.password,
'dateutc' : None, # 'YYYY-MM-DD HH:MM:SS'
# 'winddir' : 0-360
# 'windspeedmph' : speed in mph
# 'windgustmph' : gust in mph
# 'humidity' : 0-100%
# 'tempf' : temperature F
# 'temp2f' : temperature F
# 'rainin' : inches of rain over the last hour
# 'dailyrainin' : inches of rain over the local day
# 'baromin' : barometric pressure in inches
}
payload = payloadBase.copy()
# Create a reader to process weather mes sages.
reader = Reader( config, log )
client.on_message = reader.readMsg
# Subscribe to all the weather topics we need to upload.
for topic in config.mqttTemp:
client.subscribe( topic )
client.subscribe( config.mqttHumidity )
client.subscribe( config.mqttBarometer )
client.subscribe( config.mqttRain )
client.subscribe( config.mqttWindSpeed )
client.subscribe( config.mqttWindDir )
# Start the MQTT as a background thread. This way we can run our
# upload process in the rest of the code.
client.loop_start()
while True:
# Fill in the current values into the payload dict.
haveData = reader.updatePayload( payload )
if haveData:
try:
log.info( "Uploading: %s" % payload )
# Send the HTTP request.
r = requests.get( config.uploadUrl, params=payload )
log.debug( "URL: %s" % r.url )
if r.text.strip() != "success":
log.error( "WUG response: '%s'" % r.text )
except:
log.exception( "Upload failed to run" )
# Clear the payload back to the minimum set of fields.
payload = payloadBase.copy()
else:
log.info( "Ignoring send opportunity - no data" )
# Sleep until the next time we should upload.
time.sleep( config.poll )
#===========================================================================

View File

@@ -1,71 +0,0 @@
import numpy as np
import warnings
import tHome.weatherUnderground.start as S
buf = S.CircularTimeBuf( 60, 10 )
print "================="
print "Empty"
print "================="
print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() )
print "================="
buf.append( 10, 1 )
print buf.v
print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() )
print
buf.append( 20, 2 )
print buf.v
print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() )
print
buf.append( 30, 3 )
print buf.v
print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() )
print
buf.append( 40, 4 )
print buf.v
print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() )
print
buf.append( 50, 5 )
print buf.v
print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() )
print
buf.append( 60, 6 )
print buf.v
print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() )
print
buf.append( 70, 7 )
print buf.v
print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() )
print
print "================="
buf.append( 120, 12 )
print buf.v
print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() )
print "================="
buf.append( 130, 13 )
buf.append( 140, 14 )
buf.append( 150, 15 )
buf.append( 160, 16 )
buf.append( 170, 17 )
print buf.v
print
print "Update to t=200"
print "Min/Max: %s / %s Avg: %s" % ( buf.min(200), buf.max(200), buf.mean(200) )
print buf.v
print "================="
print "Update to t=500"
buf.updateTo( 500 )
print buf.v
print "Min:",buf.min( 500 )