Moved Docker stuff to "Docker" folder

Created k8s folder for k8s stuff
Added early-stage service.yaml for K8s deployment
This commit is contained in:
erichardso
2018-08-28 11:51:23 -07:00
parent 176e2f2062
commit d880f44ca6
138 changed files with 11 additions and 0 deletions

18
Docker/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM python:2.7.15-alpine3.8
LABEL maintainer="Evan Richardson"
LABEL version="1.0"
WORKDIR /app
COPY . /app
ENV PYTHONPATH=/app/src/python
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"]
EXPOSE 22042

4
Docker/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
astral
paho-mqtt
bottle

24
Docker/src/LICENSE Normal file
View File

@@ -0,0 +1,24 @@
Copyright (c) 2015, Ted Drain
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

80
Docker/src/README.md Normal file
View File

@@ -0,0 +1,80 @@
T-Home Automation Software
==========================
A collection of scripts and utilities for various home automation projects.
- bin/ Command line tools
- conf/ Sample config files
- init.d/ Init.d style Linux start up scripts
- python/ Main scripting library
- systemd/ Systemd (latest Raspian) start up scripts
- upstart/ Upstart (Ubuntu 14.04) style start up scripts
Currently most of the scripts read data from various sources and
translate the data into JSON'ed dictionaries which get published to a
MQTT message broker.
Acurite Weather Station
-----------------------
python/tHome/acurite contains code for decoding Acurite internet
Bridge traffic. This assumes the Acurite Bridge is connected to a
network USB dongle on a Raspberry Pi. It uses iptables and ebtables
to redirect the bridge traffic (which normally posts data to Acurite's
web servers) to the script bin/tHome-acurite.py. That script
simulates the response from Acurite's servers, decodes the data, and
translates them into MQTT messages. This can also be used with
tcpflow to decode data as it's being sent to Acurite instead of
redirecting it.
Radio Thermostat
----------------
http://www.radiothermostat.com/
python/tHome/thermostat contains code for polling a radio thermostat
WIFI module and reading the temperature and furnace/AC state. The
results are published as MQTT messages.
Rainforest Eagle Energy Monitor
-------------------------------
http://rainforestautomation.com/rfa-z109-eagle/
python/tHome/eagle contains code for reading data directly from an
Eagle energy monitor. Use bin/tHome-eagle.py to start a small web
server and set the address as the "cloud provider" in the Eagle. The
Eagle will publish energy data to the server which will converts it
into a message and publishes that as a MQTT messages.
SMA Solar Inverter
------------------
python/tHome/sma contains code for reading data from an SMA WebConnect
module attached to a SunnyBoy solar inverter. The Link class is used
for communication but most needs can be satisfied by using the report
module which has several report styles (brief to full).
bin/tHome-sma.py is a process which will poll the inverter at regular
interval while the sun is up, and publish the results as MQTT messages.
The communication protocol is based on the C code in the
https://sbfspot.codeplex.com/ project.
Weather Underground
-------------------
python/tHome/weatherUnderground contains code that subscribes to
messages produced by the Acurite Bridge module and uploads that
information to Weather Underground. It will upload data at a user
specified interval and uses a sliding window average of the sensor
data over that upload interval to smooth the sensors before uploading
them (including correctly averaging wind direction data).
Use bin/tHome-wug.py to start this process.

View File

@@ -0,0 +1,18 @@
#!/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

65
Docker/src/bin/dbg-msgHub.py Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python
# dbg-msgHub.py -s "radio" "power/battery"
import argparse
import sys
import pprint
import time
import StringIO
import tHome.util as util
import paho.mqtt.client as mqtt
p = argparse.ArgumentParser( prog=sys.argv[0],
description="Msg Hub debug output" )
p.add_argument( "-p", "--port", type=int, default=1883,
help="Broker port number to connect to." )
p.add_argument( "-b", "--broker", type=str, default='127.0.0.1',
help="Broker host name to connect to." )
p.add_argument( "-s", "--skip", nargs="*", default=[] )
c = p.parse_args( sys.argv[1:] )
class Client ( mqtt.Client ):
def __init__( self ):
mqtt.Client.__init__( self )
# Restore callbacks overwritten by stupid mqtt library
self.on_connect = Client.on_connect
self.on_message = Client.on_message
def on_connect( self, userData, flags, rc ):
self.subscribe( '#' )
def on_message( self, userData, msg ):
for k in c.skip:
if msg.topic.startswith( k ):
return
data = util.json.loads( msg.payload )
s = StringIO.StringIO()
t = time.time()
dt = t - data['time']
s.write( "dt: %d " % dt )
keys = data.keys()
keys.remove( 'time' )
for k in sorted( keys ):
s.write( "%s: %s " % ( k, data[k] ) )
print "Recv time: %.0f %-30s %s" % ( t, msg.topic, s.getvalue() )
#pprint.pprint( data )
#print data
print "Connecting to %s:%d" % ( c.broker, c.port )
client = Client()
client.connect( c.broker, c.port )
# loop_forever() and loop() block ctrl-c signals so it's hard to stop.
# So start in a thread so we can ctrl-c out of this.
client.loop_start()
while True:
pass
client.loop_stop( force=True )

90
Docker/src/bin/tHome-acurite.py Executable file
View File

@@ -0,0 +1,90 @@
#!/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 )

