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:
@@ -6,11 +6,8 @@ LABEL version="1.0"
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
ENV PYTHONPATH=/app/src/python
|
||||
RUN pip install --upgrade pip
|
||||
RUN pip install -r requirements.txt
|
||||
RUN echo "http://dl-8.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories
|
||||
RUN apk --no-cache --update-cache add gcc python-dev build-base
|
||||
RUN ln -s /usr/include/locale.h /usr/include/xlocale.h
|
||||
RUN pip install --no-cache-dir numpy
|
||||
RUN rm -rf /var/cache/apk
|
||||
|
||||
CMD ["/app/src/bin/tHome-eagle.py", "-c", "/app/src/conf"]
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script configures the eb/ip tables rules to redirect traffic
|
||||
# from the bridge to a different port on the local machine. It gets
|
||||
# run by adding this line to /etc/network/interfaces
|
||||
#
|
||||
# pre-up /home/ted/proj/tHome/bin/acurite-redirect.sh
|
||||
#
|
||||
|
||||
# Redirect traffic on the bridge to port 22041 which must match the port
|
||||
# specified in tHome/conf/acurite.py.
|
||||
PORT=22041
|
||||
|
||||
# Tell the bridge to push the packet to iptables.
|
||||
ebtables -t broute -A BROUTING -p IPv4 --ip-protocol 6 --ip-destination-port 80 -j redirect --redirect-target ACCEPT
|
||||
|
||||
# Redirect the packet to the other port.
|
||||
iptables -t nat -A PREROUTING -i br0 -p tcp --dport 80 -j REDIRECT --to-port $PORT
|
||||
@@ -1,90 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Eagle posting server
|
||||
#
|
||||
#===========================================================================
|
||||
|
||||
__doc__ = """
|
||||
Starts a small web server to read packets sent from an Acurite Bridgek.
|
||||
|
||||
The Acurite must be redirected to post messages to server instead of
|
||||
it's main server. This assumes the Bridge is connected to a raspberry
|
||||
pi using a USB network adaptor with it's network bridged to the main
|
||||
network. NOTE: The last port number in the iptables command must
|
||||
match the port configured in conf/acurite.py for the acurite web
|
||||
server.
|
||||
|
||||
ebtables -t broute -A BROUTING -p IPv4 --ip-protocol 6 --ip-destination-port 80 -j redirect --redirect-target ACCEPT
|
||||
iptables -t nat -A PREROUTING -i br0 -p tcp --dport 80 -j REDIRECT --to-port 22041
|
||||
|
||||
Scripts uses the tHome.acurite package to decode the bridge posts and
|
||||
converts them to JSON dictionaries which get sent out as MQTT
|
||||
messages.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import bottle as B
|
||||
import sys
|
||||
import json
|
||||
import tHome as T
|
||||
|
||||
#===========================================================================
|
||||
@B.post( '/' )
|
||||
@B.post( '/messages/' )
|
||||
def bridge_post():
|
||||
content = B.request.body.read( B.request.content_length )
|
||||
|
||||
log.info( "Read: %s" % content )
|
||||
|
||||
# Convert the line to messages. Returns a list of tuples of
|
||||
# ( topic, dict ).
|
||||
msgs = T.acurite.cmdLine.process( cfg, content, sensorMap )
|
||||
|
||||
# Send the messages out.
|
||||
for topic, data in msgs:
|
||||
log.info( "Publish: %s: %s" % ( topic, data ) )
|
||||
|
||||
payload = json.dumps( data )
|
||||
client.publish( topic, payload )
|
||||
|
||||
# Standard acurite web site reply - found by watching traffic to
|
||||
# the acurite web site.
|
||||
return { "success" : 1, "checkversion" : "126" }
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Main applications script
|
||||
#
|
||||
#===========================================================================
|
||||
|
||||
p = argparse.ArgumentParser( prog=sys.argv[0],
|
||||
description="T-Home Acurite Server" )
|
||||
p.add_argument( "-c", "--configDir", metavar="configDir",
|
||||
default="/etc/tHome",
|
||||
help="Configuration file 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( sys.argv[1:] )
|
||||
|
||||
# Parse the eagle config file.
|
||||
cfg = T.acurite.config.parse( c.configDir )
|
||||
log = T.acurite.config.log( cfg, c.log )
|
||||
|
||||
# Create a sensor map from the configuration file.
|
||||
sensorMap = {}
|
||||
for s in cfg.sensors:
|
||||
sensorMap[s.id] = s
|
||||
|
||||
# Create the MQTT client and connect it to the broker.
|
||||
client = T.broker.connect( c.configDir, log )
|
||||
|
||||
# Start the MQTT as a background thread. This way we can run the web
|
||||
# server as the main thread here.
|
||||
client.loop_start()
|
||||
|
||||
log.info( "Starting web server at port %d" % cfg.httpPort )
|
||||
B.run( host='0.0.0.0', port=cfg.httpPort, quiet=True )
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import tHome.sma
|
||||
|
||||
tHome.sma.cmdLine.run( sys.argv )
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Radio thermostats reader
|
||||
#
|
||||
#===========================================================================
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import tHome as T
|
||||
|
||||
#===========================================================================
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Main applications script
|
||||
#
|
||||
#===========================================================================
|
||||
|
||||
p = argparse.ArgumentParser( prog=sys.argv[0],
|
||||
description="T-Home Thermostats" )
|
||||
p.add_argument( "-c", "--configDir", metavar="configDir",
|
||||
default="/etc/tHome",
|
||||
help="Configuration file 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( sys.argv[1:] )
|
||||
|
||||
# Parse the thermostat config file.
|
||||
cfg = T.thermostat.config.parse( c.configDir )
|
||||
log = T.thermostat.config.log( cfg, c.log )
|
||||
|
||||
# Create the MQTT client and connect it to the broker.
|
||||
client = T.broker.connect( c.configDir, log )
|
||||
|
||||
# Handle set messages being set to the thermostats.
|
||||
def on_message( client, userData, msg ):
|
||||
for t in cfg.thermostats:
|
||||
if mqtt.topic_matches_sub( t.mqttSetTopic, msg.topic ):
|
||||
t.processSet( client, msg )
|
||||
return
|
||||
|
||||
client.on_message = on_message
|
||||
|
||||
# Subscribe to the set messages.
|
||||
for t in cfg.thermostats:
|
||||
print t
|
||||
client.subscribe( t.mqttSetTopic )
|
||||
|
||||
# Start the MQTT as a background thread. This way we can run the web
|
||||
# server as the main thread here.
|
||||
client.loop_start()
|
||||
|
||||
while True:
|
||||
t0 = time.time()
|
||||
for t in cfg.thermostats:
|
||||
try:
|
||||
# Poll the thermostat for status.
|
||||
t.status()
|
||||
|
||||
# Publish any messages.
|
||||
msgs = t.messages()
|
||||
for topic, msg in msgs:
|
||||
payload = json.dumps( msg )
|
||||
client.publish( topic, payload )
|
||||
|
||||
except Exception as e:
|
||||
# This prints a stack trace which is more than we really want.
|
||||
#log.exception( "Error getting thermostat status." )
|
||||
log.error( "Error getting thermostat status: " + str( e ) )
|
||||
|
||||
dt = time.time() - t0
|
||||
delay = max( cfg.pollTime, cfg.pollTime-dt )
|
||||
time.sleep( delay )
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import tHome.weatherUnderground
|
||||
|
||||
tHome.weatherUnderground.cmdLine.run( sys.argv )
|
||||
@@ -1,63 +0,0 @@
|
||||
#===========================================================================
|
||||
#
|
||||
# Port to use for the web server. Configure the ebtables/iptables
|
||||
# rules to redirect Acurite Bridge posts to this port.
|
||||
#
|
||||
# NOTE: The port specified in the script tHome/bin/tHome-acurite.py
|
||||
# must match this port. If you change one, change the other.
|
||||
#
|
||||
#===========================================================================
|
||||
httpPort = 22041
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Acurite sensor configuration
|
||||
#
|
||||
#===========================================================================
|
||||
import tHome.acurite as A
|
||||
|
||||
# Sensors list.
|
||||
#
|
||||
# ID is the Acurite sensor ID (easiest to find by running the message
|
||||
# debugger and watching them come through).
|
||||
sensors = [
|
||||
# sensor ID, location label, optional args
|
||||
A.Sensor( "08260", "Garage" ),
|
||||
A.Sensor( "09096", "Kitchen" ),
|
||||
A.Sensor( "00414", "Backyard" ),
|
||||
A.Sensor( "24C86E0449A0", "Bridge" ),
|
||||
A.Sensor( "05250", "Courtyard", humidity=False ),
|
||||
A.Sensor( "16039", "Rec Room", humidity=False ),
|
||||
A.Sensor( "02717", "Front Bedroom", humidity=False ),
|
||||
A.Sensor( "05125", "Den", humidity=False ),
|
||||
A.Sensor( "08628", "Garage 2", humidity=False ),
|
||||
A.Sensor( "09338", "Side Bedroom", humidity=False ),
|
||||
A.Sensor( "01948", "Master Closet", humidity=False ),
|
||||
A.Sensor( "15116", "Attic", humidity=False ),
|
||||
A.Sensor( "05450", "Master Bath", humidity=False ),
|
||||
]
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# MQTT Topics. Each requires one %s in the topic which will be
|
||||
# replaced by the location string from the sensor list above.
|
||||
#
|
||||
#===========================================================================
|
||||
mqttBattery = "power/battery/%s"
|
||||
mqttRssi = "radio/%s"
|
||||
mqttHumidity = "env/humidity/%s"
|
||||
mqttTemp = "env/temp/%s"
|
||||
mqttWindSpeed = "env/wind/speed/%s"
|
||||
mqttWindDir = "env/wind/direction/%s"
|
||||
mqttBarometer = "env/barometer/%s"
|
||||
mqttRain = "env/rain/%s"
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Logging configuration
|
||||
#
|
||||
#===========================================================================
|
||||
logFile = '/var/log/tHome/acurite.log'
|
||||
logLevel = 40
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#===========================================================================
|
||||
#
|
||||
# SMA WebConnect solar inverter configuration
|
||||
#
|
||||
#===========================================================================
|
||||
# Lat and lon of the installation location used to compute rise/set
|
||||
# times for the sun. Time pad is the time to offset from the rise/set
|
||||
# time in seconds at which to start polling the inverter.
|
||||
lat = 34.466426
|
||||
lon = -118.521923
|
||||
timePad = 600 # 10 minutes before/after sunrise/set
|
||||
|
||||
# WebConnect module location.
|
||||
host = '192.168.1.14'
|
||||
port = 9522
|
||||
group = 'USER'
|
||||
password = '0000'
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Reporting configuration
|
||||
#
|
||||
#===========================================================================
|
||||
# Polling interval in seconds for each of the reports. Use zero
|
||||
# to disable a report.
|
||||
pollPower = 15
|
||||
pollEnergy = 600
|
||||
pollFull = 0
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# MQTT topic names
|
||||
#
|
||||
#===========================================================================
|
||||
# Meter reading topic (reports daily energy total in kWh)
|
||||
mqttEnergy = 'power/solar/Home/energy'
|
||||
|
||||
# Instantaneous power usage topic (reports power usage in W)
|
||||
mqttPower = 'power/solar/Home/power'
|
||||
|
||||
# Detailed data from full report (somewhat slow to run)
|
||||
mqttFull = 'power/solar/Home/detail'
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Logging configuration
|
||||
#
|
||||
#===========================================================================
|
||||
logFile = '/var/log/tHome/sma.log'
|
||||
logLevel = 40
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
#===========================================================================
|
||||
#
|
||||
# Radio thermostat device configuration
|
||||
#
|
||||
#===========================================================================
|
||||
# Time in seconds to poll the thermostat
|
||||
pollTime = 60
|
||||
|
||||
# Thermostats to poll. Keys are:
|
||||
# host thermostat IP address
|
||||
# label label to use in logs and messages.
|
||||
# mqttTempTopic MQTT topic to publish current temp messages to.
|
||||
# mqttModeTopic MQTT topic to publish mode changes to.
|
||||
# mqttStateTopic MQTT topic to publish state (on/off) changes to.
|
||||
# mqttSetTopic MQTT topic to subscribe to read requests to change
|
||||
# the thermostat.
|
||||
#
|
||||
thermostats = [
|
||||
{
|
||||
'host' : '192.168.1.15',
|
||||
'label' : 'Downstairs',
|
||||
'mqttTempTopic' : 'env/temp/Living Room',
|
||||
'mqttModeTopic' : 'hvac/Living Room/mode',
|
||||
'mqttStateTopic' : 'hvac/Living Room/state',
|
||||
'mqttSetTopic' : 'hvac/Living Room/set/+',
|
||||
},
|
||||
{
|
||||
'host' : '192.168.1.16',
|
||||
'label' : 'Upstairs',
|
||||
'mqttTempTopic' : 'env/temp/Master Bedroom',
|
||||
'mqttModeTopic' : 'hvac/Master Bedroom/mode',
|
||||
'mqttStateTopic' : 'hvac/Master Bedroom/state',
|
||||
'mqttSetTopic' : 'hvac/Master Bedroom/set/+',
|
||||
},
|
||||
]
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Logging configuration
|
||||
#
|
||||
#===========================================================================
|
||||
logFile = '/var/log/tHome/thermostat.log'
|
||||
logLevel = 40
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import os
|
||||
#===========================================================================
|
||||
#
|
||||
# Weather underground configuration
|
||||
#
|
||||
#===========================================================================
|
||||
|
||||
# PWD upload URL and log in information. See this url for details:
|
||||
# http://wiki.wunderground.com/index.php/PWS_-_Upload_Protocol
|
||||
uploadUrl = "http://weatherstation.wunderground.com/weatherstation/updateweatherstation.php"
|
||||
|
||||
id = os.environ[ "THOME_WUG_STATION" ]
|
||||
password = os.environ[ "THOME_WUG_PASSWORD" ]
|
||||
|
||||
# Upload interval in seconds. WUG doesn't really support high rate
|
||||
# data so 1-2 minutes is fine.
|
||||
poll = 180
|
||||
|
||||
# Maximum expected sensor input rate. System will store poll/maxRate
|
||||
# values in a circular buffer and average the last set of values to up
|
||||
# load. So if poll is 120 and maxRate is 10, a buffer with 12 values
|
||||
# will be created. As data is received, it will be inserted into the
|
||||
# buffer and the last n values (up to 12) since the last upload will
|
||||
# be averaged to get the value to upload. Some values like wind gust
|
||||
# will use the maximum value in the interval instead of the average.
|
||||
# Rain values are accumulated, not averaged.
|
||||
maxRate = 10
|
||||
|
||||
# Number of floating point digits to the right of the decimal to round
|
||||
# values to before uploading them.
|
||||
digits = 2
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# MQTT topic names. These are the sensors to upload.
|
||||
#
|
||||
#===========================================================================
|
||||
# List of temperature topics to report as outdoor temperatures.
|
||||
mqttTemp = [
|
||||
"env/temp/Backyard",
|
||||
"env/temp/Courtyard",
|
||||
]
|
||||
|
||||
mqttHumidity = 'env/humidity/Backyard'
|
||||
mqttBarometer = 'env/barometer/Bridge'
|
||||
mqttRain = 'env/rain/Backyard'
|
||||
mqttWindSpeed = 'env/wind/speed/Backyard'
|
||||
mqttWindDir = 'env/wind/direction/Backyard'
|
||||
|
||||
#===========================================================================
|
||||
#
|
||||
# Logging configuration
|
||||
#
|
||||
#===========================================================================
|
||||
logFile = '/var/log/tHome/weatherUnderground.log'
|
||||
logLevel = 20
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
SCRIPT=tHome-thermostat
|
||||
|
||||
sudo cp $SCRIPT /etc/init.d/
|
||||
cd /etc/init.d/
|
||||
ll $SCRIPT
|
||||
sudo chmod 755 $SCRIPT
|
||||
sudo update-rc.d $SCRIPT defaults
|
||||
sudo update-rc.d $SCRIPT enable
|
||||
sudo service $SCRIPT start
|
||||
@@ -1,160 +0,0 @@
|
||||
#! /bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: tHome-thermostat
|
||||
# Required-Start: $remote_fs $syslog
|
||||
# Required-Stop: $remote_fs $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Thermostat reader to ZMQ messages.
|
||||
# Description: This file should be used to construct scripts to be
|
||||
# placed in /etc/init.d.
|
||||
### END INIT INFO
|
||||
|
||||
# Author: Foo Bar <foobar@baz.org>
|
||||
#
|
||||
# Please remove the "Author" lines above and replace them
|
||||
# with your own name if you copy and modify this script.
|
||||
|
||||
# Do NOT "set -e"
|
||||
|
||||
# PATH should only include /usr/* if it runs after the mountnfs.sh script
|
||||
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
||||
DESC="Thermostat reader"
|
||||
NAME=tHome-thermostat
|
||||
DAEMON=/home/ted/proj/tHome/bin/tHome-thermostats.py
|
||||
DAEMON_ARGS="-c /home/ted/conf"
|
||||
PIDFILE=/var/run/$NAME.pid
|
||||
SCRIPTNAME=/etc/init.d/$NAME
|
||||
PYTHONPATH=/home/ted/python
|
||||
|
||||
# Exit if the package is not installed
|
||||
[ -x "$DAEMON" ] || exit 0
|
||||
|
||||
# Read configuration variable file if it is present
|
||||
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
|
||||
|
||||
# Load the VERBOSE setting and other rcS variables
|
||||
. /lib/init/vars.sh
|
||||
|
||||
# Define LSB log_* functions.
|
||||
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
|
||||
# and status_of_proc is working.
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
#
|
||||
# Function that starts the daemon/service
|
||||
#
|
||||
do_start()
|
||||
{
|
||||
# Return
|
||||
# 0 if daemon has been started
|
||||
# 1 if daemon was already running
|
||||
# 2 if daemon could not be started
|
||||
start-stop-daemon --start --background -c ted --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|
||||
|| return 1
|
||||
start-stop-daemon --start --background -c ted --quiet --pidfile $PIDFILE --exec $DAEMON -- \
|
||||
$DAEMON_ARGS \
|
||||
|| return 2
|
||||
# Add code here, if necessary, that waits for the process to be ready
|
||||
# to handle requests from services started subsequently which depend
|
||||
# on this one. As a last resort, sleep for some time.
|
||||
}
|
||||
|
||||
#
|
||||
# Function that stops the daemon/service
|
||||
#
|
||||
do_stop()
|
||||
{
|
||||
# Return
|
||||
# 0 if daemon has been stopped
|
||||
# 1 if daemon was already stopped
|
||||
# 2 if daemon could not be stopped
|
||||
# other if a failure occurred
|
||||
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
|
||||
RETVAL="$?"
|
||||
[ "$RETVAL" = 2 ] && return 2
|
||||
# Wait for children to finish too if this is a daemon that forks
|
||||
# and if the daemon is only ever run from this initscript.
|
||||
# If the above conditions are not satisfied then add some other code
|
||||
# that waits for the process to drop all resources that could be
|
||||
# needed by services started subsequently. A last resort is to
|
||||
# sleep for some time.
|
||||
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
|
||||
[ "$?" = 2 ] && return 2
|
||||
# Many daemons don't delete their pidfiles when they exit.
|
||||
rm -f $PIDFILE
|
||||
return "$RETVAL"
|
||||
}
|
||||
|
||||
#
|
||||
# Function that sends a SIGHUP to the daemon/service
|
||||
#
|
||||
do_reload() {
|
||||
#
|
||||
# If the daemon can reload its configuration without
|
||||
# restarting (for example, when it is sent a SIGHUP),
|
||||
# then implement that here.
|
||||
#
|
||||
start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
|
||||
do_start
|
||||
case "$?" in
|
||||
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
|
||||
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
|
||||
esac
|
||||
;;
|
||||
stop)
|
||||
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
|
||||
do_stop
|
||||
case "$?" in
|
||||
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
|
||||
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
|
||||
esac
|
||||
;;
|
||||
status)
|
||||
status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
|
||||
;;
|
||||
#reload|force-reload)
|
||||
#
|
||||
# If do_reload() is not implemented then leave this commented out
|
||||
# and leave 'force-reload' as an alias for 'restart'.
|
||||
#
|
||||
#log_daemon_msg "Reloading $DESC" "$NAME"
|
||||
#do_reload
|
||||
#log_end_msg $?
|
||||
#;;
|
||||
restart|force-reload)
|
||||
#
|
||||
# If the "reload" option is implemented then remove the
|
||||
# 'force-reload' alias
|
||||
#
|
||||
log_daemon_msg "Restarting $DESC" "$NAME"
|
||||
do_stop
|
||||
case "$?" in
|
||||
0|1)
|
||||
do_start
|
||||
case "$?" in
|
||||
0) log_end_msg 0 ;;
|
||||
1) log_end_msg 1 ;; # Old process is still running
|
||||
*) log_end_msg 1 ;; # Failed to start
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
# Failed to stop
|
||||
log_end_msg 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
#echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
|
||||
echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
|
||||
:
|
||||
@@ -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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
#===========================================================================
|
||||
@@ -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
|
||||
|
||||
#===========================================================================
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from tHome import acurite
|
||||
|
||||
acurite.run()
|
||||
|
||||
@@ -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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#==============================================================================
|
||||
@@ -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 )
|
||||
|
||||
|
||||
#==============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#===========================================================================
|
||||
@@ -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
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
#===========================================================================
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
@@ -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
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
@@ -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 )
|
||||
|
||||
#===========================================================================
|
||||
@@ -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 )
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[Unit]
|
||||
Description=Acu-rite weather bridge reader
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/home/ted/proj/tHome/systemd/tHome.env
|
||||
Type=simple
|
||||
User=ted
|
||||
ExecStart=/home/ted/proj/tHome/bin/tHome-acurite.py --log /var/log/tHome/acurite.log --configDir /home/ted/proj/tHome/conf
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=T-Home SMA solar inverter reader
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/home/ted/proj/tHome/systemd/tHome.env
|
||||
Type=simple
|
||||
User=ted
|
||||
ExecStart=/home/ted/proj/tHome/bin/tHome-sma.py --log /var/log/tHome/sma.log --configDir /home/ted/proj/tHome/conf
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,15 +0,0 @@
|
||||
[Unit]
|
||||
Description=T-Home WIFI thermostat reader
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/home/ted/proj/tHome/systemd/tHome.env
|
||||
Type=simple
|
||||
User=ted
|
||||
ExecStart=/home/ted/proj/tHome/bin/tHome-thermostat.py --log /var/log/tHome/thermostat.log --configDir /home/ted/proj/tHome/conf
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
description "Acu-rite bridge reader"
|
||||
author "Ted Drain"
|
||||
|
||||
start on filesystem or runlevel [2345]
|
||||
stop on shutdown
|
||||
|
||||
env USER=ted
|
||||
env CMD=/home/ted/proj/tHome/bin/tHome-acurite.py
|
||||
env LOG=/var/log/tHome/acurite.log
|
||||
env CONFIG=/home/ted/proj/tHome/conf
|
||||
|
||||
script
|
||||
exec start-stop-daemon --start -c $USER --exec $CMD -- $CONFIG $LOG
|
||||
end script
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
description "T-Home SMA solar inverter reader"
|
||||
author "Ted Drain"
|
||||
|
||||
start on filesystem or runlevel [2345]
|
||||
stop on shutdown
|
||||
|
||||
env USER=ted
|
||||
env CMD=/home/ted/proj/tHome/bin/tHome-sma.py
|
||||
env LOG=/var/log/tHome/sma.log
|
||||
env CONFIG=/home/ted/proj/tHome/conf
|
||||
|
||||
script
|
||||
exec start-stop-daemon --start -c $USER --exec $CMD -- -l $LOG -c $CONFIG
|
||||
end script
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
description "T-Home WIFI thermostat reader"
|
||||
author "Ted Drain"
|
||||
|
||||
start on filesystem or runlevel [2345]
|
||||
stop on shutdown
|
||||
|
||||
env USER=ted
|
||||
env CMD=/home/ted/proj/tHome/bin/tHome-thermostat.py
|
||||
env LOG=/var/log/tHome/thermostat.log
|
||||
env CONFIG=/home/ted/proj/tHome/conf
|
||||
|
||||
script
|
||||
exec start-stop-daemon --start -c $USER --exec $CMD -- -l $LOG -c $CONFIG
|
||||
end script
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
description "T-Home Weather Underground uploader"
|
||||
author "Ted Drain"
|
||||
|
||||
start on filesystem or runlevel [2345]
|
||||
stop on shutdown
|
||||
|
||||
env USER=ted
|
||||
env CMD=/home/ted/proj/tHome/bin/tHome-wug.py
|
||||
env LOG=/var/log/tHome/weatherUnderground.log
|
||||
env CONFIG=/home/ted/proj/tHome/conf
|
||||
env PYTHONPATH=/home/ted/python
|
||||
|
||||
script
|
||||
exec start-stop-daemon --start -c $USER --exec $CMD -- -l $LOG -c $CONFIG
|
||||
end script
|
||||
|
||||
|
||||
Reference in New Issue
Block a user