Initial Commit
This commit is contained in:
18
Dockerfile
Normal file
18
Dockerfile
Normal 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
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
astral
|
||||
paho-mqtt
|
||||
bottle
|
||||
|
||||
24
src/LICENSE
Normal file
24
src/LICENSE
Normal 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
src/README.md
Normal file
80
src/README.md
Normal 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.
|
||||
|
||||
|
||||
18
src/bin/acurite-redirect.sh
Executable file
18
src/bin/acurite-redirect.sh
Executable 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
src/bin/dbg-msgHub.py
Executable file
65
src/bin/dbg-msgHub.py
Executable 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
src/bin/tHome-acurite.py
Executable file
90
src/bin/tHome-acurite.py
Executable 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
src/bin/tHome-eagle.py
Executable file
112
src/bin/tHome-eagle.py
Executable 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
src/bin/tHome-sma.py
Executable file
6
src/bin/tHome-sma.py
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import tHome.sma
|
||||
|
||||
tHome.sma.cmdLine.run( sys.argv )
|
||||
80
src/bin/tHome-thermostat.py
Executable file
80
src/bin/tHome-thermostat.py
Executable 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
src/bin/tHome-wug.py
Executable file
6
src/bin/tHome-wug.py
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import tHome.weatherUnderground
|
||||
|
||||
tHome.weatherUnderground.cmdLine.run( sys.argv )
|
||||
63
src/conf/acurite.py
Normal file
63
src/conf/acurite.py
Normal 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
src/conf/broker.py
Normal file
34
src/conf/broker.py
Normal 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
src/conf/eagle.py
Normal file
27
src/conf/eagle.py
Normal 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
|
||||
|
||||
11
src/conf/logrotate/tHome
Normal file
11
src/conf/logrotate/tHome
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
/var/log/tHome/*.log {
|
||||
weekly
|
||||
size 5M
|
||||
missingok
|
||||
rotate 8
|
||||
compress
|
||||
delaycompress
|
||||
create 644 ted ted
|
||||
}
|
||||
|
||||
51
src/conf/sma.py
Normal file
51
src/conf/sma.py
Normal 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
|
||||
|
||||
45
src/conf/thermostat.py
Normal file
45
src/conf/thermostat.py
Normal 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
|
||||
|
||||
|
||||
57
src/conf/weatherUnderground.py
Normal file
57
src/conf/weatherUnderground.py
Normal 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
|
||||
|
||||
9
src/init.d/README.txt
Normal file
9
src/init.d/README.txt
Normal 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
|
||||
160
src/init.d/tHome-thermostat
Normal file
160
src/init.d/tHome-thermostat
Normal 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
|
||||
|
||||
:
|
||||
22
src/python/tHome/__init__.py
Normal file
22
src/python/tHome/__init__.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
30
src/python/tHome/acurite/Sensor.py
Normal file
30
src/python/tHome/acurite/Sensor.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
72
src/python/tHome/acurite/__init__.py
Normal file
72
src/python/tHome/acurite/__init__.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
68
src/python/tHome/acurite/cmdLine.py
Normal file
68
src/python/tHome/acurite/cmdLine.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
46
src/python/tHome/acurite/config.py
Normal file
46
src/python/tHome/acurite/config.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
172
src/python/tHome/acurite/decode.py
Normal file
172
src/python/tHome/acurite/decode.py
Normal 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,
|
||||
}
|
||||
|
||||
#===========================================================================
|
||||
79
src/python/tHome/acurite/mqtt.py
Normal file
79
src/python/tHome/acurite/mqtt.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
11
src/python/tHome/acurite/test/parse.py
Executable file
11
src/python/tHome/acurite/test/parse.py
Executable 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
|
||||
|
||||
29
src/python/tHome/acurite/test/process.py
Executable file
29
src/python/tHome/acurite/test/process.py
Executable 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
|
||||
|
||||
6
src/python/tHome/acurite/test/run.py
Executable file
6
src/python/tHome/acurite/test/run.py
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from tHome import acurite
|
||||
|
||||
acurite.run()
|
||||
|
||||
22
src/python/tHome/broker/__init__.py
Normal file
22
src/python/tHome/broker/__init__.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
41
src/python/tHome/broker/config.py
Normal file
41
src/python/tHome/broker/config.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
44
src/python/tHome/broker/connect.py
Normal file
44
src/python/tHome/broker/connect.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
67
src/python/tHome/config.py
Normal file
67
src/python/tHome/config.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
24
src/python/tHome/eagle/__init__.py
Normal file
24
src/python/tHome/eagle/__init__.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
36
src/python/tHome/eagle/config.py
Normal file
36
src/python/tHome/eagle/config.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
110
src/python/tHome/eagle/get.py
Normal file
110
src/python/tHome/eagle/get.py
Normal 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
|
||||
|
||||
#==========================================================================
|
||||
70
src/python/tHome/eagle/messages/Base.py
Normal file
70
src/python/tHome/eagle/messages/Base.py
Normal 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__()
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
87
src/python/tHome/eagle/messages/BlockPriceDetail.py
Normal file
87
src/python/tHome/eagle/messages/BlockPriceDetail.py
Normal 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,
|
||||
}
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
78
src/python/tHome/eagle/messages/CurrentSummation.py
Normal file
78
src/python/tHome/eagle/messages/CurrentSummation.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
59
src/python/tHome/eagle/messages/DeviceInfo.py
Normal file
59
src/python/tHome/eagle/messages/DeviceInfo.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
53
src/python/tHome/eagle/messages/FastPollStatus.py
Normal file
53
src/python/tHome/eagle/messages/FastPollStatus.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
74
src/python/tHome/eagle/messages/InstantaneousDemand.py
Normal file
74
src/python/tHome/eagle/messages/InstantaneousDemand.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
69
src/python/tHome/eagle/messages/MessageCluster.py
Normal file
69
src/python/tHome/eagle/messages/MessageCluster.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
55
src/python/tHome/eagle/messages/MeterInfo.py
Normal file
55
src/python/tHome/eagle/messages/MeterInfo.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
63
src/python/tHome/eagle/messages/NetworkInfo.py
Normal file
63
src/python/tHome/eagle/messages/NetworkInfo.py
Normal 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 )
|
||||
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
75
src/python/tHome/eagle/messages/PriceCluster.py
Normal file
75
src/python/tHome/eagle/messages/PriceCluster.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
52
src/python/tHome/eagle/messages/Reading.py
Normal file
52
src/python/tHome/eagle/messages/Reading.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
52
src/python/tHome/eagle/messages/ScheduleInfo.py
Normal file
52
src/python/tHome/eagle/messages/ScheduleInfo.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
54
src/python/tHome/eagle/messages/TimeCluster.py
Normal file
54
src/python/tHome/eagle/messages/TimeCluster.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#==========================================================================
|
||||
|
||||
|
||||
49
src/python/tHome/eagle/messages/__init__.py
Normal file
49
src/python/tHome/eagle/messages/__init__.py
Normal 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,
|
||||
}
|
||||
|
||||
#===========================================================================
|
||||
80
src/python/tHome/eagle/messages/convert.py
Normal file
80
src/python/tHome/eagle/messages/convert.py
Normal 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 )
|
||||
|
||||
#==========================================================================
|
||||
|
||||
29
src/python/tHome/eagle/messages/test/BlockPriceDetail.py
Normal file
29
src/python/tHome/eagle/messages/test/BlockPriceDetail.py
Normal 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
26
src/python/tHome/eagle/messages/test/DeviceInfo.py
Normal file
26
src/python/tHome/eagle/messages/test/DeviceInfo.py
Normal 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
|
||||
|
||||
|
||||
20
src/python/tHome/eagle/messages/test/FastPollStatus.py
Normal file
20
src/python/tHome/eagle/messages/test/FastPollStatus.py
Normal 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
|
||||
|
||||
|
||||
25
src/python/tHome/eagle/messages/test/InstantaneousDemand.py
Normal file
25
src/python/tHome/eagle/messages/test/InstantaneousDemand.py
Normal 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
|
||||
|
||||
|
||||
27
src/python/tHome/eagle/messages/test/MessageCluster.py
Normal file
27
src/python/tHome/eagle/messages/test/MessageCluster.py
Normal 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
|
||||
|
||||
|
||||
24
src/python/tHome/eagle/messages/test/MeterInfo.py
Normal file
24
src/python/tHome/eagle/messages/test/MeterInfo.py
Normal 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
|
||||
|
||||
|
||||
24
src/python/tHome/eagle/messages/test/NetworkInfo.py
Normal file
24
src/python/tHome/eagle/messages/test/NetworkInfo.py
Normal 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
|
||||
|
||||
|
||||
26
src/python/tHome/eagle/messages/test/PriceCluster.py
Normal file
26
src/python/tHome/eagle/messages/test/PriceCluster.py
Normal 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
|
||||
|
||||
|
||||
19
src/python/tHome/eagle/messages/test/Reading.py
Normal file
19
src/python/tHome/eagle/messages/test/Reading.py
Normal 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
|
||||
|
||||
|
||||
22
src/python/tHome/eagle/messages/test/ScheduleInfo.py
Normal file
22
src/python/tHome/eagle/messages/test/ScheduleInfo.py
Normal 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
|
||||
|
||||
|
||||
20
src/python/tHome/eagle/messages/test/TimeCluster.py
Normal file
20
src/python/tHome/eagle/messages/test/TimeCluster.py
Normal 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
|
||||
|
||||
|
||||
26
src/python/tHome/eagle/parse.py
Normal file
26
src/python/tHome/eagle/parse.py
Normal 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 )
|
||||
|
||||
#==========================================================================
|
||||
27
src/python/tHome/msgHub/__init__.py
Normal file
27
src/python/tHome/msgHub/__init__.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
53
src/python/tHome/msgHub/cmdLine.py
Normal file
53
src/python/tHome/msgHub/cmdLine.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
34
src/python/tHome/msgHub/config.py
Normal file
34
src/python/tHome/msgHub/config.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
75
src/python/tHome/msgHub/start.py
Normal file
75
src/python/tHome/msgHub/start.py
Normal 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()
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
127
src/python/tHome/sma/Auth.py
Normal file
127
src/python/tHome/sma/Auth.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#===========================================================================
|
||||
92
src/python/tHome/sma/Header.py
Normal file
92
src/python/tHome/sma/Header.py
Normal 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 )
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#===========================================================================
|
||||
240
src/python/tHome/sma/Link.py
Normal file
240
src/python/tHome/sma/Link.py
Normal 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 )
|
||||
|
||||
#==============================================================================
|
||||
225
src/python/tHome/sma/Reply.py
Normal file
225
src/python/tHome/sma/Reply.py
Normal 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 )
|
||||
|
||||
|
||||
#==============================================================================
|
||||
|
||||
58
src/python/tHome/sma/Request.py
Normal file
58
src/python/tHome/sma/Request.py
Normal 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
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
#===========================================================================
|
||||
28
src/python/tHome/sma/__init__.py
Normal file
28
src/python/tHome/sma/__init__.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
49
src/python/tHome/sma/cmdLine.py
Normal file
49
src/python/tHome/sma/cmdLine.py
Normal 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 )
|
||||
|
||||
|
||||
#===========================================================================
|
||||
49
src/python/tHome/sma/config.py
Normal file
49
src/python/tHome/sma/config.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
|
||||
|
||||
|
||||
99
src/python/tHome/sma/report.py
Normal file
99
src/python/tHome/sma/report.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
207
src/python/tHome/sma/start.py
Normal file
207
src/python/tHome/sma/start.py
Normal 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
|
||||
|
||||
#===========================================================================
|
||||
3431
src/python/tHome/sma/tags.py
Normal file
3431
src/python/tHome/sma/tags.py
Normal file
File diff suppressed because it is too large
Load Diff
16
src/python/tHome/sma/test/FakeSocket.py
Normal file
16
src/python/tHome/sma/test/FakeSocket.py
Normal 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
|
||||
|
||||
51
src/python/tHome/sma/test/acMaxPower.py
Normal file
51
src/python/tHome/sma/test/acMaxPower.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
51
src/python/tHome/sma/test/acPower.py
Normal file
51
src/python/tHome/sma/test/acPower.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
44
src/python/tHome/sma/test/acTotalEnergy.py
Normal file
44
src/python/tHome/sma/test/acTotalEnergy.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
43
src/python/tHome/sma/test/acTotalPower.py
Normal file
43
src/python/tHome/sma/test/acTotalPower.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
73
src/python/tHome/sma/test/acVoltage.py
Normal file
73
src/python/tHome/sma/test/acVoltage.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
47
src/python/tHome/sma/test/dcPower.py
Normal file
47
src/python/tHome/sma/test/dcPower.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
54
src/python/tHome/sma/test/dcVoltage.py
Normal file
54
src/python/tHome/sma/test/dcVoltage.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
43
src/python/tHome/sma/test/gridFrequency.py
Normal file
43
src/python/tHome/sma/test/gridFrequency.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
44
src/python/tHome/sma/test/gridRelayStatus.py
Normal file
44
src/python/tHome/sma/test/gridRelayStatus.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
58
src/python/tHome/sma/test/info.py
Normal file
58
src/python/tHome/sma/test/info.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
44
src/python/tHome/sma/test/operationTime.py
Normal file
44
src/python/tHome/sma/test/operationTime.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
9
src/python/tHome/sma/test/real/energy.py
Executable file
9
src/python/tHome/sma/test/real/energy.py
Executable 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
|
||||
9
src/python/tHome/sma/test/real/full.py
Executable file
9
src/python/tHome/sma/test/real/full.py
Executable 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
|
||||
9
src/python/tHome/sma/test/real/power.py
Executable file
9
src/python/tHome/sma/test/real/power.py
Executable 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
|
||||
44
src/python/tHome/sma/test/status.py
Normal file
44
src/python/tHome/sma/test/status.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
43
src/python/tHome/sma/test/temp.py
Normal file
43
src/python/tHome/sma/test/temp.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
44
src/python/tHome/sma/test/version.py
Normal file
44
src/python/tHome/sma/test/version.py
Normal 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 )
|
||||
|
||||
#===========================================================================
|
||||
6
src/python/tHome/test/config.py
Normal file
6
src/python/tHome/test/config.py
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import tHome as T
|
||||
|
||||
d = T.config.parse( "/home/ted/proj/tHome/conf" )
|
||||
print d
|
||||
149
src/python/tHome/thermostat/Thermostat.py
Normal file
149
src/python/tHome/thermostat/Thermostat.py
Normal 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
Reference in New Issue
Block a user