112
Docker/src/bin/tHome-eagle.py Executable file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python
#===========================================================================
#
# Eagle posting server
#
#===========================================================================
__doc__ = """
Starts a small web server. The Rain Forest Eagle is configured with
this web server as it's 'cloud' provider so it posts messages to the
server as XML data packets.
Scripts uses the tHome.eagle package to decode the XML packets 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
#===========================================================================
def meter( client, data, cfg ):
msg = {
"time" : data.TimeUnix,
"consumed" : data.Consumed, # kWh
"produced" : data.Produced, # kWh
}
return ( cfg.mqttEnergy, msg )
#===========================================================================
def instant( client, data, cfg ):
msg = {
"time" : data.TimeUnix,
"power" : data.Power * 1000, # W
}
return ( cfg.mqttPower, msg )
#===========================================================================
handlers = {
#"BlockPriceDetail" :
"CurrentSummation" : meter,
#"DeviceInfo" :
#"FastPollStatus" :
"InstantaneousDemand" : instant,
#"MessageCluster" :
#"MeterInfo" :
#"NetworkInfo" :
#"PriceCluster" :
#"Reading" :
#"ScheduleInfo" :
#"TimeCluster" :
}
#===========================================================================
@B.post( '/' )
def root_post():
data = B.request.body.read( B.request.content_length )
try:
obj = T.eagle.parse( data )
except:
log.exception( "Error parsing Eagle posted data" )
return "ERROR"
log.info( "Read packet: %s" % obj.name )
func = handlers.get( obj.name, None )
if func:
topic, msg = func( client, obj, cfg )
if msg:
log.info( "Publish: %s: %s" % ( topic, msg ) )
payload = json.dumps( msg )
client.publish( topic, payload )
return "ok"
#===========================================================================
#
# Main applications script
#
#===========================================================================
p = argparse.ArgumentParser( prog=sys.argv[0],
description="T-Home Eagle 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.eagle.config.parse( c.configDir )
log = T.eagle.config.log( cfg, c.log )
# 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 )

6
Docker/src/bin/tHome-sma.py Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python
import sys
import tHome.sma
tHome.sma.cmdLine.run( sys.argv )

View File

@@ -0,0 +1,80 @@
#!/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 )

6
Docker/src/bin/tHome-wug.py Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python
import sys
import tHome.weatherUnderground
tHome.weatherUnderground.cmdLine.run( sys.argv )

View File

@@ -0,0 +1,63 @@
#===========================================================================
#
# 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

34
Docker/src/conf/broker.py Normal file
View File

@@ -0,0 +1,34 @@
#===========================================================================
#
# MQTT message broker location. Default broker location is port 1883
# for regular and 8883 for SSL.
#
#===========================================================================
host = '192.168.1.20'
port = 31333
# Keep alive time in seconds. Client sends a ping if no other message
# is sent in this interval.
keepAlive = 60
#===========================================================================
#
# User name and password (strings) for broker log in.
#
#===========================================================================
user = None
password = None
#===========================================================================
#
# Secure connection options. See the paho-mqtt docs for details.
#
#===========================================================================
# List of certificate files.
ca_certs = [
]
certFile = None
keyFile = None

27
Docker/src/conf/eagle.py Normal file
View File

@@ -0,0 +1,27 @@
#===========================================================================
#
# Port to use for the web server. Configure the Eagle to use this
# port as it's 'cloud provider' using http://host:PORT
#
#===========================================================================
httpPort = 22042
#===========================================================================
#
# MQTT topic names
#
#===========================================================================
# Meter reading topic (reports current meter reading in kWh)
mqttEnergy = 'power/elec/Home/energy'
# Instantaneous power usage topic (reports power usage in W)
mqttPower = 'power/elec/Home/power'
#===========================================================================
#
# Logging configuration. Env variables are allowed in the file name.
#
#===========================================================================
logFile = '/var/log/tHome/eagle.log'
logLevel = 40

View File

@@ -0,0 +1,11 @@
/var/log/tHome/*.log {
weekly
size 5M
missingok
rotate 8
compress
delaycompress
create 644 ted ted
}

51
Docker/src/conf/sma.py Normal file
View File

@@ -0,0 +1,51 @@
#===========================================================================
#
# 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

View File

@@ -0,0 +1,45 @@
#===========================================================================
#
# 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

View File

@@ -0,0 +1,57 @@
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

View File

@@ -0,0 +1,9 @@
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

View File

@@ -0,0 +1,160 @@
#! /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
:

View File

@@ -0,0 +1,22 @@
#===========================================================================
#
# tHome package
#
#===========================================================================
__doc__ = """T-Home Python package
"""
#===========================================================================
from . import acurite
from . import broker
from . import eagle
from . import sma
from . import thermostat
from . import util
from . import weatherUnderground
#===========================================================================

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
#===========================================================================
#
# RainForest Eagle Electric meter reading package
#
#===========================================================================
__doc__ = """RainForest Eagle electric meter reader.
This package implements a web server which the RainForest Eagle can use as a cloud service. The Eagle will post data to the this module which parses the XML messages and sends them out as ZeroMQ messages (usually to a tHome.msgHub).
Logging object name: tHome.eagle
"""
#===========================================================================
#===========================================================================
from . import config
from .connect import connect
#===========================================================================

View File

@@ -0,0 +1,41 @@
#===========================================================================
#
# 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, 1883 ),
C.Entry( "keepAlive", int, 60 ),
C.Entry( "user", str ),
C.Entry( "password", str ),
C.Entry( "ca_certs", list ),
C.Entry( "certFile", util.path.expand ),
C.Entry( "keyFile", util.path.expand ),
}
#===========================================================================
def parse( configDir, configFile='broker.py' ):
cfg = C.readAndCheck( configDir, configFile, configEntries )
if cfg.ca_certs:
for i in range( len( cfg.ca_certs ) ):
cfg.ca_certs[i] = util.path.expand( cfg.ca_certs[i] )
return cfg
#===========================================================================

View File

@@ -0,0 +1,44 @@
#===========================================================================
#
# Broker connection
#
#===========================================================================
from . import config
import paho.mqtt.client as mqtt
#===========================================================================
class Client( mqtt.Client ):
"""Logging client
"""
def __init__( self, log=None ):
mqtt.Client.__init__( self )
self._logger = log
# Restore callbacks overwritten by stupid mqtt library
self.on_log = Client.on_log
def on_log( self, userData, level, buf ):
if self._logger:
self._logger.log( level, buf )
#===========================================================================
def connect( configDir, log, client=None ):
cfg = config.parse( configDir )
if client is None:
client = Client( log )
if cfg.user:
client.username_pw_set( cfg.user, cfg.password )
if cfg.ca_certs:
client.tls_set( cfg.ca_certs, cfg.certFile, cfg.keyFile )
log.info( "Connecting to broker at %s:%d" % ( cfg.host, cfg.port ) )
client.connect( cfg.host, cfg.port, cfg.keepAlive )
return client
#===========================================================================

View File

@@ -0,0 +1,67 @@
#===========================================================================
#
# Config parsing
#
#===========================================================================
__doc__ = """Config parsing.
"""
from .util import Data
import ConfigParser
import glob
import os.path
#===========================================================================
def parse( configDir ):
# Parse the files. Default xform makes all keys lower case so set
# it to str to stop that behavior.
p = ConfigParser.ConfigParser()
p.optionxform = str
files = glob.glob( os.path.join( configDir, "*.conf" ) )
for f in files:
p.read( f )
cfg = Data( _config = p )
for s in p.sections():
d = Data()
for o in p.options( s ):
setattr( d, o, p.get( s, o ) )
setattr( cfg, s, d )
return cfg
#===========================================================================
def update( data, secDef ):
for section, fields in secDef.iteritems():
if not hasattr( data, section ):
setattr( data, section, Data() )
secData = data[section]
for name, convertFunc, defaultValue in fields:
if hasattr( secData, name ):
secData[name] = convertFunc( secData[name] )
else:
secData[name] = defaultValue
#===========================================================================
def toPath( value ):
"""TODO: doc
"""
if value is None:
return None
value = str( value )
if "$" in value:
value = os.path.expandvars( value )
if "~" in value:
value = os.path.expanduser( value )
return value
#===========================================================================

View File

@@ -0,0 +1,24 @@
#===========================================================================
#
# RainForest Eagle Electric meter reading package
#
#===========================================================================
__doc__ = """RainForest Eagle electric meter reader.
This package implements a web server which the RainForest Eagle can use as a cloud service. The Eagle will post data to the this module which parses the XML messages and sends them out as ZeroMQ messages (usually to a tHome.msgHub).
Logging object name: tHome.eagle
"""
#===========================================================================
#===========================================================================
from . import config
from . import get
from . import messages
from .parse import parse
#===========================================================================

View File

@@ -0,0 +1,36 @@
#===========================================================================
#
# 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( "httpPort", int, 22042 ),
C.Entry( "mqttEnergy", str ),
C.Entry( "mqttPower", str ),
C.Entry( "logFile", util.path.expand ),
C.Entry( "logLevel", int, 20 ), # INFO
]
#===========================================================================
def parse( configDir, configFile='eagle.py' ):
return C.readAndCheck( configDir, configFile, configEntries )
#===========================================================================
def log( config, logFile=None ):
if not logFile:
logFile = config.logFile
return util.log.get( "eagle", config.logLevel, logFile )
#===========================================================================

View File

@@ -0,0 +1,110 @@
from . import config
from . import messages as msg
#from . import convert
#from .DeviceData import DeviceData
#from .DeviceInfo import DeviceInfo
#from .InstantDemand import InstantDemand
#from .Reading import Reading
#from .Total import Total
import xml.etree.ElementTree as ET
import socket
#==========================================================================
def all():
# Newlines are required
xmlCmd = "<LocalCommand>\n<Name>get_device_data</Name>\n" \
"<MacId>%s</MacId>\n</LocalCommand>\n" % ( config.macAddress )
xmlData = sendXml( xmlCmd )
# Add fake wrapper for parsing list of elements
xmlData = "<root>%s</root>" % xmlData
root = ET.fromstring( xmlData )
return DeviceData( root )
#==========================================================================
def device():
# Newlines are required
xmlCmd = "<LocalCommand>\n<Name>list_devices</Name>\n</LocalCommand>\n"
xmlData = sendXml( xmlCmd )
root = ET.fromstring( xmlData )
return msg.DeviceInfo( root )
#==========================================================================
def instant():
# Newlines are required
xmlCmd = "<LocalCommand>\n<Name>get_instantaneous_demand</Name>\n" \
"<MacId>%s</MacId>\n</LocalCommand>\n" % ( config.macAddress )
xmlData = sendXml( xmlCmd )
root = ET.fromstring( xmlData )
return msg.InstantaneousDemand( root )
#==========================================================================
def history( start ):
"start == datetime in utc"
startHex = convert.fromTime( start )
# Newlines are required
xmlCmd = "<LocalCommand>\n<Name>get_history_data</Name>\n" \
"<MacId>%s</MacId>\n<StartTime>%s</StartTime>\n" \
"</LocalCommand>\n" % ( config.macAddress, startHex )
xmlData = sendXml( xmlCmd )
# Add fake wrapper for parsing list of elements
root = ET.fromstring( xmlData )
return [ Total( child ) for child in root ]
#==========================================================================
def instantHistory( interval ):
"interval = 'hour', 'day', 'week'"
assert( interval in [ 'hour', 'day', 'week' ] )
# Newlines are required
xmlCmd = "<LocalCommand>\n<Name>get_demand_values</Name>\n" \
"<MacId>%s</MacId>\n</LocalCommand>\n" % ( config.macAddress )
xmlData = sendXml( xmlCmd )
# Add fake wrapper for parsing list of elements
xmlData = "<root>%s</root>" % xmlData
root = ET.fromstring( xmlData )
return msg.Reading.xmlToList( root )
#==========================================================================
def totalHistory( interval ):
"interval = 'day', 'week', 'month', 'year'"
assert( interval in [ 'day', 'week', 'month', 'year' ] )
# Newlines are required
xmlCmd = "<LocalCommand>\n<Name>get_summation_values</Name>\n" \
"<MacId>%s</MacId>\n</LocalCommand>\n" % ( config.macAddress )
xmlData = sendXml( xmlCmd )
# Add fake wrapper for parsing list of elements
xmlData = "<root>%s</root>" % xmlData
root = ET.fromstring( xmlData )
return msg.Reading.xmlToList( root )
#==========================================================================
def sendXml( xmlCmd ):
sock = socket.create_connection( ( config.host, config.port ) )
try:
sock.send( xmlCmd )
buf = ""
while True:
s = sock.recv( 1024 )
if not s:
break
buf += s
finally:
sock.close()
return buf
#==========================================================================

View File

@@ -0,0 +1,70 @@
#===========================================================================
#
# Base class for messages
#
#===========================================================================
import datetime
from . import convert
#==========================================================================
class Base:
def __init__( self, name, node=None ):
self.name = name
# Copy the attributes.
if node is not None:
for child in node:
setattr( self, child.tag, child.text )
# Convert hex values to int and float.
convert.hexKeys( self, self._numHexKeys, float )
convert.hexKeys( self, self._intHexKeys, int )
#------------------------------------------------------------------------
def msgDict( self ):
assert( self._jsonKeys )
msg = {}
for key in self._jsonKeys:
if hasattr( self, key ):
msg[key] = getattr( self, key )
if hasattr( self, "TimeUnix" ):
msg["Time"] = self.TimeUnix
return msg
#------------------------------------------------------------------------
def _format( self, indent=3 ):
i = " "*indent
s = "%s(\n" % self.name
for key in dir( self ):
if key[0] == "_":
continue
v = getattr( self, key )
if callable( v ):
continue
if hasattr( v, "_format" ):
s += "%s%s : %s,\n" % ( i, key, v._format( indent+3 ) )
elif isinstance( v, str ):
s += "%s%s : '%s',\n" % ( i, key, v )
else:
s += "%s%s : %s,\n" % ( i, key, v )
s += "%s)" % i
return s
#------------------------------------------------------------------------
def __str__( self ):
return self._format()
def __repr__( self ):
return self.__str__()
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,87 @@
#===========================================================================
#
# BlockPriceDetail Message
#
#===========================================================================
from .Base import Base
from . import convert
#==========================================================================
class BlockPriceDetail ( Base ):
"""Block price detail message
After construction, will have the following attributes:
DeviceMacId int
MeterMacId int
TimeStamp float (UTC sec past 1-JAN-2000 00:00)
CurrentStart
CurrentDuration
BlockPeriodConsumption
BlockPeriodConsumptionMultiplier
BlockPeriodConsumptionDivisor
NumberOfBlocks
Multiplier
Divisor
Currency
TrailingDigits
Time datetime UTC time stamp
TimeUnix float (UTC sec past 1-JAN-1970 00:00)
Consumption
Sample:
<BlockPriceDetail>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp>0x1c531d6b</TimeStamp>
<CurrentStart>0x00000000</CurrentStart>
<CurrentDuration>0x0000</CurrentDuration>
<BlockPeriodConsumption>0x0000000000231c38</BlockPeriodConsumption>
<BlockPeriodConsumptionMultiplier>0x00000001</BlockPeriodConsumptionMultiplier>
<BlockPeriodConsumptionDivisor>0x000003e8</BlockPeriodConsumptionDivisor>
<NumberOfBlocks>0x00</NumberOfBlocks>
<Multiplier>0x00000001</Multiplier>
<Divisor>0x00000001</Divisor>
<Currency>0x0348</Currency>
<TrailingDigits>0x00</TrailingDigits>
</BlockPriceDetail>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_intHexKeys = [ "DeviceMacId", "MeterMacId", "NumberOfBlocks", "Currency",
"TrailingDigits", ]
_numHexKeys = [ "TimeStamp", "CurrentStart", "CurrentDuration",
"BlockPeriodConsumption", "BlockPeriodConsumptionMultiplier",
"BlockPeriodConsumptionDivisor", "Multiplier", "Divisor" ]
_jsonKeys = [ "DeviceMacid", "MeterMacId", "Consumption" ]
#------------------------------------------------------------------------
def __init__( self, node ):
assert( node.tag == "BlockPriceDetail" )
Base.__init__( self, "BlockPriceDetail", node )
# Convert a 0 value to 1 (special case).
convert.zeroToOne( self, [ "Multiplier", "Divisor" ] )
convert.zeroToOne( self, [ "BlockPeriodConsumptionMultiplier",
"BlockPeriodConsumptionDivisor" ] )
self.Consumption = convert.toValue( self.BlockPeriodConsumption,
self.BlockPeriodConsumptionMultiplier,
self.BlockPeriodConsumptionDivisor )
convert.time( self, "Time", "TimeUnix", self.TimeStamp )
#------------------------------------------------------------------------
def jsonMsg( self ):
return {
"MacId" : self.DeviceMacId,
"Time" : self.TimeUnix,
"Consumption" : self.Consumption,
}
#==========================================================================

View File

@@ -0,0 +1,78 @@
#===========================================================================
#
# CurrentSummation Message
#
#===========================================================================
from .Base import Base
from . import convert
#==========================================================================
class CurrentSummation ( Base ):
"""Current summation message
After construction, will have the following attributes:
DeviceMacId int
MeterMacId int
TimeStamp float (UTC sec past 1-JAN-2000 00:00)
SummationDelivered float (utility->user)
SummationReceived float (user->utility)
Multiplier float
Divisor float
DigitsRight int
DigitsLeft int
SuppressLeadingZero str
Consumed float in kWh (utility->user)
Produced float in kWh (user->utility)
Time datetime UTC time stamp
TimeUnix float (UTC sec past 1-JAN-1970 00:00)
Sample:
NOTE: Socket API uses CurrentSummation, uploader (cloud) API uses
CurrentSummationDelivered. Both are fine.
<CurrentSummationDelivered>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp>0x1c531e54</TimeStamp>
<SummationDelivered>0x0000000001321a5f</SummationDelivered>
<SummationReceived>0x00000000003f8240</SummationReceived>
<Multiplier>0x00000001</Multiplier>
<Divisor>0x000003e8</Divisor>
<DigitsRight>0x01</DigitsRight>
<DigitsLeft>0x06</DigitsLeft>
<SuppressLeadingZero>Y</SuppressLeadingZero>
</CurrentSummationDelivered>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_intHexKeys = [ "DeviceMacId", "MeterMacId", "DigitsRight", "DigitsLeft" ]
_numHexKeys = [ "TimeStamp", "SummationDelivered", "SummationReceived",
"Multiplier", "Divisor" ]
_jsonKeys = [ "DeviceMacid", "MeterMacId", "Consumed", "Produced" ]
#------------------------------------------------------------------------
def __init__( self, node ):
assert( node.tag == "CurrentSummation" or
node.tag == "CurrentSummationDelivered" )
Base.__init__( self, "CurrentSummation", node )
# Convert a 0 value to 1 (special case).
convert.zeroToOne( self, [ "Multiplier", "Divisor" ] )
self.Consumed = convert.toValue( self.SummationDelivered,
self.Multiplier, self.Divisor )
self.Produced = convert.toValue( self.SummationReceived,
self.Multiplier, self.Divisor )
convert.time( self, "Time", "TimeUnix", self.TimeStamp )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,59 @@
#===========================================================================
#
# DeviceInfo Message
#
#===========================================================================
from .Base import Base
#==========================================================================
class DeviceInfo ( Base ):
"""Network info message
After construction, will have the following attributes:
DeviceMacId int
InstallCode int
LinkKey int
FWVersion str
HWVersion str
ImageType int
Manufacturer str
ModelId str
DateCode str
Port str
Sample:
<DeviceInfo>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<InstallCode>0x8ba7f1dee6c4f5cc</InstallCode>
<LinkKey>0x2b26f9124113b1e2b317d402ed789a47</LinkKey>
<FWVersion>1.4.47 (6798)</FWVersion>
<HWVersion>1.2.3</HWVersion>
<ImageType>0x1301</ImageType>
<Manufacturer>Rainforest Automation, Inc.</Manufacturer>
<ModelId>Z109-EAGLE</ModelId>
<DateCode>2013103023220630</DateCode>
<Port>/dev/ttySP0</Port>
</DeviceInfo>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_numHexKeys = []
_intHexKeys = [ "DeviceMacId", "InstallCode", "LinkKey", "ImageType" ]
_jsonKeys = [ "DeviceMacid", "InstallCode", "LinkKey", "FWVersion",
"HWVersion", "ImageType", "Manufacturer", "ModelId",
"DateCode", "Port" ]
#------------------------------------------------------------------------
def __init__( self, node ):
assert( node.tag == "DeviceInfo" )
Base.__init__( self, "DeviceInfo", node )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,53 @@
#===========================================================================
#
# FastPollStatus Message
#
#===========================================================================
from .Base import Base
from . import convert
#==========================================================================
class FastPollStatus ( Base ):
"""Fast polling status message
After construction, will have the following attributes:
DeviceMacId int
CoordMacId int
Frequency float (sec)
EndTime float (UTC sec past 1-JAN-2000 00:00)
End datetime UTC time stamp
EndUnix float (UTC sec past 1-JAN-1970 00:00)
Sample:
<FastPollStatus>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<Frequency>0x00</Frequency>
<EndTime>0xFFFFFFFF</EndTime>
</FastPollStatus>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_numHexKeys = [ "Frequency", "EndTime" ]
_intHexKeys = [ "DeviceMacId", "MeterMacId" ]
_jsonKeys = [ "DeviceMacid", "Frequency" ]
#------------------------------------------------------------------------
def __init__( self, node ):
"""node == xml ETree node
"""
assert( node.tag == "FastPollStatus" )
Base.__init__( self, "FastPollStatus", node )
convert.time( self, "End", "EndUnix", self.EndTime )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,74 @@
#===========================================================================
#
# InstantaneousDemand Message
#
#===========================================================================
from .Base import Base
from . import convert
#==========================================================================
class InstantaneousDemand ( Base ):
"""Instantaneous demand message
After construction, will have the following attributes:
DeviceMacId int
MeterMacId int
TimeStamp float (UTC sec past 1-JAN-2000 00:00)
Demand float in Watt (may be negative)
Multiplier float
Divisor float
DigitsRight int
DigitsLeft int
SuppressLeadingZero str
Power float in kWatt (may be negative)
Time datetime UTC time stamp
TimeUnix float (UTC sec past 1-JAN-1970 00:00)
Sample:
<InstantaneousDemand>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp>0x1c531d48</TimeStamp>
<Demand>0x00032d</Demand>
<Multiplier>0x00000001</Multiplier>
<Divisor>0x000003e8</Divisor>
<DigitsRight>0x03</DigitsRight>
<DigitsLeft>0x06</DigitsLeft>
<SuppressLeadingZero>Y</SuppressLeadingZero>
</InstantaneousDemand>
plus:
Time : datetime object
Power : intantaneous power reading
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_intHexKeys = [ "DeviceMacId", "MeterMacId", "DigitsRight", "DigitsLeft" ]
_numHexKeys = [ "Demand", "Multiplier", "Divisor", "TimeStamp" ]
_jsonKeys = [ "DeviceMacid", "MeterMacId", "Power" ]
#------------------------------------------------------------------------
def __init__( self, node ):
assert( node.tag == "InstantaneousDemand" )
Base.__init__( self, "InstantaneousDemand", node )
# Convert a 0 value to 1 (special case).
convert.zeroToOne( self, [ "Multiplier", "Divisor" ] )
# Handle the signed demand field.
self.Demand = convert.toSigned4( self.Demand )
self.Power = convert.toValue( self.Demand, self.Multiplier, self.Divisor )
convert.time( self, "Time", "TimeUnix", self.TimeStamp )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,69 @@
#===========================================================================
#
# MessageCluster Message
#
#===========================================================================
from .Base import Base
from . import convert
#==========================================================================
class MessageCluster ( Base ):
"""Message cluster message
After construction, will have the following attributes:
DeviceMacId int
MeterMacId int
TimeStamp float (UTC sec past 1-JAN-2000 00:00)
Id int
Text str
Priority str
StartTime float (UTC sec past 1-JAN-2000 00:00)
Duration float
ConfirmationRequired str
Confirmed str
Queue str
Time datetime UTC time stamp
TimeUnix float (UTC sec past 1-JAN-1970 00:00)
Start datetime UTC time stamp
StartUnix float (UTC sec past 1-JAN-1970 00:00)
Sample:
<MessageCluster>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp></TimeStamp>
<Id></Id>
<Text></Text>
<Priority></Priority>
<StartTime></StartTime>
<Duration></Duration>
<ConfirmationRequired>N</ConfirmationRequired>
<Confirmed>N</Confirmed>
<Queue>Active</Queue>
</MessageCluster>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_intHexKeys = [ "DeviceMacId", "MeterMacId", "Id" ]
_numHexKeys = [ "TimeStamp", "StartTime", "Duration" ]
_jsonKeys = [ "DeviceMacid", "MeterMacId", "Id", "Text", "Priority",
"Duration", "StartTime", "Queue" ]
#------------------------------------------------------------------------
def __init__( self, node ):
assert( node.tag == "MessageCluster" )
Base.__init__( self, "MessageCluster", node )
convert.time( self, "Time", "TimeUnix", self.TimeStamp )
convert.time( self, "Start", "StartUnix", self.StartTime )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,55 @@
#===========================================================================
#
# MeterInfo Message
#
#===========================================================================
from .Base import Base
#==========================================================================
class MeterInfo ( Base ):
"""Network info message
After construction, will have the following attributes:
DeviceMacId int
CoordMacId int
Type str
Nickname str
Optional:
Account str
Auth str
Host str
Enabled str
Sample:
<MeterInfo>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<Type>0x0000</Type>
<Nickname></Nickname>
<Account></Account>
<Auth></Auth>
<Host></Host>
<Enabled>Y</Enabled>
</MeterInfo>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_numHexKeys = []
_intHexKeys = [ "DeviceMacId", "CoordMacId" ]
_jsonKeys = [ "DeviceMacid", "Type", "Enabled" ]
#------------------------------------------------------------------------
def __init__( self, node ):
"""node == xml ETree node
"""
assert( node.tag == "MeterInfo" )
Base.__init__( self, "MeterInfo", node )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,63 @@
#===========================================================================
#
# NetworkInfo Message
#
#===========================================================================
from .Base import Base
#==========================================================================
class NetworkInfo ( Base ):
"""Network info message
After construction, will have the following attributes:
DeviceMacId int
CoordMacId int
Status str
LinkStrength int
Optional:
Description str
ExtPanId int
Channel int
ShortAddr int
Sample:
<NetworkInfo>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<CoordMacId>0x000781000086d0fe</CoordMacId>
<Status>Connected</Status>
<Description>Successfully Joined</Description>
<ExtPanId>0x000781000086d0fe</ExtPanId>
<Channel>20</Channel>
<ShortAddr>0xe1aa</ShortAddr>
<LinkStrength>0x64</LinkStrength>
</NetworkInfo>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_numHexKeys = []
_intHexKeys = [ "DeviceMacId", "CoordMacId", "ExtPanId", "ShortAddr",
"StatusCode", "LinkStrength" ]
_jsonKeys = [ "DeviceMacid", "Status", "LinkStrength", "Description",
"Channel" ]
#------------------------------------------------------------------------
def __init__( self, node ):
"""node == xml ETree node
"""
assert( node.tag == "NetworkInfo" )
Base.__init__( self, "NetworkInfo", node )
# Convert channel string to integer.
if hasattr( self, "Channel" ):
self.Channel = int( self.Channel )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,75 @@
#===========================================================================
#
# Message
#
#===========================================================================
from .Base import Base
from . import convert
#==========================================================================
class PriceCluster ( Base ):
"""Price cluster message
After construction, will have the following attributes:
DeviceMacId int
MeterMacId int
TimeStamp float (UTC sec past 1-JAN-2000 00:00)
Price float
Currency int
TrailingDigits int
Tier int
StartTime float (UTC sec past 1-JAN-2000 00:00)
Duration float
Time datetime UTC time stamp
TimeUnix float (UTC sec past 1-JAN-1970 00:00)
Start datetime UTC time stamp
StartUnix float (UTC sec past 1-JAN-1970 00:00)
Optional:
RateLabel str
TierLabel str
Sample:
<PriceCluster>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp>0xffffffff</TimeStamp>
<Price>0x0000000e</Price>
<Currency>0x0348</Currency>
<TrailingDigits>0x02</TrailingDigits>
<Tier>0x01</Tier>
<StartTime>0xffffffff</StartTime>
<Duration>0xffff</Duration>
<RateLabel>Tier 1</RateLabel>
</PriceCluster>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_intHexKeys = [ "DeviceMacId", "MeterMacId", "Currency", "TrailingDigits",
"Tier" ]
_numHexKeys = [ "TimeStamp", "StartTime", "Price", "Duration" ]
_jsonKeys = [ "DeviceMacid", "MeterMacId", "Time", "Price", "Tier" ]
#------------------------------------------------------------------------
def __init__( self, node ):
assert( node.tag == "PriceCluster" )
Base.__init__( self, "PriceCluster", node )
if self.Price == 0xffffffff:
self.Price = 0.0
self.Price = self.Price / 10**self.TrailingDigits
convert.time( self, "Time", "TimeUnix", self.TimeStamp )
convert.time( self, "Start", "StartUnix", self.StartTime )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,52 @@
#===========================================================================
#
# Reading Message
#
#===========================================================================
from .Base import Base
from . import convert
#==========================================================================
class Reading ( Base ):
"""Reading message
After construction, will have the following attributes:
Value float
TimeStamp float (UTC sec past 1-JAN-2000 00:00)
Type str
Time datetime UTC time stamp
TimeUnix float (UTC sec past 1-JAN-1970 00:00)
Sample:
<Reading>
<Value>-123.345</Value>
<TimeStamp>0x1c531d48</TimeStamp>
<Type>Summation</Type>
</Reading>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_intHexKeys = []
_numHexKeys = [ "TimeStamp" ]
_jsonKeys = [ "Value", "Type" ]
#------------------------------------------------------------------------
def __init__( self, node ):
"""node == xml ETree node
"""
assert( node.tag == "Reading" )
Base.__init__( self, "Reading", node )
convert.time( self, "Time", "TimeUnix", self.TimeStamp )
self.Value = float( self.Value )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,52 @@
#===========================================================================
#
# ScheduleInfo Message
#
#===========================================================================
from .Base import Base
#==========================================================================
class ScheduleInfo ( Base ):
"""Schedule info message
After construction, will have the following attributes:
DeviceMacId int
MeterMacId int
Mode str
Event str
Frequency float (sec)
Enabled str
Sample:
<ScheduleInfo>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<Mode>default</Mode>
<Event>message</Event>
<Frequency>0x00000078</Frequency>
<Enabled>Y</Enabled>
</ScheduleInfo>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_numHexKeys = [ "Frequency" ]
_intHexKeys = [ "DeviceMacId", "MeterMacId" ]
_jsonKeys = [ "DeviceMacid", "MeterMacId", "Mode", "Event", "Frequency",
"Enabled" ]
#------------------------------------------------------------------------
def __init__( self, node ):
"""node == xml ETree node
"""
assert( node.tag == "ScheduleInfo" )
Base.__init__( self, "ScheduleInfo", node )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,54 @@
#===========================================================================
#
# TimeCluster Message
#
#===========================================================================
from .Base import Base
from . import convert
#==========================================================================
class TimeCluster ( Base ):
"""Time cluster message
After construction, will have the following attributes:
DeviceMacId int
MeterMacId int
UTCTime float (UTC sec past 1-JAN-2000 00:00)
LocalTime float (Local sec past 1-JAN-2000 00:00)
Time datetime UTC time stamp
TimeUnix float (UTC sec past 1-JAN-1970 00:00)
Local datetime local time stamp
LocalUnix float (local sec past 1-JAN-1970 00:00)
Sample:
<TimeCluster>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<UTCTime>0x1c531da7</UTCTime>
<LocalTime>0x1c52ad27</LocalTime>
</TimeCluster>
"""
# Hex keys turn into floats or ints. Taken care of automatically
# in Base.__init__().
_intHexKeys = [ "DeviceMacId", "MeterMacId" ]
_numHexKeys = [ "UTCTime", "LocalTime" ]
_jsonKeys = [ "DeviceMacid", "MeterMacId", "LocalUnix" ]
#------------------------------------------------------------------------
def __init__( self, node ):
assert( node.tag == "TimeCluster" )
Base.__init__( self, "TimeCluster", node )
convert.time( self, "Time", "TimeUnix", self.UTCTime )
convert.time( self, "Local", "LocalUnix", self.LocalTime )
#------------------------------------------------------------------------
#==========================================================================

View File

@@ -0,0 +1,49 @@
#===========================================================================
#
# RainForest Eagle XML messages
#
#===========================================================================
__doc__ = """Decodes Eagle XML messages.
One class per message type. Use the eagle.parse() function to do the
conversions.
"""
#===========================================================================
from . import convert
from .Base import Base
from .BlockPriceDetail import BlockPriceDetail
from .CurrentSummation import CurrentSummation
from .DeviceInfo import DeviceInfo
from .FastPollStatus import FastPollStatus
from .InstantaneousDemand import InstantaneousDemand
from .MessageCluster import MessageCluster
from .MeterInfo import MeterInfo
from .NetworkInfo import NetworkInfo
from .PriceCluster import PriceCluster
from .Reading import Reading
from .ScheduleInfo import ScheduleInfo
from .TimeCluster import TimeCluster
#===========================================================================
# Map XML names to class names.
tagMap = {
"BlockPriceDetail" : BlockPriceDetail,
"CurrentSummation" : CurrentSummation, # socket API
"CurrentSummationDelivered" : CurrentSummation, # cloud API
"DeviceInfo" : DeviceInfo,
"FastPollStatus" : FastPollStatus,
"InstantaneousDemand" : InstantaneousDemand,
"MessageCluster" : MessageCluster,
"MeterInfo" : MeterInfo,
"NetworkInfo" : NetworkInfo,
"PriceCluster" : PriceCluster,
"Reading" : Reading,
"ScheduleInfo" : ScheduleInfo,
"TimeCluster" : TimeCluster,
}
#===========================================================================

View File

@@ -0,0 +1,80 @@
#===========================================================================
#
# Field conversion utilities.
#
#===========================================================================
import datetime
#==========================================================================
# Eagle reference date as a datetime object.
eagleT0 = datetime.datetime( 2000, 1, 1 )
# Delta in seconds between Eagle ref and UNIX ref time.
eagleT0_unixT0 = ( eagleT0 - datetime.datetime( 1970, 1, 1 ) ).total_seconds()
#==========================================================================
def hexKeys( obj, keyList, cvtFunc ):
for key in keyList:
strVal = getattr( obj, key, None )
if strVal is None:
continue
intVal = int( strVal, 16 )
setattr( obj, key, cvtFunc( intVal ) )
#==========================================================================
def time( obj, timeKey, unixKey, eagleSec ):
timeValue = None
unixValue = None
if eagleSec:
timeValue = toDateTime( eagleSec )
unixValue = toUnixTime( eagleSec )
setattr( obj, timeKey, timeValue )
setattr( obj, unixKey, unixValue )
#==========================================================================
def zeroToOne( obj, keyList ):
for key in keyList:
val = getattr( obj, key )
if not val:
setattr( obj, key, 1.0 )
#==========================================================================
def toValue( value, multiplier, divisor ):
return float( value ) * multiplier / divisor
#==========================================================================
def toSigned4( value ):
if value > 0x7FFFFFFF:
return value - 0xFFFFFFFF
return value
#==========================================================================
def toUnixTime( eagleSec ):
"""Input is EAGLE UTC seconds past 00:00:00 1-jan-2000
Returns a float of UTC seconds past UTC 1-jan-1970.
"""
return eagleSec + eagleT0_unixT0
#==========================================================================
def toDateTime( eagleSec ):
"""Input is EAGLE UTC seconds past 00:00:00 1-jan-2000
Returns a datetime object
"""
return eagleT0 + datetime.timedelta( 0, float( eagleSec ) )
#==========================================================================
def fromTime( dateTime ):
"datetime object MUST be utc"
dt = dateTime - eagleT0
isec = int( dt.total_seconds() )
return hex( isec )
#==========================================================================

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<BlockPriceDetail>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp>0x1c531d6b</TimeStamp>
<CurrentStart>0x00000000</CurrentStart>
<CurrentDuration>0x0000</CurrentDuration>
<BlockPeriodConsumption>0x0000000000231c38</BlockPeriodConsumption>
<BlockPeriodConsumptionMultiplier>0x00000001</BlockPeriodConsumptionMultiplier>
<BlockPeriodConsumptionDivisor>0x000003e8</BlockPeriodConsumptionDivisor>
<NumberOfBlocks>0x00</NumberOfBlocks>
<Multiplier>0x00000001</Multiplier>
<Divisor>0x00000001</Divisor>
<Currency>0x0348</Currency>
<TrailingDigits>0x00</TrailingDigits>
</BlockPriceDetail>
"""
root = ET.fromstring( s )
n = E.messages.BlockPriceDetail( root )
print n

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<CurrentSummationDelivered>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp>0x1c531e54</TimeStamp>
<SummationDelivered>0x0000000001321a5f</SummationDelivered>
<SummationReceived>0x00000000003f8240</SummationReceived>
<Multiplier>0x00000001</Multiplier>
<Divisor>0x000003e8</Divisor>
<DigitsRight>0x01</DigitsRight>
<DigitsLeft>0x06</DigitsLeft>
<SuppressLeadingZero>Y</SuppressLeadingZero>
</CurrentSummationDelivered>
"""
root = ET.fromstring( s )
n = E.messages.CurrentSummation( root )
print n

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<DeviceInfo>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<InstallCode>0x8ba7f1dee6c4f5cc</InstallCode>
<LinkKey>0x2b26f9124113b1e2b317d402ed789a47</LinkKey>
<FWVersion>1.4.47 (6798)</FWVersion>
<HWVersion>1.2.3</HWVersion>
<ImageType>0x1301</ImageType>
<Manufacturer>Rainforest Automation, Inc.</Manufacturer>
<ModelId>Z109-EAGLE</ModelId>
<DateCode>2013103023220630</DateCode>
<Port>/dev/ttySP0</Port>
</DeviceInfo>
"""
root = ET.fromstring( s )
n = E.messages.DeviceInfo( root )
print n

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<FastPollStatus>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<Frequency>0x00</Frequency>
<EndTime>0xFFFFFFFF</EndTime>
</FastPollStatus>
"""
root = ET.fromstring( s )
n = E.messages.FastPollStatus( root )
print n

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<InstantaneousDemand>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp>0x1c531d48</TimeStamp>
<Demand>0x00032d</Demand>
<Multiplier>0x00000001</Multiplier>
<Divisor>0x000003e8</Divisor>
<DigitsRight>0x03</DigitsRight>
<DigitsLeft>0x06</DigitsLeft>
<SuppressLeadingZero>Y</SuppressLeadingZero>
</InstantaneousDemand>
"""
root = ET.fromstring( s )
n = E.messages.InstantaneousDemand( root )
print n

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<MessageCluster>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp></TimeStamp>
<Id></Id>
<Text></Text>
<Priority></Priority>
<StartTime></StartTime>
<Duration></Duration>
<ConfirmationRequired>N</ConfirmationRequired>
<Confirmed>N</Confirmed>
<Queue>Active</Queue>
</MessageCluster>
"""
root = ET.fromstring( s )
n = E.messages.MessageCluster( root )
print n

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<MeterInfo>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<Type>0x0000</Type>
<Nickname></Nickname>
<Account></Account>
<Auth></Auth>
<Host></Host>
<Enabled>Y</Enabled>
</MeterInfo>
"""
root = ET.fromstring( s )
n = E.messages.MeterInfo( root )
print n

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<NetworkInfo>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<CoordMacId>0x000781000086d0fe</CoordMacId>
<Status>Connected</Status>
<Description>Successfully Joined</Description>
<ExtPanId>0x000781000086d0fe</ExtPanId>
<Channel>20</Channel>
<ShortAddr>0xe1aa</ShortAddr>
<LinkStrength>0x64</LinkStrength>
</NetworkInfo>
"""
root = ET.fromstring( s )
n = E.messages.NetworkInfo( root )
print n

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<PriceCluster>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<TimeStamp>0xffffffff</TimeStamp>
<Price>0x0000000e</Price>
<Currency>0x0348</Currency>
<TrailingDigits>0x02</TrailingDigits>
<Tier>0x01</Tier>
<StartTime>0xffffffff</StartTime>
<Duration>0xffff</Duration>
<RateLabel>Tier 1</RateLabel>
</PriceCluster>
"""
root = ET.fromstring( s )
n = E.messages.PriceCluster( root )
print n

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<Reading>
<Value>-123.345</Value>
<TimeStamp>0x1c531d48</TimeStamp>
<Type>Summation</Type>
</Reading>
"""
root = ET.fromstring( s )
n = E.messages.Reading( root )
print n

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<ScheduleInfo>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<Mode>default</Mode>
<Event>message</Event>
<Frequency>0x00000078</Frequency>
<Enabled>Y</Enabled>
</ScheduleInfo>
"""
root = ET.fromstring( s )
n = E.messages.ScheduleInfo( root )
print n

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import tHome.eagle as E
s="""
<TimeCluster>
<DeviceMacId>0xd8d5b9000000103f</DeviceMacId>
<MeterMacId>0x000781000086d0fe</MeterMacId>
<UTCTime>0x1c531da7</UTCTime>
<LocalTime>0x1c52ad27</LocalTime>
</TimeCluster>
"""
root = ET.fromstring( s )
n = E.messages.TimeCluster( root )
print n

View File

@@ -0,0 +1,26 @@
#===========================================================================
#
# Parse XML messages into an object.
#
#===========================================================================
import xml.etree.ElementTree as ET
from . import messages
#==========================================================================
# <rainForest ...>
# <[Message]>...</[Message]>
# </rainForest>
def parse( xmlText ):
root = ET.fromstring( xmlText )
assert( root.tag == "rainforest" )
child = root[0]
msgClass = messages.tagMap.get( child.tag, None )
if not msgClass:
return None
return msgClass( child )
#==========================================================================

View File

@@ -0,0 +1,27 @@
#===========================================================================
#
# msgHub package
#
#===========================================================================
__doc__ = """Zero-MQ Message Hub
The msgHub is a pub/sub forwarder. All of the various data producers
send messages to the msgHub as a single point of contact for the
producers. Consumers of the messages read from the hub as a single
point of contact for the consumers.
Logging object name: tHome.msgHub
"""
#===========================================================================
#===========================================================================
from . import cmdLine
from . import config
from .start import start
#===========================================================================

View File

@@ -0,0 +1,53 @@
#===========================================================================
#
# Command line parsing.
#
#===========================================================================
__doc__ = """Command line parsing.
"""
import argparse
from .. import config as C
from .. import util
from . import config
from . import start
#===========================================================================
def run( args ):
"""Parse command line arguments to start the hub.
This will parse the inputs and start the hub (it never returns).
= INPUTS
- args [str]: List of command line arguments. [0] should be the
program name.
"""
p = argparse.ArgumentParser( prog=args[0], description="T-Home message hub" )
p.add_argument( "-c", "--configDir", metavar="configDir",
default="/var/config/tHome",
help="Message hub 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 all the config files and extract the MsgHub data.
data = C.parse( c.configDir )
cfg = config.update( data )
# Override the log file.
if c.log:
cfg.LogFile = C.toPath( c.log )
if cfg.LogFile:
log = util.log.get( "msgHub" )
log.writeTo( cfg.LogFile )
log.setLevel( cfg.LogLevel )
start.start( cfg.InputPort, cfg.OutputPort )
#===========================================================================

View File

@@ -0,0 +1,34 @@
#===========================================================================
#
# Config file
#
#===========================================================================
__doc__ = """Config file parsing.
"""
from .. import config as C
#===========================================================================
# Config file section name and defaults.
sectionDef = {
"MsgHub" : [
# ( name, converter function, default value )
( "Host", str, None ),
( "InputPort", int, 22040 ),
( "OutputPort", int, 22041 ),
( "LogFile", C.toPath, None ),
( "LogLevel", int, 20 ), # INFO
],
}
#===========================================================================
def update( data ):
C.update( data, sectionDef )
return data.MsgHub
#===========================================================================

View File

@@ -0,0 +1,75 @@
#===========================================================================
#
# Main MsgHub class.
#
#===========================================================================
__doc__ = """Zero-MQ Message Hub
The msgHub is a pub/sub forwarder. All of the various data producers
send messages to the msgHub as a single point of contact for the
producers. Consumers of the messages read from the hub as a single
point of contact for the consumers.
Original code from:
http://learning-0mq-with-pyzmq.readthedocs.org/en/latest/pyzmq/devices/forwarder.html
"""
import zmq
from .. import util
#===========================================================================
def start( inPort, outPort ):
"""Start forwarding messages.
This function never returns.
= INPUTS
- inPort int: Input XSUB subscriber port number to use.
- outPort int: Output XPUB publisher port number to use.
"""
log = tHome.util.log.get( "msgHub" )
ctx = zmq.Context()
intSock, outSock = None, None
try:
# Inbound message port.
log.info( "Starting inbound subscribe socket at port %d" % inPort )
inSock = ctx.socket( zmq.XSUB )
# Use * to bind on all interfaces. Otherwise the address has to
# be an exact match (127.0.0.1 != IP).
inSock.bind( "tcp://*:%d" % inPort )
# Outbound message port.
log.info( "Starting outbound publish socket at port %d" % outPort )
outSock = ctx.socket( zmq.XPUB )
outSock.bind( "tcp://*:%d" % outPort )
# Use ZMP to handle all the forwarding. We could add logging
# here but it's easier just to add a new subscriber to read and
# log any messages.
#
# NOTE: this never returns.
log.info( "Starting forwarding" )
zmq.device( zmq.FORWARDER, inSock, outSock )
except ( Exception, KeyboardInterrupt ) as e:
log.critical( "Exception thrown", exc_info=True )
raise
finally:
if inSock:
inSock.close()
if outSock:
outSock.close()
ctx.term()
#===========================================================================

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python
import tHome as T
d = T.config.parse( "/home/ted/proj/tHome/conf" )
print d

View File

@@ -0,0 +1,149 @@
#===========================================================================
#
# 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 )
#------------------------------------------------------------------------

Some files were not shown because too many files have changed in this diff Show More