From 15546c97820ea6117e7cf3cde4eb1fc8baec12f1 Mon Sep 17 00:00:00 2001 From: Evan Richardson Date: Mon, 27 Aug 2018 21:22:50 -0700 Subject: [PATCH] Initial Commit --- Dockerfile | 18 + requirements.txt | 4 + src/LICENSE | 24 + src/README.md | 80 + src/bin/acurite-redirect.sh | 18 + src/bin/dbg-msgHub.py | 65 + src/bin/tHome-acurite.py | 90 + src/bin/tHome-eagle.py | 112 + src/bin/tHome-sma.py | 6 + src/bin/tHome-thermostat.py | 80 + src/bin/tHome-wug.py | 6 + src/conf/acurite.py | 63 + src/conf/broker.py | 34 + src/conf/eagle.py | 27 + src/conf/logrotate/tHome | 11 + src/conf/sma.py | 51 + src/conf/thermostat.py | 45 + src/conf/weatherUnderground.py | 57 + src/init.d/README.txt | 9 + src/init.d/tHome-thermostat | 160 + src/python/tHome/__init__.py | 22 + src/python/tHome/acurite/Sensor.py | 30 + src/python/tHome/acurite/__init__.py | 72 + src/python/tHome/acurite/cmdLine.py | 68 + src/python/tHome/acurite/config.py | 46 + src/python/tHome/acurite/decode.py | 172 + src/python/tHome/acurite/mqtt.py | 79 + src/python/tHome/acurite/test/parse.py | 11 + src/python/tHome/acurite/test/process.py | 29 + src/python/tHome/acurite/test/run.py | 6 + src/python/tHome/broker/__init__.py | 22 + src/python/tHome/broker/config.py | 41 + src/python/tHome/broker/connect.py | 44 + src/python/tHome/config.py | 67 + src/python/tHome/eagle/__init__.py | 24 + src/python/tHome/eagle/config.py | 36 + src/python/tHome/eagle/get.py | 110 + src/python/tHome/eagle/messages/Base.py | 70 + .../tHome/eagle/messages/BlockPriceDetail.py | 87 + .../tHome/eagle/messages/CurrentSummation.py | 78 + src/python/tHome/eagle/messages/DeviceInfo.py | 59 + .../tHome/eagle/messages/FastPollStatus.py | 53 + .../eagle/messages/InstantaneousDemand.py | 74 + .../tHome/eagle/messages/MessageCluster.py | 69 + src/python/tHome/eagle/messages/MeterInfo.py | 55 + .../tHome/eagle/messages/NetworkInfo.py | 63 + .../tHome/eagle/messages/PriceCluster.py | 75 + src/python/tHome/eagle/messages/Reading.py | 52 + .../tHome/eagle/messages/ScheduleInfo.py | 52 + .../tHome/eagle/messages/TimeCluster.py | 54 + src/python/tHome/eagle/messages/__init__.py | 49 + src/python/tHome/eagle/messages/convert.py | 80 + .../eagle/messages/test/BlockPriceDetail.py | 29 + .../test/CurrentSummationDelivered.py | 26 + .../tHome/eagle/messages/test/DeviceInfo.py | 26 + .../eagle/messages/test/FastPollStatus.py | 20 + .../messages/test/InstantaneousDemand.py | 25 + .../eagle/messages/test/MessageCluster.py | 27 + .../tHome/eagle/messages/test/MeterInfo.py | 24 + .../tHome/eagle/messages/test/NetworkInfo.py | 24 + .../tHome/eagle/messages/test/PriceCluster.py | 26 + .../tHome/eagle/messages/test/Reading.py | 19 + .../tHome/eagle/messages/test/ScheduleInfo.py | 22 + .../tHome/eagle/messages/test/TimeCluster.py | 20 + src/python/tHome/eagle/parse.py | 26 + src/python/tHome/msgHub/__init__.py | 27 + src/python/tHome/msgHub/cmdLine.py | 53 + src/python/tHome/msgHub/config.py | 34 + src/python/tHome/msgHub/start.py | 75 + src/python/tHome/sma/Auth.py | 127 + src/python/tHome/sma/Header.py | 92 + src/python/tHome/sma/Link.py | 240 ++ src/python/tHome/sma/Reply.py | 225 ++ src/python/tHome/sma/Request.py | 58 + src/python/tHome/sma/__init__.py | 28 + src/python/tHome/sma/cmdLine.py | 49 + src/python/tHome/sma/config.py | 49 + src/python/tHome/sma/report.py | 99 + src/python/tHome/sma/start.py | 207 + src/python/tHome/sma/tags.py | 3431 +++++++++++++++++ src/python/tHome/sma/test/FakeSocket.py | 16 + src/python/tHome/sma/test/acMaxPower.py | 51 + src/python/tHome/sma/test/acPower.py | 51 + src/python/tHome/sma/test/acTotalEnergy.py | 44 + src/python/tHome/sma/test/acTotalPower.py | 43 + src/python/tHome/sma/test/acVoltage.py | 73 + src/python/tHome/sma/test/dcPower.py | 47 + src/python/tHome/sma/test/dcVoltage.py | 54 + src/python/tHome/sma/test/gridFrequency.py | 43 + src/python/tHome/sma/test/gridRelayStatus.py | 44 + src/python/tHome/sma/test/info.py | 58 + src/python/tHome/sma/test/operationTime.py | 44 + src/python/tHome/sma/test/real/energy.py | 9 + src/python/tHome/sma/test/real/full.py | 9 + src/python/tHome/sma/test/real/power.py | 9 + src/python/tHome/sma/test/status.py | 44 + src/python/tHome/sma/test/temp.py | 43 + src/python/tHome/sma/test/version.py | 44 + src/python/tHome/test/config.py | 6 + src/python/tHome/thermostat/Thermostat.py | 149 + src/python/tHome/thermostat/__init__.py | 22 + src/python/tHome/thermostat/config.py | 64 + src/python/tHome/util/Data.py | 87 + src/python/tHome/util/Error.py | 61 + src/python/tHome/util/NamedStruct.py | 51 + src/python/tHome/util/__init__.py | 19 + src/python/tHome/util/config.py | 68 + src/python/tHome/util/fimport.py | 28 + src/python/tHome/util/hex/__init__.py | 10 + src/python/tHome/util/hex/dump.py | 32 + src/python/tHome/util/hex/toBytes.py | 23 + src/python/tHome/util/jsonUtil.py | 54 + src/python/tHome/util/log.py | 82 + src/python/tHome/util/net/Poll.py | 229 ++ src/python/tHome/util/path.py | 44 + src/python/tHome/util/process/__init__.py | 5 + src/python/tHome/util/process/simple.py | 22 + src/python/tHome/util/test.py | 30 + src/python/tHome/util/test/Data.py | 19 + .../tHome/weatherUnderground/__init__.py | 19 + .../tHome/weatherUnderground/cmdLine.py | 52 + src/python/tHome/weatherUnderground/config.py | 47 + src/python/tHome/weatherUnderground/start.py | 403 ++ .../tHome/weatherUnderground/test/buf.py | 71 + src/requirements | 4 + src/systemd/README.txt | 22 + src/systemd/tHome-acurite.service | 14 + src/systemd/tHome-eagle.service | 13 + src/systemd/tHome-sma.service | 12 + src/systemd/tHome-thermostat.service | 15 + src/systemd/tHome.env | 2 + src/upstart/README.txt | 6 + src/upstart/tHome-acurite.conf | 16 + src/upstart/tHome-eagle.conf | 16 + src/upstart/tHome-sma.conf | 16 + src/upstart/tHome-thermostat.conf | 16 + src/upstart/tHome-wug.conf | 17 + 137 files changed, 10509 insertions(+) create mode 100644 Dockerfile create mode 100644 requirements.txt create mode 100644 src/LICENSE create mode 100644 src/README.md create mode 100755 src/bin/acurite-redirect.sh create mode 100755 src/bin/dbg-msgHub.py create mode 100755 src/bin/tHome-acurite.py create mode 100755 src/bin/tHome-eagle.py create mode 100755 src/bin/tHome-sma.py create mode 100755 src/bin/tHome-thermostat.py create mode 100755 src/bin/tHome-wug.py create mode 100644 src/conf/acurite.py create mode 100644 src/conf/broker.py create mode 100644 src/conf/eagle.py create mode 100644 src/conf/logrotate/tHome create mode 100644 src/conf/sma.py create mode 100644 src/conf/thermostat.py create mode 100644 src/conf/weatherUnderground.py create mode 100644 src/init.d/README.txt create mode 100644 src/init.d/tHome-thermostat create mode 100644 src/python/tHome/__init__.py create mode 100644 src/python/tHome/acurite/Sensor.py create mode 100644 src/python/tHome/acurite/__init__.py create mode 100644 src/python/tHome/acurite/cmdLine.py create mode 100644 src/python/tHome/acurite/config.py create mode 100644 src/python/tHome/acurite/decode.py create mode 100644 src/python/tHome/acurite/mqtt.py create mode 100755 src/python/tHome/acurite/test/parse.py create mode 100755 src/python/tHome/acurite/test/process.py create mode 100755 src/python/tHome/acurite/test/run.py create mode 100644 src/python/tHome/broker/__init__.py create mode 100644 src/python/tHome/broker/config.py create mode 100644 src/python/tHome/broker/connect.py create mode 100644 src/python/tHome/config.py create mode 100644 src/python/tHome/eagle/__init__.py create mode 100644 src/python/tHome/eagle/config.py create mode 100644 src/python/tHome/eagle/get.py create mode 100644 src/python/tHome/eagle/messages/Base.py create mode 100644 src/python/tHome/eagle/messages/BlockPriceDetail.py create mode 100644 src/python/tHome/eagle/messages/CurrentSummation.py create mode 100644 src/python/tHome/eagle/messages/DeviceInfo.py create mode 100644 src/python/tHome/eagle/messages/FastPollStatus.py create mode 100644 src/python/tHome/eagle/messages/InstantaneousDemand.py create mode 100644 src/python/tHome/eagle/messages/MessageCluster.py create mode 100644 src/python/tHome/eagle/messages/MeterInfo.py create mode 100644 src/python/tHome/eagle/messages/NetworkInfo.py create mode 100644 src/python/tHome/eagle/messages/PriceCluster.py create mode 100644 src/python/tHome/eagle/messages/Reading.py create mode 100644 src/python/tHome/eagle/messages/ScheduleInfo.py create mode 100644 src/python/tHome/eagle/messages/TimeCluster.py create mode 100644 src/python/tHome/eagle/messages/__init__.py create mode 100644 src/python/tHome/eagle/messages/convert.py create mode 100644 src/python/tHome/eagle/messages/test/BlockPriceDetail.py create mode 100644 src/python/tHome/eagle/messages/test/CurrentSummationDelivered.py create mode 100644 src/python/tHome/eagle/messages/test/DeviceInfo.py create mode 100644 src/python/tHome/eagle/messages/test/FastPollStatus.py create mode 100644 src/python/tHome/eagle/messages/test/InstantaneousDemand.py create mode 100644 src/python/tHome/eagle/messages/test/MessageCluster.py create mode 100644 src/python/tHome/eagle/messages/test/MeterInfo.py create mode 100644 src/python/tHome/eagle/messages/test/NetworkInfo.py create mode 100644 src/python/tHome/eagle/messages/test/PriceCluster.py create mode 100644 src/python/tHome/eagle/messages/test/Reading.py create mode 100644 src/python/tHome/eagle/messages/test/ScheduleInfo.py create mode 100644 src/python/tHome/eagle/messages/test/TimeCluster.py create mode 100644 src/python/tHome/eagle/parse.py create mode 100644 src/python/tHome/msgHub/__init__.py create mode 100644 src/python/tHome/msgHub/cmdLine.py create mode 100644 src/python/tHome/msgHub/config.py create mode 100644 src/python/tHome/msgHub/start.py create mode 100644 src/python/tHome/sma/Auth.py create mode 100644 src/python/tHome/sma/Header.py create mode 100644 src/python/tHome/sma/Link.py create mode 100644 src/python/tHome/sma/Reply.py create mode 100644 src/python/tHome/sma/Request.py create mode 100644 src/python/tHome/sma/__init__.py create mode 100644 src/python/tHome/sma/cmdLine.py create mode 100644 src/python/tHome/sma/config.py create mode 100644 src/python/tHome/sma/report.py create mode 100644 src/python/tHome/sma/start.py create mode 100644 src/python/tHome/sma/tags.py create mode 100644 src/python/tHome/sma/test/FakeSocket.py create mode 100644 src/python/tHome/sma/test/acMaxPower.py create mode 100644 src/python/tHome/sma/test/acPower.py create mode 100644 src/python/tHome/sma/test/acTotalEnergy.py create mode 100644 src/python/tHome/sma/test/acTotalPower.py create mode 100644 src/python/tHome/sma/test/acVoltage.py create mode 100644 src/python/tHome/sma/test/dcPower.py create mode 100644 src/python/tHome/sma/test/dcVoltage.py create mode 100644 src/python/tHome/sma/test/gridFrequency.py create mode 100644 src/python/tHome/sma/test/gridRelayStatus.py create mode 100644 src/python/tHome/sma/test/info.py create mode 100644 src/python/tHome/sma/test/operationTime.py create mode 100755 src/python/tHome/sma/test/real/energy.py create mode 100755 src/python/tHome/sma/test/real/full.py create mode 100755 src/python/tHome/sma/test/real/power.py create mode 100644 src/python/tHome/sma/test/status.py create mode 100644 src/python/tHome/sma/test/temp.py create mode 100644 src/python/tHome/sma/test/version.py create mode 100644 src/python/tHome/test/config.py create mode 100644 src/python/tHome/thermostat/Thermostat.py create mode 100644 src/python/tHome/thermostat/__init__.py create mode 100644 src/python/tHome/thermostat/config.py create mode 100644 src/python/tHome/util/Data.py create mode 100644 src/python/tHome/util/Error.py create mode 100644 src/python/tHome/util/NamedStruct.py create mode 100644 src/python/tHome/util/__init__.py create mode 100644 src/python/tHome/util/config.py create mode 100644 src/python/tHome/util/fimport.py create mode 100644 src/python/tHome/util/hex/__init__.py create mode 100644 src/python/tHome/util/hex/dump.py create mode 100644 src/python/tHome/util/hex/toBytes.py create mode 100644 src/python/tHome/util/jsonUtil.py create mode 100644 src/python/tHome/util/log.py create mode 100644 src/python/tHome/util/net/Poll.py create mode 100644 src/python/tHome/util/path.py create mode 100644 src/python/tHome/util/process/__init__.py create mode 100644 src/python/tHome/util/process/simple.py create mode 100644 src/python/tHome/util/test.py create mode 100644 src/python/tHome/util/test/Data.py create mode 100644 src/python/tHome/weatherUnderground/__init__.py create mode 100644 src/python/tHome/weatherUnderground/cmdLine.py create mode 100644 src/python/tHome/weatherUnderground/config.py create mode 100644 src/python/tHome/weatherUnderground/start.py create mode 100644 src/python/tHome/weatherUnderground/test/buf.py create mode 100644 src/requirements create mode 100644 src/systemd/README.txt create mode 100644 src/systemd/tHome-acurite.service create mode 100644 src/systemd/tHome-eagle.service create mode 100644 src/systemd/tHome-sma.service create mode 100644 src/systemd/tHome-thermostat.service create mode 100644 src/systemd/tHome.env create mode 100644 src/upstart/README.txt create mode 100644 src/upstart/tHome-acurite.conf create mode 100644 src/upstart/tHome-eagle.conf create mode 100644 src/upstart/tHome-sma.conf create mode 100644 src/upstart/tHome-thermostat.conf create mode 100644 src/upstart/tHome-wug.conf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3b76174 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..11903ab --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +astral +paho-mqtt +bottle + diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..2205ee8 --- /dev/null +++ b/src/LICENSE @@ -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. + diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..ee910d9 --- /dev/null +++ b/src/README.md @@ -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. + + diff --git a/src/bin/acurite-redirect.sh b/src/bin/acurite-redirect.sh new file mode 100755 index 0000000..805aa7e --- /dev/null +++ b/src/bin/acurite-redirect.sh @@ -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 diff --git a/src/bin/dbg-msgHub.py b/src/bin/dbg-msgHub.py new file mode 100755 index 0000000..95e6246 --- /dev/null +++ b/src/bin/dbg-msgHub.py @@ -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 ) diff --git a/src/bin/tHome-acurite.py b/src/bin/tHome-acurite.py new file mode 100755 index 0000000..69b4f4a --- /dev/null +++ b/src/bin/tHome-acurite.py @@ -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 ) + diff --git a/src/bin/tHome-eagle.py b/src/bin/tHome-eagle.py new file mode 100755 index 0000000..f905ff0 --- /dev/null +++ b/src/bin/tHome-eagle.py @@ -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 ) diff --git a/src/bin/tHome-sma.py b/src/bin/tHome-sma.py new file mode 100755 index 0000000..d889c2f --- /dev/null +++ b/src/bin/tHome-sma.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import sys +import tHome.sma + +tHome.sma.cmdLine.run( sys.argv ) diff --git a/src/bin/tHome-thermostat.py b/src/bin/tHome-thermostat.py new file mode 100755 index 0000000..52cab70 --- /dev/null +++ b/src/bin/tHome-thermostat.py @@ -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 ) + + diff --git a/src/bin/tHome-wug.py b/src/bin/tHome-wug.py new file mode 100755 index 0000000..834c298 --- /dev/null +++ b/src/bin/tHome-wug.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import sys +import tHome.weatherUnderground + +tHome.weatherUnderground.cmdLine.run( sys.argv ) diff --git a/src/conf/acurite.py b/src/conf/acurite.py new file mode 100644 index 0000000..406ca6b --- /dev/null +++ b/src/conf/acurite.py @@ -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 + + diff --git a/src/conf/broker.py b/src/conf/broker.py new file mode 100644 index 0000000..73cbf39 --- /dev/null +++ b/src/conf/broker.py @@ -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 + + diff --git a/src/conf/eagle.py b/src/conf/eagle.py new file mode 100644 index 0000000..8f13f50 --- /dev/null +++ b/src/conf/eagle.py @@ -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 + diff --git a/src/conf/logrotate/tHome b/src/conf/logrotate/tHome new file mode 100644 index 0000000..5add9fc --- /dev/null +++ b/src/conf/logrotate/tHome @@ -0,0 +1,11 @@ + +/var/log/tHome/*.log { + weekly + size 5M + missingok + rotate 8 + compress + delaycompress + create 644 ted ted +} + diff --git a/src/conf/sma.py b/src/conf/sma.py new file mode 100644 index 0000000..b6c7145 --- /dev/null +++ b/src/conf/sma.py @@ -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 + diff --git a/src/conf/thermostat.py b/src/conf/thermostat.py new file mode 100644 index 0000000..303bc50 --- /dev/null +++ b/src/conf/thermostat.py @@ -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 + + diff --git a/src/conf/weatherUnderground.py b/src/conf/weatherUnderground.py new file mode 100644 index 0000000..7ada9f5 --- /dev/null +++ b/src/conf/weatherUnderground.py @@ -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 + diff --git a/src/init.d/README.txt b/src/init.d/README.txt new file mode 100644 index 0000000..95962f3 --- /dev/null +++ b/src/init.d/README.txt @@ -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 diff --git a/src/init.d/tHome-thermostat b/src/init.d/tHome-thermostat new file mode 100644 index 0000000..5a96280 --- /dev/null +++ b/src/init.d/tHome-thermostat @@ -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 +# +# 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 + +: diff --git a/src/python/tHome/__init__.py b/src/python/tHome/__init__.py new file mode 100644 index 0000000..e729df3 --- /dev/null +++ b/src/python/tHome/__init__.py @@ -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 + +#=========================================================================== + + diff --git a/src/python/tHome/acurite/Sensor.py b/src/python/tHome/acurite/Sensor.py new file mode 100644 index 0000000..e086514 --- /dev/null +++ b/src/python/tHome/acurite/Sensor.py @@ -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 + +#=========================================================================== + diff --git a/src/python/tHome/acurite/__init__.py b/src/python/tHome/acurite/__init__.py new file mode 100644 index 0000000..be7ccf4 --- /dev/null +++ b/src/python/tHome/acurite/__init__.py @@ -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 + +#=========================================================================== + + diff --git a/src/python/tHome/acurite/cmdLine.py b/src/python/tHome/acurite/cmdLine.py new file mode 100644 index 0000000..af43d56 --- /dev/null +++ b/src/python/tHome/acurite/cmdLine.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/acurite/config.py b/src/python/tHome/acurite/config.py new file mode 100644 index 0000000..aa2df14 --- /dev/null +++ b/src/python/tHome/acurite/config.py @@ -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 ) + +#=========================================================================== + + + diff --git a/src/python/tHome/acurite/decode.py b/src/python/tHome/acurite/decode.py new file mode 100644 index 0000000..ed51faa --- /dev/null +++ b/src/python/tHome/acurite/decode.py @@ -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, + } + +#=========================================================================== diff --git a/src/python/tHome/acurite/mqtt.py b/src/python/tHome/acurite/mqtt.py new file mode 100644 index 0000000..b0aacd7 --- /dev/null +++ b/src/python/tHome/acurite/mqtt.py @@ -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 + +#=========================================================================== diff --git a/src/python/tHome/acurite/test/parse.py b/src/python/tHome/acurite/test/parse.py new file mode 100755 index 0000000..a0009a9 --- /dev/null +++ b/src/python/tHome/acurite/test/parse.py @@ -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 + diff --git a/src/python/tHome/acurite/test/process.py b/src/python/tHome/acurite/test/process.py new file mode 100755 index 0000000..f88fd65 --- /dev/null +++ b/src/python/tHome/acurite/test/process.py @@ -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 + diff --git a/src/python/tHome/acurite/test/run.py b/src/python/tHome/acurite/test/run.py new file mode 100755 index 0000000..cdb111d --- /dev/null +++ b/src/python/tHome/acurite/test/run.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +from tHome import acurite + +acurite.run() + diff --git a/src/python/tHome/broker/__init__.py b/src/python/tHome/broker/__init__.py new file mode 100644 index 0000000..5c1a611 --- /dev/null +++ b/src/python/tHome/broker/__init__.py @@ -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 + +#=========================================================================== diff --git a/src/python/tHome/broker/config.py b/src/python/tHome/broker/config.py new file mode 100644 index 0000000..928737c --- /dev/null +++ b/src/python/tHome/broker/config.py @@ -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 + +#=========================================================================== + + + diff --git a/src/python/tHome/broker/connect.py b/src/python/tHome/broker/connect.py new file mode 100644 index 0000000..0e166f2 --- /dev/null +++ b/src/python/tHome/broker/connect.py @@ -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 + +#=========================================================================== + + + diff --git a/src/python/tHome/config.py b/src/python/tHome/config.py new file mode 100644 index 0000000..3bc7b4e --- /dev/null +++ b/src/python/tHome/config.py @@ -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 + +#=========================================================================== diff --git a/src/python/tHome/eagle/__init__.py b/src/python/tHome/eagle/__init__.py new file mode 100644 index 0000000..a1f21c5 --- /dev/null +++ b/src/python/tHome/eagle/__init__.py @@ -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 + +#=========================================================================== diff --git a/src/python/tHome/eagle/config.py b/src/python/tHome/eagle/config.py new file mode 100644 index 0000000..375aec5 --- /dev/null +++ b/src/python/tHome/eagle/config.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/eagle/get.py b/src/python/tHome/eagle/get.py new file mode 100644 index 0000000..8048ef5 --- /dev/null +++ b/src/python/tHome/eagle/get.py @@ -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 = "\nget_device_data\n" \ + "%s\n\n" % ( config.macAddress ) + xmlData = sendXml( xmlCmd ) + + # Add fake wrapper for parsing list of elements + xmlData = "%s" % xmlData + root = ET.fromstring( xmlData ) + + return DeviceData( root ) + +#========================================================================== +def device(): + # Newlines are required + xmlCmd = "\nlist_devices\n\n" + xmlData = sendXml( xmlCmd ) + root = ET.fromstring( xmlData ) + + return msg.DeviceInfo( root ) + +#========================================================================== +def instant(): + # Newlines are required + xmlCmd = "\nget_instantaneous_demand\n" \ + "%s\n\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 = "\nget_history_data\n" \ + "%s\n%s\n" \ + "\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 = "\nget_demand_values\n" \ + "%s\n\n" % ( config.macAddress ) + xmlData = sendXml( xmlCmd ) + + # Add fake wrapper for parsing list of elements + xmlData = "%s" % 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 = "\nget_summation_values\n" \ + "%s\n\n" % ( config.macAddress ) + xmlData = sendXml( xmlCmd ) + + # Add fake wrapper for parsing list of elements + xmlData = "%s" % 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 + +#========================================================================== diff --git a/src/python/tHome/eagle/messages/Base.py b/src/python/tHome/eagle/messages/Base.py new file mode 100644 index 0000000..4c52ce8 --- /dev/null +++ b/src/python/tHome/eagle/messages/Base.py @@ -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__() + + #------------------------------------------------------------------------ + +#========================================================================== diff --git a/src/python/tHome/eagle/messages/BlockPriceDetail.py b/src/python/tHome/eagle/messages/BlockPriceDetail.py new file mode 100644 index 0000000..44f1964 --- /dev/null +++ b/src/python/tHome/eagle/messages/BlockPriceDetail.py @@ -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: + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x1c531d6b + 0x00000000 + 0x0000 + 0x0000000000231c38 + 0x00000001 + 0x000003e8 + 0x00 + 0x00000001 + 0x00000001 + 0x0348 + 0x00 + + """ + + # 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, + } + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/CurrentSummation.py b/src/python/tHome/eagle/messages/CurrentSummation.py new file mode 100644 index 0000000..d2034a9 --- /dev/null +++ b/src/python/tHome/eagle/messages/CurrentSummation.py @@ -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. + + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x1c531e54 + 0x0000000001321a5f + 0x00000000003f8240 + 0x00000001 + 0x000003e8 + 0x01 + 0x06 + Y + + """ + + # 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/DeviceInfo.py b/src/python/tHome/eagle/messages/DeviceInfo.py new file mode 100644 index 0000000..8221c1d --- /dev/null +++ b/src/python/tHome/eagle/messages/DeviceInfo.py @@ -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: + + + 0xd8d5b9000000103f + 0x8ba7f1dee6c4f5cc + 0x2b26f9124113b1e2b317d402ed789a47 + 1.4.47 (6798) + 1.2.3 + 0x1301 + Rainforest Automation, Inc. + Z109-EAGLE + 2013103023220630 + /dev/ttySP0 + + """ + + # 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/FastPollStatus.py b/src/python/tHome/eagle/messages/FastPollStatus.py new file mode 100644 index 0000000..4f7132a --- /dev/null +++ b/src/python/tHome/eagle/messages/FastPollStatus.py @@ -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: + + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x00 + 0xFFFFFFFF + + """ + + # 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/InstantaneousDemand.py b/src/python/tHome/eagle/messages/InstantaneousDemand.py new file mode 100644 index 0000000..7e1166b --- /dev/null +++ b/src/python/tHome/eagle/messages/InstantaneousDemand.py @@ -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: + + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x1c531d48 + 0x00032d + 0x00000001 + 0x000003e8 + 0x03 + 0x06 + Y + + + 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/MessageCluster.py b/src/python/tHome/eagle/messages/MessageCluster.py new file mode 100644 index 0000000..51d48c2 --- /dev/null +++ b/src/python/tHome/eagle/messages/MessageCluster.py @@ -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: + + + 0xd8d5b9000000103f + 0x000781000086d0fe + + + + + + + N + N + Active + + """ + + # 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/MeterInfo.py b/src/python/tHome/eagle/messages/MeterInfo.py new file mode 100644 index 0000000..306f091 --- /dev/null +++ b/src/python/tHome/eagle/messages/MeterInfo.py @@ -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: + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x0000 + + + + + Y + + """ + + # 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/NetworkInfo.py b/src/python/tHome/eagle/messages/NetworkInfo.py new file mode 100644 index 0000000..df6e60e --- /dev/null +++ b/src/python/tHome/eagle/messages/NetworkInfo.py @@ -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: + + + 0xd8d5b9000000103f + 0x000781000086d0fe + Connected + Successfully Joined + 0x000781000086d0fe + 20 + 0xe1aa + 0x64 + + """ + + # 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 ) + + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/PriceCluster.py b/src/python/tHome/eagle/messages/PriceCluster.py new file mode 100644 index 0000000..0ad6aee --- /dev/null +++ b/src/python/tHome/eagle/messages/PriceCluster.py @@ -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: + + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0xffffffff + 0x0000000e + 0x0348 + 0x02 + 0x01 + 0xffffffff + 0xffff + Tier 1 + + """ + + # 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/Reading.py b/src/python/tHome/eagle/messages/Reading.py new file mode 100644 index 0000000..692178a --- /dev/null +++ b/src/python/tHome/eagle/messages/Reading.py @@ -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: + + + -123.345 + 0x1c531d48 + Summation + + """ + + # 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/ScheduleInfo.py b/src/python/tHome/eagle/messages/ScheduleInfo.py new file mode 100644 index 0000000..c000267 --- /dev/null +++ b/src/python/tHome/eagle/messages/ScheduleInfo.py @@ -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: + + + 0xd8d5b9000000103f + 0x000781000086d0fe + default + message + 0x00000078 + Y + + """ + + # 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/TimeCluster.py b/src/python/tHome/eagle/messages/TimeCluster.py new file mode 100644 index 0000000..4e3864f --- /dev/null +++ b/src/python/tHome/eagle/messages/TimeCluster.py @@ -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: + + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x1c531da7 + 0x1c52ad27 + + """ + + # 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 ) + + #------------------------------------------------------------------------ + +#========================================================================== + + diff --git a/src/python/tHome/eagle/messages/__init__.py b/src/python/tHome/eagle/messages/__init__.py new file mode 100644 index 0000000..bd14bc0 --- /dev/null +++ b/src/python/tHome/eagle/messages/__init__.py @@ -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, + } + +#=========================================================================== diff --git a/src/python/tHome/eagle/messages/convert.py b/src/python/tHome/eagle/messages/convert.py new file mode 100644 index 0000000..128a8e6 --- /dev/null +++ b/src/python/tHome/eagle/messages/convert.py @@ -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 ) + +#========================================================================== + diff --git a/src/python/tHome/eagle/messages/test/BlockPriceDetail.py b/src/python/tHome/eagle/messages/test/BlockPriceDetail.py new file mode 100644 index 0000000..768993c --- /dev/null +++ b/src/python/tHome/eagle/messages/test/BlockPriceDetail.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x1c531d6b + 0x00000000 + 0x0000 + 0x0000000000231c38 + 0x00000001 + 0x000003e8 + 0x00 + 0x00000001 + 0x00000001 + 0x0348 + 0x00 + +""" + +root = ET.fromstring( s ) + +n = E.messages.BlockPriceDetail( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/CurrentSummationDelivered.py b/src/python/tHome/eagle/messages/test/CurrentSummationDelivered.py new file mode 100644 index 0000000..ca2722e --- /dev/null +++ b/src/python/tHome/eagle/messages/test/CurrentSummationDelivered.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x1c531e54 + 0x0000000001321a5f + 0x00000000003f8240 + 0x00000001 + 0x000003e8 + 0x01 + 0x06 + Y + +""" + +root = ET.fromstring( s ) + +n = E.messages.CurrentSummation( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/DeviceInfo.py b/src/python/tHome/eagle/messages/test/DeviceInfo.py new file mode 100644 index 0000000..12ff5f2 --- /dev/null +++ b/src/python/tHome/eagle/messages/test/DeviceInfo.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x8ba7f1dee6c4f5cc + 0x2b26f9124113b1e2b317d402ed789a47 + 1.4.47 (6798) + 1.2.3 + 0x1301 + Rainforest Automation, Inc. + Z109-EAGLE + 2013103023220630 + /dev/ttySP0 + +""" + +root = ET.fromstring( s ) + +n = E.messages.DeviceInfo( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/FastPollStatus.py b/src/python/tHome/eagle/messages/test/FastPollStatus.py new file mode 100644 index 0000000..08f67ce --- /dev/null +++ b/src/python/tHome/eagle/messages/test/FastPollStatus.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x00 + 0xFFFFFFFF + +""" + +root = ET.fromstring( s ) + +n = E.messages.FastPollStatus( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/InstantaneousDemand.py b/src/python/tHome/eagle/messages/test/InstantaneousDemand.py new file mode 100644 index 0000000..87528c4 --- /dev/null +++ b/src/python/tHome/eagle/messages/test/InstantaneousDemand.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x1c531d48 + 0x00032d + 0x00000001 + 0x000003e8 + 0x03 + 0x06 + Y + +""" + +root = ET.fromstring( s ) + +n = E.messages.InstantaneousDemand( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/MessageCluster.py b/src/python/tHome/eagle/messages/test/MessageCluster.py new file mode 100644 index 0000000..af1f9c0 --- /dev/null +++ b/src/python/tHome/eagle/messages/test/MessageCluster.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + + + + + + + N + N + Active + +""" + +root = ET.fromstring( s ) + +n = E.messages.MessageCluster( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/MeterInfo.py b/src/python/tHome/eagle/messages/test/MeterInfo.py new file mode 100644 index 0000000..ea8c8d5 --- /dev/null +++ b/src/python/tHome/eagle/messages/test/MeterInfo.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x0000 + + + + + Y + +""" + +root = ET.fromstring( s ) + +n = E.messages.MeterInfo( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/NetworkInfo.py b/src/python/tHome/eagle/messages/test/NetworkInfo.py new file mode 100644 index 0000000..958956d --- /dev/null +++ b/src/python/tHome/eagle/messages/test/NetworkInfo.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + Connected + Successfully Joined + 0x000781000086d0fe + 20 + 0xe1aa + 0x64 + +""" + +root = ET.fromstring( s ) + +n = E.messages.NetworkInfo( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/PriceCluster.py b/src/python/tHome/eagle/messages/test/PriceCluster.py new file mode 100644 index 0000000..d4c1658 --- /dev/null +++ b/src/python/tHome/eagle/messages/test/PriceCluster.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0xffffffff + 0x0000000e + 0x0348 + 0x02 + 0x01 + 0xffffffff + 0xffff + Tier 1 + +""" + +root = ET.fromstring( s ) + +n = E.messages.PriceCluster( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/Reading.py b/src/python/tHome/eagle/messages/test/Reading.py new file mode 100644 index 0000000..f9ef027 --- /dev/null +++ b/src/python/tHome/eagle/messages/test/Reading.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + -123.345 + 0x1c531d48 + Summation + +""" + +root = ET.fromstring( s ) + +n = E.messages.Reading( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/ScheduleInfo.py b/src/python/tHome/eagle/messages/test/ScheduleInfo.py new file mode 100644 index 0000000..728a6ae --- /dev/null +++ b/src/python/tHome/eagle/messages/test/ScheduleInfo.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + default + message + 0x00000078 + Y + +""" + +root = ET.fromstring( s ) + +n = E.messages.ScheduleInfo( root ) +print n + + diff --git a/src/python/tHome/eagle/messages/test/TimeCluster.py b/src/python/tHome/eagle/messages/test/TimeCluster.py new file mode 100644 index 0000000..e1437e3 --- /dev/null +++ b/src/python/tHome/eagle/messages/test/TimeCluster.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import xml.etree.ElementTree as ET +import tHome.eagle as E + +s=""" + + 0xd8d5b9000000103f + 0x000781000086d0fe + 0x1c531da7 + 0x1c52ad27 + +""" + +root = ET.fromstring( s ) + +n = E.messages.TimeCluster( root ) +print n + + diff --git a/src/python/tHome/eagle/parse.py b/src/python/tHome/eagle/parse.py new file mode 100644 index 0000000..a411dfe --- /dev/null +++ b/src/python/tHome/eagle/parse.py @@ -0,0 +1,26 @@ +#=========================================================================== +# +# Parse XML messages into an object. +# +#=========================================================================== +import xml.etree.ElementTree as ET +from . import messages + +#========================================================================== + +# +# <[Message]>... +# +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 ) + +#========================================================================== diff --git a/src/python/tHome/msgHub/__init__.py b/src/python/tHome/msgHub/__init__.py new file mode 100644 index 0000000..1f12f14 --- /dev/null +++ b/src/python/tHome/msgHub/__init__.py @@ -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 + +#=========================================================================== + diff --git a/src/python/tHome/msgHub/cmdLine.py b/src/python/tHome/msgHub/cmdLine.py new file mode 100644 index 0000000..6ceaedf --- /dev/null +++ b/src/python/tHome/msgHub/cmdLine.py @@ -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 ) + +#=========================================================================== + + + diff --git a/src/python/tHome/msgHub/config.py b/src/python/tHome/msgHub/config.py new file mode 100644 index 0000000..dc4a6ae --- /dev/null +++ b/src/python/tHome/msgHub/config.py @@ -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 + +#=========================================================================== + + + diff --git a/src/python/tHome/msgHub/start.py b/src/python/tHome/msgHub/start.py new file mode 100644 index 0000000..ae0a820 --- /dev/null +++ b/src/python/tHome/msgHub/start.py @@ -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() + +#=========================================================================== + + + diff --git a/src/python/tHome/sma/Auth.py b/src/python/tHome/sma/Auth.py new file mode 100644 index 0000000..d23bdd3 --- /dev/null +++ b/src/python/tHome/sma/Auth.py @@ -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 ) + + #------------------------------------------------------------------------ + +#=========================================================================== diff --git a/src/python/tHome/sma/Header.py b/src/python/tHome/sma/Header.py new file mode 100644 index 0000000..02890db --- /dev/null +++ b/src/python/tHome/sma/Header.py @@ -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 ) + + #------------------------------------------------------------------------ + +#=========================================================================== diff --git a/src/python/tHome/sma/Link.py b/src/python/tHome/sma/Link.py new file mode 100644 index 0000000..5a44f54 --- /dev/null +++ b/src/python/tHome/sma/Link.py @@ -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 ) + +#============================================================================== diff --git a/src/python/tHome/sma/Reply.py b/src/python/tHome/sma/Reply.py new file mode 100644 index 0000000..3ca3ed8 --- /dev/null +++ b/src/python/tHome/sma/Reply.py @@ -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( ' 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 ) + + +#============================================================================== + diff --git a/src/python/tHome/sma/Request.py b/src/python/tHome/sma/Request.py new file mode 100644 index 0000000..0117400 --- /dev/null +++ b/src/python/tHome/sma/Request.py @@ -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 + + #------------------------------------------------------------------------ + +#=========================================================================== diff --git a/src/python/tHome/sma/__init__.py b/src/python/tHome/sma/__init__.py new file mode 100644 index 0000000..d887b27 --- /dev/null +++ b/src/python/tHome/sma/__init__.py @@ -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 + +#=========================================================================== diff --git a/src/python/tHome/sma/cmdLine.py b/src/python/tHome/sma/cmdLine.py new file mode 100644 index 0000000..aba5355 --- /dev/null +++ b/src/python/tHome/sma/cmdLine.py @@ -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 ) + + +#=========================================================================== diff --git a/src/python/tHome/sma/config.py b/src/python/tHome/sma/config.py new file mode 100644 index 0000000..051d5ed --- /dev/null +++ b/src/python/tHome/sma/config.py @@ -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 ) + +#=========================================================================== + + + diff --git a/src/python/tHome/sma/report.py b/src/python/tHome/sma/report.py new file mode 100644 index 0000000..0678623 --- /dev/null +++ b/src/python/tHome/sma/report.py @@ -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 + +#=========================================================================== diff --git a/src/python/tHome/sma/start.py b/src/python/tHome/sma/start.py new file mode 100644 index 0000000..0778567 --- /dev/null +++ b/src/python/tHome/sma/start.py @@ -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 + +#=========================================================================== diff --git a/src/python/tHome/sma/tags.py b/src/python/tHome/sma/tags.py new file mode 100644 index 0000000..ff873f9 --- /dev/null +++ b/src/python/tHome/sma/tags.py @@ -0,0 +1,3431 @@ +#=========================================================================== +# +# SMA attribute tags -> string values. +# +# Values take from SMASpot software TagListEN-US.txt file. +# https://sbfspot.codeplex.com/ +# +#=========================================================================== + +values = { + 1 : "%", + 2 : "C", + 3 : "A", + 4 : "dBm", + 5 : "deg", + 6 : "h", + 7 : "Hz", + 8 : "kWh", + 9 : "m/s", + 10 : "ms", + 11 : "Ohm", + 12 : "Pa", + 13 : "s", + 14 : "V", + 15 : "VA", + 16 : "var", + 17 : "W/m2", + 18 : "W", + 19 : "Wh", + 20 : "Phase currents", + 21 : "Number grid connections device", + 22 : "Total operating time of device", + 23 : "Total feed-in time of device", + 24 : "Total energy absorbed from the grid by the device", + 25 : "Total energy fed by device", + 26 : "Acknowledge fault", + 27 : "Special setting", + 28 : "Islanding detection", + 29 : "Number of detections", + 30 : "Time of the last detection", + 31 : "K", + 32 : "F", + 33 : "W/s", + 34 : "min", + 35 : "Fault", + 36 : "Tripping threshold DC current monitoring", + 37 : "Tripping time DC current monitoring", + 38 : "Current", + 39 : "Operating condition current", + 40 : "Escalation factor", + 41 : "Operating condition current", + 42 : "AS4777.3", + 44 : "External", + 45 : "External 2", + 46 : "Battery", + 47 : "Maximum Bluetooth transmission power", + 48 : "Interior", + 49 : "Function", + 50 : "Status", + 51 : "Closed", + 52 : "Reset operating data", + 53 : "Country standard", + 54 : "Set country standard", + 55 : "Communication disturbed", + 56 : "Country settings", + 57 : "Constant voltage control", + 58 : "Cooling system", + 59 : "Data logging", + 60 : "Number of Flash write cycles", + 61 : "DC overcurrent", + 62 : "DC settings", + 63 : "Load parameter", + 64 : "DHCP", + 65 : "Intermediate circuit voltage", + 66 : "Start conditions not met", + 67 : "DC measurements", + 68 : "DC control", + 69 : "DC overvoltage", + 70 : "KiB", + 71 : "Interference of device", + 72 : "Load preset", + 73 : "Diffuse insolation", + 74 : "Direct insolation", + 75 : "DK5940E2.2", + 76 : "Fault correction measure", + 77 : "Check AC circuit breaker", + 78 : "Check generator", + 79 : "Disconnect generator", + 80 : "Check parameter", + 81 : "Check connection", + 82 : "Environment", + 83 : "UCE monitoring", + 84 : "Overcurrent grid (HW)", + 85 : "Overcurrent grid (SW)", + 86 : "Offset grid current sensor", + 87 : "Grid frequency disturbance", + 88 : "Grid frequency not permitted", + 89 : "Grid disconnection point", + 90 : "Deviation grid voltage measurement", + 91 : "Overvoltage grid (HW)", + 92 : "Grid overvoltage fast", + 93 : "Grid overvoltage slow", + 94 : "Grid overvoltage (spot value)", + 95 : "Grid undervoltage fast", + 96 : "Grid undervoltage slow", + 97 : "Grid voltage measurement Offset", + 98 : "Voltage increase protection", + 99 : "High discharge current", + 100 : "On-board supply system disturbance", + 101 : "General BSP fault", + 102 : "Events for developer", + 103 : "Events for installer", + 104 : "Events for service", + 105 : "Events for user", + 106 : "Execution (Reboot)", + 107 : "CPLD (HW)", + 108 : "CPLD version check", + 109 : "CPU self-test HP", + 110 : "DI converter fault", + 111 : "DI converter communication", + 112 : "Residual current", + 113 : "DI converter test current", + 114 : "Data storage blocked", + 115 : "Overcurrent input A (SW)", + 116 : "Overcurrent input B (SW)", + 117 : "Offset DC current sensor A", + 118 : "Offset DC current sensor B", + 119 : "DC grid feed-in", + 120 : "Overcurrent input A (HW)", + 121 : "Overcurrent input B (HW)", + 122 : "Overvoltage intermediate circuit (HW)", + 123 : "Overvoltage intermediate circuit (SW)", + 124 : "DC voltage measurement deviation", + 125 : "Overvoltage input A (SW)", + 126 : "Overvoltage input B (SW)", + 127 : "Generator voltage too low", + 128 : "DC power too low", + 129 : "DC converter string A defective", + 130 : "DC converter string B defective", + 131 : "Generator output too low", + 132 : "System data defective", + 133 : "System data access not possible", + 134 : "System data restored", + 135 : "External watchdog (enable)", + 136 : "Grid parameter unchanged", + 137 : "Waiting for main CPU", + 138 : "Grid parameter locked", + 139 : "Execution (Test HW)", + 140 : "Restart diagnosis system", + 141 : "Derating occurred", + 142 : "Execution (Taskinit)", + 143 : "Installer code invalid", + 144 : "Installer code valid", + 145 : "Relay defect", + 146 : "24 hour relay test", + 147 : "Execution (Mail)", + 148 : "Internal communication", + 149 : "Insulation failure", + 150 : "Sensor system insulation resistance", + 151 : "Relay insulation resistance", + 152 : "Current event number", + 153 : "Event number manufacturer", + 154 : "Grid failure", + 155 : "Island grid", + 156 : "Execution (Operation)", + 157 : "Execution", + 158 : "General OSL fault", + 159 : "Overtemperature interior", + 160 : "Overtemperature power unit", + 161 : "Varistor defective", + 162 : "PE not connected", + 163 : "L / N swapped", + 164 : "2nd phase connected to N", + 165 : "PLL outside limits", + 166 : "Memory defective", + 167 : "Reference voltage test", + 168 : "Code memory defective", + 169 : "SD card defective", + 170 : "SD card is read", + 171 : "Parameter file not found or defective", + 172 : "Set parameter", + 173 : "Parameter setting failed", + 174 : "Parameters set successfully", + 175 : "No new update on the SD card", + 176 : "Update file defective", + 177 : "Update file OK", + 178 : "No update file found", + 179 : "Execution (SharedMemory)", + 180 : "Self-test", + 181 : "Abort self-test", + 182 : "Abort self-test", + 183 : "Self-test current disconnection limit", + 184 : "Self-test standard value for display", + 185 : "Self-test disconnection time for display", + 186 : "Self-test disconnection limit for display", + 187 : "Long term data defective", + 188 : "Data storage defective", + 189 : "Execution (State machine)", + 190 : "Execution (Startup)", + 191 : "No system data", + 192 : "Fault sensor interior temperature", + 193 : "Fault sensor power unit temperature", + 194 : "Update Bluetooth", + 195 : "Update Bluetooth failed", + 196 : "Update completed", + 197 : "Update main CPU", + 198 : "Update main CPU failed", + 199 : "Update RS485I module", + 200 : "Update RS485I module failed", + 201 : "Update communication", + 202 : "Update language table", + 203 : "Update language table failed", + 204 : "Update display", + 205 : "Update display failed", + 206 : "Power unit", + 207 : "Bridge short-circuit", + 208 : "Execution (Watchdog)", + 209 : "24h watchdog test", + 210 : "Ethernet", + 211 : "Fan interior", + 212 : "Fan interior 2", + 213 : "Fan heat sink", + 214 : "Fan transformer", + 215 : "Fan transformer 2", + 216 : "Fan test", + 217 : "Residual current", + 218 : "High discharge current", + 219 : "Operating condition residual current", + 220 : "Residual current too high", + 221 : "Installation fault", + 222 : "Frequency monitoring", + 223 : "G83/1", + 224 : "Display self-test start message", + 225 : "Grid parameter unchanged", + 226 : "Changing of grid parameters not possible", + 228 : "Grid parameter locked", + 229 : "Grid monitoring", + 230 : "Grid measurements", + 231 : "Grid fault", + 232 : "Reconnection time upon grid interruption", + 233 : "Reconnection time upon short interruption", + 234 : "Maximum duration of a short interruption", + 235 : "Parallel grid operation", + 236 : "Reconnection time upon restart", + 237 : "N grid conn. at grid conn.pt.", + 238 : "Grid relay status", + 239 : "Set group", + 240 : "Condition", + 241 : "Device status", + 242 : "Median maximum threshold", + 243 : "Median maximum threshold tripping time", + 244 : "Overcurrent input C (HW)", + 245 : "Lower maximum threshold", + 246 : "Lower maximum threshold tripping time", + 247 : "G83/1-1", + 248 : "Air humidity", + 249 : "Wind direction", + 250 : "Wind speed", + 251 : "Heat sink", + 252 : "Heat sink 2", + 253 : "Hardware version", + 254 : "Grid frequency", + 255 : "End point of the power control via frequency", + 256 : "Start point of the power control via frequency", + 257 : "Frequency not permitted", + 258 : "Switching status grid relay", + 259 : "Operating condition grid frequency", + 260 : "HTTP", + 261 : "Derating occurred", + 262 : "Unstable operation", + 263 : "SMA grid guard code invalid", + 264 : "SMA grid guard code valid", + 265 : "GridGuard password", + 266 : "Interface", + 267 : "Inverter", + 268 : "Insulation monitoring", + 269 : "Data storage not possible", + 270 : "KEPCO guide", + 271 : "kB", + 272 : "Insulation resistance", + 273 : "Minimum insulation resistance", + 274 : "Operating condition insulation resistance", + 275 : "10 minute average", + 276 : "Instantaneous value", + 277 : "Derating", + 278 : "Upper minimum threshold", + 279 : "Upper minimum threshold tripping time", + 280 : "Byte", + 281 : "Median minimum threshold", + 282 : "Median minimum threshold tripping time", + 283 : "B", + 284 : "Device name", + 285 : "KEMCO501/2008", + 286 : "Device class", + 287 : "Upper maximum threshold", + 288 : "Upper maximum threshold tripping time", + 289 : "PV module", + 290 : "Measured values", + 291 : "Lower minimum threshold", + 292 : "Lower minimum threshold tripping time", + 294 : "Device type", + 295 : "MPP", + 296 : "Modem", + 297 : "Message", + 298 : "Name", + 299 : "Type Label", + 300 : "Nat", + 301 : "Grid failure", + 302 : "-------", + 303 : "Off", + 304 : "Island mode", + 305 : "Island mode", + 306 : "Island mode 60 Hz", + 307 : "Ok", + 308 : "On", + 309 : "Operation", + 310 : "Operating condition", + 311 : "Open", + 312 : "Phase assignment", + 313 : "Island mode 50 Hz", + 314 : "Maximum active power", + 315 : "Currently set active power limit", + 316 : "Operating mode Active power", + 317 : "All phases", + 318 : "Overload", + 319 : "Overtemperature", + 320 : "Varistor defective", + 321 : "Printed circuit board", + 322 : "PE connection missing", + 323 : "PE connection monitoring", + 324 : "L / N swapped", + 325 : "Phase L1", + 326 : "Phases L1, L2 and L3", + 327 : "Phase L2", + 328 : "Proxy settings", + 329 : "Phase L3", + 330 : "Port", + 331 : "Phase voltage", + 332 : "Operating condition voltage", + 333 : "PPC", + 334 : "Atmospheric pressure", + 335 : "Recommended action", + 336 : "Contact manufacturer", + 337 : "Contact installer", + 338 : "invalid", + 339 : "DC voltage control type", + 340 : "PV system", + 341 : "Recommended action", + 342 : "Production test mode", + 343 : "RD1663", + 344 : "Reset operating data", + 345 : "Controller", + 346 : "Remote control", + 347 : "Device restart triggered", + 348 : "Revision status", + 349 : "Grid relay closed", + 350 : "Waiting time until feed-in", + 351 : "Voltage increase protection", + 352 : "RD1663/661", + 353 : "Reset events", + 354 : "Reset maximum values", + 355 : "Reset energy logger", + 356 : "Reset operation inhibition", + 357 : "Number of Resets", + 358 : "SB 4000TL-20", + 359 : "SB 5000TL-20", + 360 : "Storage card", + 361 : "SD card defective", + 362 : "Reading SD Card", + 363 : "Parameter file not found or defective", + 364 : "Set parameter", + 365 : "Parameter setting failed", + 366 : "Parameters set successfully", + 367 : "No new update on the SD card", + 368 : "Update file defective", + 369 : "Update file OK", + 370 : "No update file found", + 372 : "Serial number", + 373 : "RD1663-A", + 374 : "Self diagnosis", + 375 : "self-test", + 376 : "Abort self-test", + 377 : "Number of S0 impulses", + 378 : "Consumed energy", + 379 : "Software version Update", + 380 : "Deactivation delay", + 381 : "Stop", + 382 : "Number of DC disconnects", + 383 : "Input A defective", + 384 : "Input B defective", + 385 : "Start delay", + 386 : "unclear", + 387 : "Critical voltage to start feed-in", + 388 : "Operation status", + 389 : "Startup status", + 390 : "Stop status", + 391 : "Cold start status", + 392 : "Test HW status", + 393 : "Software version", + 394 : "System", + 395 : "Temperature", + 396 : "Derating temperature", + 397 : "Operating condition temperatures", + 398 : "Time", + 399 : "Sensor fault fan permanently on", + 400 : "Temperature", + 401 : "Highest measured temperature", + 402 : "Phases L1 and L2", + 403 : "Phases L1 and L3", + 404 : "Phases L2 and L3", + 405 : "Set total time", + 406 : "Phase total Current", + 407 : "Operating condition current", + 408 : "Set operating time", + 409 : "Operating time", + 410 : "Set feed-in time", + 411 : "Feed-in time", + 412 : "Apparent power", + 413 : "Reactive power", + 414 : "Operating condition reactive power", + 416 : "Power", + 417 : "Absorbed energy", + 418 : "Total yield", + 419 : "Operating condition power", + 422 : "Transformer", + 423 : "Validation system", + 424 : "Update Bluetooth", + 425 : "Update Bluetooth failed", + 426 : "Update completed", + 427 : "Update main CPU", + 428 : "Update main CPU failed", + 429 : "Update RS485I module", + 430 : "Update RS485I module failed", + 431 : "Update communication", + 432 : "Update language table", + 433 : "Update language table failed", + 434 : "Update display", + 435 : "Update display failed", + 436 : "Apparent power", + 437 : "Reactive power", + 438 : "VDE0126-1-1", + 439 : "Special setting VDE0126-1-1", + 440 : "Manufacturer", + 441 : "Voltage", + 442 : "Voltage monitoring", + 443 : "Constant voltage", + 444 : "Voltage limit", + 445 : "Maximum voltage", + 446 : "Operating condition voltage", + 447 : "Voltage setpoint", + 448 : "Power per phase", + 449 : "Nominal voltage", + 450 : "Power", + 451 : "Operating condition power", + 452 : "Web service", + 455 : "Warning", + 456 : "Waiting for DC start conditions", + 457 : "Waiting for grid voltage", + 458 : "Sunny Central", + 459 : "Overcurrent, input in short-circuit (HW)", + 460 : "Sunny Boy", + 461 : "SMA", + 462 : "On-board supply sys. dist. 15V", + 463 : "Check DC generator", + 464 : "DC overvoltage (HW)", + 465 : "DC switch", + 466 : "DC overvoltage", + 467 : "DC overcurrent", + 468 : "Fault CAN Initialisation", + 469 : "Fault DA converter", + 470 : "Fault reverse current", + 471 : "Fault IPC communication", + 472 : "Fault RTC Initialization", + 473 : "Fault overvoltage protector", + 474 : "Ground fault detected", + 475 : "Frequent watchdog fault", + 476 : "Internal timing fault", + 477 : "Reverse current", + 478 : "Check inverter electronics", + 479 : "Check inv. electr. and comm.", + 480 : "Check inverter electr. and fan", + 481 : "Chk inv.electr. and contactors", + 482 : "Check inv.electr. and SW vers.", + 483 : "Communication fault CAN", + 484 : "Communication fault IPC", + 485 : "Fan fault", + 486 : "Fan fault interior", + 487 : "Fan fault interior 2", + 488 : "Fan fault heat sink", + 489 : "Fan fault heat sink 2", + 490 : "Fan fault motor prot. switch", + 491 : "Fan fault coilware", + 492 : "Fan fault coilware 2", + 493 : "Check grid and fuses", + 494 : "Check grid frequency", + 495 : "Maximum grid frequency disturbance", + 496 : "Maximum grid frequency disturbance", + 497 : "Minimum grid frequency disturbance", + 498 : "Minimum grid frequency disturbance", + 499 : "Check grid voltage", + 500 : "Grid overvoltage fast", + 501 : "Grid undervoltage fast", + 502 : "Smoke alarm", + 503 : "Fault sensor ambient temp.", + 504 : "Fault sensor battery temp.", + 505 : "Fault sensor DC voltage", + 506 : "Fault sensor pow.unit temp2", + 507 : "Fault sensor fan perm. on", + 508 : "Fault sensor transf. temp.", + 509 : "Synchronization error", + 510 : "Team switch", + 511 : "Overvoltage EVR (HW)", + 512 : "Overvoltage protector", + 513 : "Check overvoltage protector", + 514 : "Overcurrent EVR (HW)", + 515 : "Overtemperature outside", + 516 : "Overtemperature battery", + 517 : "Overtemperature EVR (HW)", + 518 : "Overtemp. power unit (HW)", + 519 : "Overtemperature power unit 2", + 520 : "Overtemp. transformer area", + 521 : "Overtemperature switch", + 522 : "Invalid device address", + 523 : "Invalid parameter file", + 524 : "Unspecified HW fault (HW)", + 525 : "Watchdog BFR", + 526 : "Watchdog DSP", + 527 : "Display self-test start mess.", + 528 : "Insolation", + 529 : "Insolation on external sensor", + 530 : "Set total yield", + 531 : "Set absorbed energy", + 532 : "Set highest measured temperature", + 533 : "Communication version", + 534 : "Generator control", + 535 : "String protection defective", + 536 : "DC converter string C defective", + 537 : "EvtTmpPwrLim", + 538 : "Grid impedance jump", + 539 : "EvtExlIn", + 540 : "Ground fuse missing", + 541 : "DC current sensor C offset", + 542 : "Internal meas. comp. fault", + 543 : "Internal meas. comp. fault", + 544 : "Internal meas. comp. fault", + 545 : "Internal meas. comp. fault", + 546 : "Meas. recording fault", + 547 : "Grid fault reported", + 548 : "Team relay defective", + 549 : "Team instable", + 550 : "Team config", + 551 : "Team coupling", + 552 : "Team disconnection", + 553 : "Team head config", + 554 : "Team error", + 555 : "Grid impedance too high", + 556 : "EvtGriOp", + 557 : "Temperature derating", + 558 : "SB 3000TL-20", + 559 : "VDE0126-1-1 B", + 560 : "EN50438", + 561 : "EN50438-CZ", + 562 : "C10/11", + 563 : "Day yield", + 564 : "Set number of grid connections", + 565 : "Power specif. via char. curve", + 566 : "Temporal control of the power limitation", + 567 : "Reset operating data (for Service)", + 568 : "Execute all", + 569 : "Activated", + 570 : "Execute write operation", + 571 : "Write events on memory card", + 572 : "Write faults to SD", + 577 : "Specific plant yield", + 578 : "Performance ratio", + 579 : "Revenue", + 580 : "CO2 avoidance", + 581 : "Specific inverter yield", + 582 : "Active power limitation", + 583 : "Checking firmware", + 584 : "Access Control", + 585 : "Parameter '%s' set successfully", + 586 : "Setting of parameter '%s' failed", + 587 : "Parameter '%s' set successfully", + 588 : "Setting of parameter '%s' failed", + 589 : "Parameter '%s' set successfully", + 590 : "Setting of parameter '%s' failed", + 591 : "Set user password", + 592 : "Set installer password", + 593 : "Set service password", + 594 : "Set developer password", + 595 : "Update successful", + 596 : "Update failed", + 597 : "Time adjusted / old time", + 598 : "Time adjusted / new time", + 599 : "Update to ver.|s0| successful", + 661 : "Minimum", + 662 : "Maximum", + 663 : "Sum", + 664 : "Average", + 665 : "Number of devices", + 666 : "Piece/yield", + 667 : "Minimum of lower limits", + 668 : "Maximum of lower limits", + 669 : "Minimum of upper limits", + 670 : "Maximum of upper limits", + 671 : "Minimum of current values", + 672 : "Maximum of current values", + 700 : "Hardware Interface 1", + 701 : "Hardware Interface 2", + 702 : "Hardware Interface 3", + 703 : "Time settings", + 704 : "Update", + 705 : "Device update", + 706 : "Status and Actions", + 707 : "User settings", + 708 : "Basic settings", + 709 : "SunnyDNS", + 710 : "FTP Push", + 711 : "FTP server", + 714 : "DNS server IP", + 715 : "Gateway IP", + 716 : "IP Address", + 717 : "Subnet mask", + 718 : "WAN IP", + 719 : "Internet service provider IP", + 722 : "Standard or Daylight Saving Time", + 724 : "Date format", + 725 : "Language", + 726 : "Unit of length", + 727 : "Number format", + 728 : "Unit of temperature", + 729 : "Time format", + 730 : "Standard/Daylight Saving Time conversion on", + 731 : "Automatic time synchronization", + 732 : "Time zone", + 733 : "Activated", + 734 : "Time interval", + 735 : "Login", + 736 : "Password", + 737 : "Server", + 738 : "Connection test", + 739 : "GPRS-APN", + 740 : "Dial-in number", + 741 : "Dial-in password", + 742 : "PIN", + 743 : "Signal strength test", + 744 : "Upload data", + 745 : "Result of the last upload", + 746 : "Portal connection test", + 747 : "Result of the last portal connection test", + 748 : "Register", + 749 : "Result of the last registration", + 750 : "Email", + 751 : "Plant name", + 752 : "Plant ID", + 753 : "Upload frequency", + 754 : "Write access allowed", + 755 : "GPRS-Always-On activated", + 766 : "DD.MM.YYYY", + 767 : "MM/DD/YYYY", + 768 : "YYYY.MM.DD", + 769 : "MM.DD.YYYY", + 770 : "DD/MM/YYYY", + 771 : "YYYY/MM/DD", + 772 : "DD-MM-YYYY", + 773 : "YYYY-MM-DD", + 774 : "MM-DD-YYYY", + 775 : "HH:mm", + 776 : "hh:mm", + 777 : "Deutsch", + 778 : "English", + 779 : "Italiano", + 780 : "Espanol", + 781 : "Francais", + 782 : "????????", + 783 : "???", + 784 : "Cesky", + 785 : "Portugues", + 786 : "Nederlands", + 787 : "123456.0", + 788 : "123456", + 789 : "123456", + 790 : "123,456.0", + 791 : "Celsius", + 792 : "Kelvin", + 793 : "Fahrenheit", + 794 : "Metric", + 795 : "Imperial", + 796 : "Slovenski", + 797 : "?????????", + 798 : "Polski", + 799 : "???", + 800 : "Disabled", + 801 : "???????", + 802 : "Active", + 803 : "Inactive", + 804 : "?????", + 805 : "HH:mm:ss", + 806 : "hh:mm:ss", + 830 : "Status", + 831 : "Type Label", + 832 : "Device", + 833 : "User Rights", + 834 : "DC Side", + 835 : "AC Side", + 836 : "Grid Monitoring", + 837 : "Generator", + 838 : "Battery", + 839 : "Plant Communication", + 840 : "External Communication", + 841 : "Data Recording", + 842 : "Sunny Portal", + 843 : "Further Applications", + 844 : "Meteorology", + 845 : "Theft Protection", + 846 : "Device Components", + 847 : "Equipment & device control system", + 861 : "User", + 862 : "Installer", + 863 : "Service", + 864 : "Developer", + 867 : "10MBit", + 868 : "100MBit", + 869 : "Bluetooth", + 870 : "Speedwire", + 871 : "Analog modem", + 872 : "GSM", + 873 : "Interface GPRS", + 874 : "UMTS", + 875 : "Software package", + 876 : "Total yield logger partially deleted", + 877 : "Find device", + 878 : "LED blinking", + 879 : "Components", + 880 : "SUSyID", + 881 : "Sunny Explorer", + 882 : "Plant name", + 883 : "Time difference Plant/System", + 884 : "not active", + 885 : "none", + 886 : "none", + 887 : "none", + 888 : "Current event", + 889 : "Bluetooth", + 890 : "Power absorbed", + 891 : "WebBox-20", + 892 : "String failure detection", + 893 : "Signal processor update", + 894 : "Signal processor update failed", + 900 : "Central assembly", + 901 : "Communication assembly", + 902 : "Residual current monitoring unit", + 903 : "Display", + 904 : "Logic component", + 905 : "RS485 module", + 906 : "Zigbee components", + 907 : "Bluetooth component", + 908 : "Operating system", + 909 : "String protection", + 910 : "Protocol converter", + 911 : "Module shaft 1", + 912 : "Webconnect module", + 913 : "Power control modules", + 914 : "Module shaft 2", + 932 : "Initialization Start: |tn0|", + 933 : "Initialization End: |tn0|", + 934 : "Operation Start: |tn0|", + 935 : "Operation End: |tn0|", + 936 : "Shutdown Start: |tn0|", + 937 : "Shutdown End: |tn0|", + 938 : "Internal fault 0x|x4||x5||x6||x7|/|u0|", + 939 : "Calculation overflow in module |u0|", + 940 : "Startup |x5|:|x4|:|x3|:|x2|:|x1|:|x0|:|5u8|", + 941 : "Resource |s0| could not load.", + 942 : "Server could not start|x5|:|x4|:|x3|:|x2|:|x1|:|x0|:|5u8|", + 943 : "24h Timeout at parameter setting |ln8c|", + 944 : "Login OK: Level: |u8|Device: |x1||x0||x7||x6||x5||x4|", + 945 : "Logout: Level: |u8|Device:|x1||x0||x7||x6||x5||x4|", + 946 : "Auth. error: Level: |u8|Device: |x1||x0||x7||x6||x5||x4|", + 947 : "Login error: Level: |u8|Device: |x1||x0||x7||x6||x5||x4|Error: |dc|", + 948 : "Detection started", + 949 : "New Device: |x1||x0||x7||x6||x5||x4|", + 950 : "Rec. Hello World: Device: |x1||x0||x7||x6||x5||x4|", + 951 : "Sent Hello World", + 952 : "New Object Unknown: Device: |x1||x0| |x7||x6||x5||x4| - |x9||x8|", + 953 : "New Master: Device: |x1||x0||x7||x6||x5||x4|", + 954 : "Login started: Level: |u0|", + 955 : "Login End: Level: |u0|", + 956 : "Logout started: Level: |u0|", + 957 : "Bluetooth connection to root node established", + 958 : "Bluetooth connection to root node terminated", + 959 : "Bluetooth connection has broken", + 960 : "New dev.info ['|x5|:|x4|:|x3|:|x2|:|x1|:|x0|'], Type", + 961 : "Conn.device ['|x5|:|x4|:|x3|:|x2|:|x1|:|x0|'] lost", + 962 : "BCN: Radio network established", + 963 : "Server prob. not available until |s0| due to maint", + 964 : "Server not available: |d0|", + 965 : "Registration failed |d0|", + 966 : "|d0| bytes successfully transmitted in |d4| sec.", + 967 : "Data succ. transmitted: |d0| bytes in |d4| seconds", + 968 : "Data transm. aborted after |d0| bytes and |d4| sec. flt: |d8|", + 969 : "Time synchronization failed |d0|", + 970 : "Faulty parameter '%s'", + 971 : "Internal ODB fault |d0|", + 972 : "Ensure DC supply", + 973 : "------", + 974 : "String protection", + 975 : "Restart Learning", + 976 : "Reverse currents or substring |s0| polarity rev.", + 977 : "Substring |s0| failed", + 978 : "ESS-IGBT defective Input |s0|", + 979 : "ESS Relay fault Input |s0|", + 980 : "Relay control defective input |s0|", + 981 : "HSS-IGBT defective input |s0|", + 982 : "Ring line interrupted", + 983 : "Short-circuit String A, risk of arc.Don't pull ESS", + 984 : "Arcing danger DC plug input; reinsert ESS", + 985 : "Varistor A or B through thermal fuse", + 986 : "Varistor A or B through arcing", + 987 : "Lightning protection device at input A defective", + 988 : "Overheating", + 989 : "String |s0| defective", + 990 : "String |s0| defective", + 991 : "Lightning protection inactive", + 992 : "Overheating", + 993 : "Reverse currents or input |s0| polarity reversed", + 994 : "Phase(s) or neutral conductor not connected", + 995 : "Do not disconnect ESS", + 996 : "Connect ESS, do not open cover", + 997 : "Check varistors", + 998 : "Check lightning protector A/PE", + 999 : "Disconnect device from generator and grid", + 1000 : "New device cannot be administered: Device: |x1||x0| |x7||x6||x5||x4|", + 1001 : "Parameter |ln8c| was sent", + 1002 : "Bluetooth Repeater", + 1003 : "Intermediate circuit voltages not permitted", + 1004 : "Grid type detection failed", + 1005 : "Grid current sensor defective", + 1006 : "General fault", + 1007 : "Overcurrent Ground fuse", + 1008 : "MSD", + 1009 : "G77", + 1010 : "AGL", + 1011 : "UL1741", + 1012 : "UL1741/2005", + 1013 : "Other standard", + 1014 : "DK5950", + 1015 : "UL1741/2001", + 1016 : "Constant current", + 1017 : "PEA", + 1018 : "MEA", + 1020 : "Medium-Voltage Directive (Germany)", + 1021 : "Airdolphine", + 1022 : "Operating mode of static voltage stabilisation", + 1023 : "Operating mode active power red., overfreq. P(f)", + 1024 : "Configuration of static voltage stabilization", + 1025 : "Config. reactive power/voltage charact. curve Q(U)", + 1026 : "Voltage difference to next charact. curve value", + 1027 : "Time to adoption of next charact. curve value", + 1028 : "Nominal voltage offset", + 1029 : "Reactive power gradient", + 1030 : "Stop voltage", + 1031 : "Reactive power setpoint Q", + 1032 : "MVtgDirective Internal", + 1033 : "Configuration of reactive power mode Q(P)", + 1034 : "Reactive power value starting point", + 1035 : "Active power value starting point", + 1036 : "Reactive power value end point", + 1037 : "Active power value end point", + 1038 : "Configuration of cosPhi, direct specification", + 1039 : "cosPhi specification", + 1040 : "Excitation type of cosPhi", + 1041 : "Overexcited", + 1042 : "Underexcited", + 1043 : "Configuration of cosPhi(P) characteristic", + 1044 : "Excitation type at starting point", + 1045 : "cosPhi of start point", + 1046 : "Excitation type at end point", + 1047 : "cosPhi of end point", + 1048 : "Configuration of feed-in management", + 1049 : "Configuration of active power P limitation", + 1050 : "Power per phase", + 1051 : "Reference value for normalized active power", + 1052 : "Config. active power reduct. at overfrequency P(f)", + 1053 : "Config. of linear instantaneous power gradient", + 1054 : "Difference between starting freq. and grid freq.", + 1055 : "Active power gradient", + 1056 : "Difference between reset frequency and grid freq.", + 1057 : "Activation of stay-set indicator function", + 1058 : "Synchronize time with portal", + 1059 : "Current reactive power limit", + 1060 : "Max. reactive power", + 1061 : "Currently set apparent power limit", + 1062 : "Maximum apparent power", + 1063 : "Current cosPhi limit", + 1064 : "Min. cosPhi", + 1065 : "Power gradient for reconnection after grid fault", + 1066 : "Curr. active power grad., reconnection, grid fault", + 1067 : "Config. of reactive power Q, direct specification", + 1068 : "Activation of active power gradient", + 1069 : "React. power/volt. char. Q(U)", + 1070 : "Reactive power Q, direct spec.", + 1071 : "React. power const. Q in kvar", + 1072 : "Q specified by plant control", + 1073 : "Reactive power Q(P)", + 1074 : "cosPhi, direct specific.", + 1075 : "cosPhi, specified by plant control", + 1076 : "cosPhi(P) characteristic", + 1077 : "Active power limitation P in W", + 1078 : "Act. power lim. as % of Pmax", + 1079 : "Act. power lim. via plant ctrl", + 1080 : "Plant control", + 1081 : "AC circuit breaker activated or open", + 1082 : "Installation failure grid connection", + 1083 : "Check grid and rotating field", + 1084 : "Rotating field left", + 1085 : "Primary Master", + 1086 : "Secondary Master", + 1087 : "Device control", + 1088 : "Data inconsistency", + 1089 : "AC bridge not ready for operation", + 1090 : "UCE monitoring HF half-bridge", + 1091 : "Bridge short circuit HF half-bridge", + 1092 : "Voltage conditions not met", + 1093 : "Fault sensor PCB temperature", + 1094 : "External fan fault", + 1095 : "Boost converter", + 1096 : "Boost converter 2", + 1097 : "Maximum phase shift", + 1098 : "Tripping time for max. phase shift", + 1099 : "Max. frequency change per second", + 1100 : "Tripping time for max. frequency change per second", + 1101 : "Lower reconnection limit", + 1102 : "Upper limit for reconnection", + 1103 : "Nominal frequency", + 1104 : "Normalized total apparent power", + 1105 : "Boost converter", + 1106 : "Overtemperature boost converter", + 1107 : "Frequency change not permitted", + 1108 : "Calibration voltage", + 1109 : "Calibration status", + 1110 : "AC voltage calibration failed", + 1111 : "Failed", + 1112 : "Not executed", + 1113 : "Ground fuse fault", + 1114 : "Check ground fuse", + 1115 : "Replace fan", + 1116 : "Clean fan", + 1117 : "Varistor monitoring defective", + 1118 : "Calibration failed", + 1119 : "Selection of the voltage(s) to be calibrated", + 1120 : "Maximum ground current", + 1121 : "Tripping time maximum ground current", + 1122 : "Grounding prescribed?", + 1123 : "Prescribed grounding type", + 1124 : "Grounding status", + 1125 : "Positive", + 1126 : "Negative", + 1127 : "No grounding", + 1128 : "None prescribed", + 1129 : "Yes", + 1130 : "No", + 1131 : "Grid relay open", + 1132 : "Linear gradient", + 1133 : "Configuration of reactive power mode const. Q as %", + 1134 : "Configuration of active power mode const. W as %", + 1135 : "Update string prot. failed", + 1136 : "Update string prot. failed", + 1137 : "Update string protection", + 1138 : "Update string protection", + 1139 : "UL1741/2005, 208 V", + 1140 : "UL1741/2005, 240 V", + 1141 : "Grounding error", + 1142 : "Wrong earthing type", + 1143 : "Wrong earthing type; check earthing set", + 1144 : "Set plant time", + 1145 : "Use Sunny Portal", + 1146 : "Execute", + 1147 : "Not initialized", + 1148 : "Not connected", + 1149 : "Searching", + 1150 : "Not connected", + 1151 : "Connected", + 1152 : "Not connected", + 1153 : "Not connected", + 1154 : "Firmware update completed", + 1163 : "Zigbee", + 1164 : "Bluetooth", + 1165 : "Ethernet", + 1166 : "Serial", + 1167 : "Speedwire", + 1170 : "Downlink", + 1171 : "Uplink", + 1173 : "Current root node", + 1174 : "Possible root nodes", + 1175 : "Connection quality", + 1176 : "Status", + 1177 : "Fault indication", + 1179 : "NetID", + 1180 : "Current event", + 1186 : "DC current measurement defective", + 1187 : "every 15 minutes", + 1188 : "hourly", + 1189 : "daily", + 1190 : "Syslog service", + 1191 : "Activated until", + 1192 : "Config. reactive power mode of plant control", + 1193 : "Configuration of active power mode of plant ctrl", + 1194 : "cosPhi config. method of plant control", + 1195 : "Time-out for communication fault indication", + 1196 : "Export", + 1197 : "Measurement name in local language", + 1198 : "Events in local language", + 1199 : "PPDS", + 1200 : "String protection OK", + 1201 : "OptiTrac Global Peak", + 1202 : "Cycle time", + 1203 : "Power limit", + 1204 : "Setpoint for PV voltage", + 1205 : "Reference voltage", + 1206 : "Setpoint for intermediate circuit voltage", + 1207 : "Update file", + 1208 : "Adaptor: Conn. interrupted", + 1209 : "Adapter event: NetID changed", + 1210 : "Not connected", + 1211 : "Not connected", + 1212 : "Not connected", + 1213 : "DAA type ID", + 1214 : "RAA type ID", + 1215 : "Time interval expired", + 1216 : "Dynamic settings established", + 1217 : "Hysteresis voltage", + 1218 : "Gradient K of reactive current droop", + 1219 : "Specified voltage UQ0", + 1220 : "Symmetrical limit for maximum reactive power", + 1221 : "PWM reverse voltage", + 1222 : "Reactive power gradient", + 1223 : "Adjustment time for charact. curve operating point", + 1224 : "Active power at starting point", + 1225 : "Active power at end point", + 1226 : "Reference correction voltage", + 1227 : "PWM inverse voltage", + 1228 : "PWM inversion delay", + 1229 : "Reactive current droop", + 1230 : "Configuration of dynamic grid support", + 1231 : "NetID changed: New ID: |x3||x2||x1||x0|", + 1232 : "Check for update and install it", + 1233 : "SDLWindV", + 1234 : "Diagnosis", + 1235 : "CPU monitor", + 1236 : "CPU load", + 1237 : "Server path", + 1238 : "Update started", + 1239 : "Search for update completed successfully", + 1240 : "Search for update not compl. successfully: |tn0|", + 1241 : "Update aborted", + 1242 : "Download of an update started", + 1243 : "Download of an update completed successfully", + 1244 : "Download of update not compl. successfully: |tn0|", + 1245 : "Version unknown", + 1246 : "Incorrect package size", + 1247 : "One or more parameters are missing", + 1248 : "Next connection", + 1249 : "Next connection 2", + 1250 : "Next connection 3", + 1251 : "IRE status", + 1252 : "Adaptor event: Conn. restored", + 1253 : "Lower limit, voltage dead band", + 1254 : "Upper limit, voltage dead band", + 1255 : "Grid voltage fault", + 1256 : "Update Speedwire", + 1257 : "Update Speedwire", + 1258 : "Result of the last connection test", + 1259 : "Short-circuit String B, risk of arc.Don't pull ESS", + 1260 : "Lightning protection device at input B defective", + 1261 : "Check lightning protector B/PE", + 1262 : "Configuration of full dynamic grid support", + 1263 : "Operating mode of dynamic grid support", + 1264 : "Full dynamic grid support", + 1265 : "Limited dynamic grid support", + 1266 : "mA", + 1267 : "km/h", + 1268 : "mph", + 1269 : "kOhm", + 1270 : "mOhm", + 1271 : "mV", + 1272 : "kVA", + 1273 : "kvar", + 1274 : "kW/m2", + 1275 : "kW", + 1276 : "MW", + 1277 : "GW", + 1278 : "MWh", + 1279 : "GWh", + 1280 : "Ws", + 1281 : "g", + 1282 : "kg", + 1283 : "lb.", + 1284 : "t", + 1285 : "m2", + 1286 : "sq.ft.", + 1287 : "m", + 1288 : "km", + 1289 : "ft.", + 1290 : "g/kWh", + 1291 : "kg/kWh", + 1292 : "kWh/kWp", + 1293 : "Ah", + 1294 : "As", + 1296 : "New update available: Version |s0|", + 1297 : "FTP Push connection test successfully completed.", + 1298 : "An error occurred in the FTP Push conn. test: |s0|", + 1299 : "FTP server started.", + 1300 : "FTP server stopped.", + 1301 : "An error occurred when start. the FTP server: |s0|", + 1302 : "An error occurred when stop. the FTP server: |s0|", + 1303 : "FTP upload completed.", + 1304 : "An error occurred during the FTP upload: |s0|", + 1305 : "Learning", + 1306 : "Update Speedwire module failed", + 1307 : "Server not available", + 1308 : "Registration failed: |tn0|", + 1309 : "Time synchronization failed: |tn0|", + 1310 : "Data transmission aborted: |tn0|", + 1311 : "Unknown device class", + 1312 : "System fault", + 1313 : "Authentication fault", + 1314 : "Web service not available", + 1315 : "Sunny WebBox not registered with the Sunny Portal", + 1316 : "Update version of the software package", + 1317 : "Automatic update", + 1318 : "Time of the automatic update", + 1319 : "No version available", + 1320 : "Identification of WAN IP via Sunny Portal failed", + 1321 : "Measures in the case of a grounding error", + 1322 : "Disconnect from grid", + 1323 : "Warn", + 1324 : "Server not available: |lv04|", + 1325 : "Data export in CSV format", + 1326 : "Data export in XML format", + 1327 : "Displacement power factor", + 1328 : "Reactive power droop mode", + 1329 : "Voltage spread", + 1330 : "Hysteresis", + 1331 : "Dead band", + 1332 : "Frequency monitoring", + 1333 : "Unbalance detection", + 1334 : "Permissible grid unbalance", + 1335 : "Tripping time of the unbalance detection", + 1336 : "Tripping time of the frequency monitor", + 1337 : "UL1741/120", + 1338 : "UL1741/208", + 1339 : "UL1741/240", + 1340 : "Multifunction relay", + 1341 : "Fault indication", + 1342 : "Fan control", + 1343 : "Self-consumption", + 1344 : "Minimum On time", + 1345 : "Minimum On power", + 1346 : "Minimum power On time", + 1347 : "Operating mode", + 1348 : "Initiate device restart", + 1349 : "Control via communication", + 1350 : "Configuration of 'Turbine' operating mode", + 1351 : "Constant deviation of power calculation", + 1352 : "Coefficient of power calculation based on Udc", + 1353 : "Coefficient of power calculation based on Udc^2", + 1354 : "Coefficient of power calculation based on Udc^3", + 1355 : "Proportional factor of power control", + 1356 : "Integral factor of power control", + 1357 : "Differential factor of power control", + 1358 : "Critical voltage to end feed-in", + 1359 : "Battery bank", + 1360 : "Minimum time before reconnection", + 1361 : "0,001", + 1362 : "0,000001", + 1363 : "Rate for feed-in reimbursement", + 1364 : "Rate for self-use reimbursement", + 1365 : "Electricity tariff", + 1366 : "Feed-in reimbursement", + 1367 : "Self-use reimbursement", + 1368 : "Electricity costs", + 1369 : "Login prescribed", + 1370 : "Transformer prot. triggered", + 1371 : "Quick stop triggered", + 1372 : "CAN communication fault", + 1374 : "AC separating point", + 1376 : "Invalid analogue active power specification", + 1377 : "Invalid digital active power specification", + 1378 : "Invalid analogue reactive power specification", + 1379 : "Invalid analogue power factor specification", + 1380 : "Doors have been opened during operation", + 1387 : "Reactive power Q, specific. via analogue input", + 1388 : "cosPhi specific. analog input", + 1389 : "Reactive power/volt. char. Q(U) param.", + 1390 : "Active power limitation P via analogue input", + 1391 : "Active power limitation P via digital inputs", + 1392 : "Fault", + 1393 : "Waiting for PV voltage", + 1394 : "Waiting for valid AC grid", + 1395 : "DC range", + 1396 : "AC grid", + 1397 : "Check active and reactive power interface", + 1398 : "Close doors and cancel release", + 1399 : "Check external transformer", + 1400 : "DC overvoltage peak", + 1401 : "Displacement power factor", + 1402 : "Excitation type of cosPhi", + 1403 : "Energy counter type", + 1404 : "PV generation counter", + 1405 : "Grid feed-in counter", + 1406 : "Grid reference counter", + 1407 : "PV plant measurements", + 1408 : "Supplied power", + 1409 : "Frequency threshold", + 1410 : "Grip impedance monitoring", + 1411 : "Current amplitude", + 1412 : "Max. impedance gradient", + 1413 : "Periodic AID", + 1414 : "Amplitude", + 1415 : "Normalized upper threshold", + 1416 : "Normalized lower threshold", + 1417 : "Norm. max. reconnection threshold", + 1418 : "Norm. min. reconnection threshold", + 1419 : "Specific CO2 reduction", + 1420 : "Grid type", + 1421 : "Impedance", + 1422 : "Centre point of P-coordinate", + 1423 : "Centre point of U-coordinate", + 1424 : "End point of U-coordinate", + 1425 : "Start point of U-coordinate", + 1426 : "Ripple control signal detection", + 1427 : "Ground voltage", + 1428 : "Backup mode operating mode", + 1429 : "Backup mode status", + 1430 : "PowerBalancer", + 1431 : "PowerGuard", + 1432 : "Power max. Value", + 1433 : "277 Volt", + 1434 : "208 Volt", + 1435 : "240 Volt", + 1436 : "208 Volt without neutral conductor", + 1437 : "240 Volt without neutral conductor", + 1438 : "Automatic", + 1439 : "In all phases", + 1440 : "Grid mode", + 1441 : "Separate network mode", + 1442 : "PhaseGuard", + 1443 : "PowerGuard", + 1444 : "FaultGuard", + 1445 : "Passive AID", + 1446 : "GridGuard-Version", + 1447 : "S0-pulse feed-in", + 1448 : "S0-pulse reference", + 1449 : "Insulation failure ignored", + 1450 : "Grounding error ignored", + 1451 : "Insulation monitoring configuration error", + 1452 : "No Remote GFDI response", + 1453 : "Permanent insulation failure", + 1454 : "Soft grounding triggered", + 1455 : "Emergency OFF", + 1456 : "Consumption", + 1457 : "Internal consumption increase", + 1458 : "Rise in self-consumption today", + 1459 : "PV mains connection", + 1460 : "Power supply status", + 1461 : "Mains connected", + 1462 : "Backup not available", + 1463 : "Backup", + 1464 : "Rated capacity throughput", + 1465 : "Lower battery charge limit", + 1466 : "Wait", + 1467 : "Start", + 1468 : "MPP search", + 1469 : "Shut down", + 1470 : "Fault", + 1471 : "Warning/error mail OK", + 1472 : "Warning/error mail not OK", + 1473 : "System info mail OK", + 1474 : "System info mail not OK", + 1475 : "Error mail OK", + 1476 : "Error mail not OK", + 1477 : "Warning mail OK", + 1478 : "Warning mail not OK", + 1479 : "Wait after mains interruption", + 1480 : "Waiting for utilities company", + 1481 : "Sunny central control", + 1482 : "Plant is not available in the portal", + 1483 : "Device is already available in the portal", + 1484 : "Invalid parameter was transmitted", + 1485 : "Unknown device version", + 1486 : "No rights to execute the operation", + 1487 : "Device successfully registered in the portal", + 1488 : "SSM Id for which comm. fault has occurred", + 1489 : "SMU warning code for string fault", + 1490 : "Status of signal contact 1", + 1491 : "Status of signal contact 2", + 1492 : "String temporarily deselected due to earth fault", + 1493 : "String permanently deselected due to earth fault", + 1494 : "Eliminate earth fault", + 1495 : "String detected w. yield loss", + 1496 : "Check functionality of the whole string", + 1497 : "Watchdog SMID CONT", + 1498 : "Watchdog SMID CT", + 1499 : "On-board supply sys. dist. 12V", + 1500 : "Replace SD card with a functional one", + 1501 : "Transmission attempts", + 1502 : "Time of the last transmission attempt", + 1503 : "Update source", + 1504 : "Automatic update", + 1505 : "Manual update", + 1506 : "Update portal", + 1507 : "Information", + 1508 : "90% of DC switch. cycl. reach.", + 1509 : "100% of DC switch. cyc. reach.", + 1510 : "Compress", + 1511 : "Compress and delete", + 1512 : "The data export to the SD card was temporarily deactivated as at least one update file is on the SD card.", + 1513 : "The data export to the SD card was re-activated.", + 1514 : "The firmware update with version number |u/9/0| for device type |tn4| was sent to the plant.", + 1515 : "The firmware update with version number |u/9/0| for device types |tn4| and |tn8| was sent to the plant.", + 1516 : "The firmware update with version number |u/9/0| for device types |tn4|, |tn8| and |tnc| was sent to the plant.", + 1517 : "The device with serial number |u0| was successfully updated to firmware version |u/9/4|.", + 1518 : "The device with serial number |u0| could not be updated to firmware version |u/9/4|.", + 1519 : "The device with serial number |u0| and firmware version |u/9/4| is no longer considered for the current update process as this appliance update has failed several times.", + 1520 : "Download of the update file with version |u/9/0| for device type |tn4| did not complete successfully.", + 1521 : "Download of the update file with version |u/9/0| for device types |tn4| and |tn8| did not complete successfully.", + 1522 : "Download of the update file with version |u/9/0| for device types |tn4|, |tn8| and |tnc| did not complete successfully.", + 1523 : "Wait for update conditions", + 1524 : "S0 pulses per infed kWh", + 1525 : "S0 pulses per consumed kWh", + 1526 : "Target version of the software package", + 1527 : "Force", + 1528 : "Reconnection fault grid", + 1529 : "Reconnection condition not satisfied", + 1530 : "208V WYE", + 1531 : "Grid switch", + 1532 : "Status after loss of communication in autom. mode", + 1533 : "Status after switching on automatic", + 1535 : "Jet offline test", + 1536 : "Timeout after loss of communication", + 1537 : "Starting current detection", + 1538 : "Gradient K of the reactive current droop", + 1539 : "Automatic mode can be activated", + 1540 : "Quick stop", + 1541 : "Unspecif. DSP fault", + 1542 : "Unspecif. BFR fault", + 1543 : "Overtemperature DC clamps", + 1544 : "A DC switch does not close", + 1545 : "Insulation failure stack", + 1546 : "Warning grounding error", + 1547 : "Permanent grounding error", + 1548 : "Warning softgrounding activated", + 1549 : "Permanent softgrounding error", + 1550 : "Softgrounding ignored", + 1551 : "Softgrounding hardware fault", + 1552 : "Warning insulation failure", + 1553 : "Do not change", + 1554 : "Disturbance sensor display temperature", + 1555 : "No display", + 1556 : "Display fault (temp. too low)", + 1557 : "Display fault (unknown display type)", + 1558 : "Display fault (display not connected)", + 1559 : "Run update", + 1560 : "Remote control disconnection active", + 1562 : "Phase L1 against L2", + 1563 : "Offset to AC current", + 1564 : "K1", + 1565 : "K1+K2", + 1566 : "K1+K2+K3", + 1567 : "K1+K2+K3+K4", + 1568 : "K1+K2+K4", + 1569 : "K1+K3", + 1570 : "K1+K3+K4", + 1571 : "K1+K4", + 1572 : "K2", + 1573 : "K2+K3", + 1574 : "K2+K3+K4", + 1575 : "K2+K4", + 1576 : "K3", + 1577 : "K3+K4", + 1578 : "K4", + 1579 : "Restart in |u0| seconds", + 1580 : "Please connect AC and DC", + 1581 : "Feeding", + 1582 : "Reset measured values", + 1583 : "Current flow direction", + 1584 : "Positive", + 1585 : "Negative", + 1586 : "Sunny portal occupied", + 1587 : "Network fault", + 1588 : "Reactive power specification", + 1589 : "cosPhi specification", + 1590 : "Active special functionality", + 1591 : "Control of wireless sockets", + 1592 : "Overtemperature throttle range", + 1593 : "Update file successfully copied", + 1594 : "The passwords of the device reset to factory s.", + 1595 : "Network settings of device reset to the factory s.", + 1596 : "The device has been reset to the factory setting", + 1597 : "The memory card is full or write-protected", + 1598 : "Transformer incorrectly connected", + 1599 : "Grid disconnection at 0% specification", + 1600 : "Conf. of the plant control procedure P(U)", + 1601 : "Characteristic curve number", + 1602 : "Conf. of the grid integr. characteristic curves", + 1603 : "Characteristic 1", + 1604 : "Adjustm. time characteristic curve operating point", + 1605 : "Decrease ramp", + 1606 : "Increase ramp", + 1607 : "Number of points to be used", + 1608 : "X value 1", + 1609 : "Y value 1", + 1610 : "X value 2", + 1611 : "Y value 2", + 1612 : "X value 3", + 1613 : "Y value 3", + 1614 : "X value 4", + 1615 : "Y value 4", + 1616 : "X value 5", + 1617 : "Y value 5", + 1618 : "X value 6", + 1619 : "Y value 6", + 1620 : "Active power gradient after reset frequency", + 1621 : "Active power gradient connection", + 1623 : "Root node", + 1624 : "Password recovery failed: Device: |u4|", + 1625 : "Sunny Portal", + 1626 : "NTP server", + 1627 : "Alarm", + 1628 : "Modbus", + 1629 : "TCP server", + 1630 : "UDP server", + 1631 : "DHCP server", + 1632 : "Repeated notification", + 1633 : "Email address(es)", + 1634 : "Send test e-mail", + 1635 : "Upload interval", + 1636 : "Start IP address", + 1637 : "End IP address", + 1638 : "Time synchronisation source", + 1639 : "Inputs/outputs", + 1640 : "Digital input group 1 (DI1 .. DI4)", + 1641 : "Digital input group 2 (DI5 .. DI8)", + 1642 : "Analogue current input 1", + 1643 : "Analogue current input 2", + 1644 : "Analogue current input 3", + 1645 : "Analogue voltage input 1", + 1646 : "Remaining space", + 1647 : "Detect string failures", + 1648 : "Detect partial string failures", + 1649 : "String |s0| low power", + 1650 : "Partial string |s0| low power", + 1651 : "Overvoltage suppression", + 1652 : "Minimum power reduction", + 1653 : "Sensor fault", + 1654 : "Offset DCinAC defective", + 1655 : "Electric arc detected", + 1656 : "Serial el.arc in String |s0| detected by AFCI mod.", + 1657 : "AFCI self-test failed", + 1658 : "AFCI self-test successful", + 1659 : "Knock to reset", + 1660 : "AFCI switched on", + 1661 : "Status: |tn0| / |tn4| / |tn8|", + 1662 : "Measuring interface 1", + 1663 : "Measuring interface 2", + 1664 : "Measuring interface 3", + 1665 : "Connection point", + 1666 : "Type of counter", + 1667 : "S0 reference", + 1668 : "S0 feed-in", + 1669 : "D0 reference", + 1670 : "D0 feed-in", + 1671 : "D0 reference and feed-in", + 1672 : "Activate island mode", + 1673 : "Not enough energy available from wind generator", + 1674 : "IsNotLogin response: Device: |x1||x0| |x7||x6||x5||x4|", + 1675 : "Login status response: Device: |x1||x0| |x7||x6||x5||x4|, status: |xb||xa||x9||x8|", + 1676 : "IsNotLogin query started", + 1677 : "LoginStatus query started", + 1678 : "Devices LoginStatus query: Device: |x1||x0| |x7||x6||x5||x4|", + 1679 : "Devices Login started: Device: |x1||x0| |x7||x6||x5||x4|, Level: |u8|", + 1680 : "Devices Logout started: Device: |x1||x0| |x7||x6||x5||x4|, Level: |xb||xa||x9||x8|", + 1681 : "MandatoryData queried: Device: |x1||x0| |x7||x6||x5||x4|, Obj: |x9||x8|, LRI: |xf||xe||xd||xc|", + 1682 : "Minimum insolation", + 1683 : "Maximum insolation", + 1684 : "Minimum measurement", + 1685 : "Maximum measurement", + 1686 : "Insolation sensor", + 1687 : "USB connection", + 1688 : "USB connection 2", + 1689 : "Energy absorbed today", + 1690 : "Fast shut-down", + 1691 : "Type", + 1692 : "String deactivated due to WMax", + 1693 : "No string connected", + 1694 : "DC switch tripped", + 1695 : "DC switch waits for connection", + 1696 : "DC switch blocked by spindle", + 1697 : "DC switch manually blocked", + 1698 : "DC switch 3 x tripped", + 1699 : "DC switch defective", + 1700 : "Key switch", + 1701 : "Local time", + 1702 : "DC switch", + 1703 : "String status", + 1704 : "Derating due to temperature", + 1705 : "Derating due to frequency", + 1706 : "Derating due to PV current", + 1707 : "SMA radio socket has switched to the status Automatic", + 1708 : "Speedwire", + 1709 : "SMACOM A", + 1710 : "SMACOM B", + 1711 : "Connection speed", + 1712 : "Current IP address", + 1713 : "Current subnet mask", + 1714 : "Current gateway address", + 1715 : "Current DNS server address", + 1716 : "Webconnect", + 1717 : "MAC address", + 1718 : "Automatic configuration switched on", + 1719 : "Faulty communication", + 1720 : "10 Mbit/s", + 1721 : "100 Mbit/s", + 1722 : "IRE faulty", + 1723 : "Supplied power (calculated)", + 1724 : "Counter status generation counter (calculated)", + 1725 : "No connection", + 1726 : "Half duplex", + 1727 : "Full duplex", + 1728 : "Duplex mode", + 1729 : "Communication error power unit", + 1730 : "Battery values", + 1731 : "Energy absorbed charge amount", + 1732 : "Charge amount released", + 1733 : "Charge", + 1734 : "Active charging process", + 1735 : "Maintenance", + 1736 : "Full and compensation charge", + 1737 : "Charging status", + 1738 : "Absorbed energy", + 1739 : "Energy released", + 1740 : "Generator", + 1741 : "Automatic start", + 1742 : "Reason for requesting generator", + 1743 : "Generator measured values", + 1744 : "Manual control", + 1745 : "Generator queries charge level", + 1746 : "Switch-off limit in time range 1", + 1747 : "Switch-off limit in time range 2", + 1748 : "Number of starts", + 1749 : "Full stop", + 1750 : "Current rise in self-consumption", + 1751 : "Current self-consumption", + 1752 : "Current capacity", + 1753 : "Time for quick charge", + 1754 : "Time for compensation charge", + 1755 : "Time for full charge", + 1756 : "Maximum start attempts after error", + 1757 : "Maximum charging current", + 1758 : "Current set charging voltage", + 1759 : "Nominal capacity", + 1760 : "Maximum temperature", + 1761 : "Nominal current", + 1762 : "Generator request via power", + 1763 : "Switch-off power", + 1764 : "Switch-on power", + 1765 : "Operating status", + 1766 : "Rise in self-consumption switched on", + 1767 : "Quick charge", + 1768 : "Full charge", + 1769 : "Compensation charge", + 1770 : "Maintenance charge", + 1771 : "Charge with solar power", + 1772 : "Charge w. solar a. mains power", + 1773 : "No request", + 1774 : "Load", + 1775 : "Time control", + 1776 : "Manual one hour", + 1777 : "Manual start", + 1778 : "External source", + 1779 : "Separated", + 1780 : "Public electricity mains", + 1781 : "Island mains", + 1782 : "Sealed lead battery (VRLA)", + 1783 : "Flooded lead acid batt. (FLA)", + 1784 : "Nickel/Cadmium (NiCd)", + 1785 : "Lithium-Ion (Li-Ion)", + 1787 : "Initialisation", + 1788 : "Ready", + 1789 : "Warming", + 1790 : "Synchronisation", + 1791 : "Activated", + 1792 : "Resynchronisation", + 1793 : "Generator separation", + 1794 : "Slow down", + 1795 : "Bolted", + 1796 : "Blocked after error", + 1797 : "Energy release today", + 1798 : "Grid creating generator", + 1799 : "No", + 1800 : "Generator", + 1801 : "Mains", + 1802 : "Mains and generator", + 1803 : "Invalid configuration for the PV generation counter", + 1804 : "Invalid configuration for the mains infeed counter", + 1805 : "Invalid configuration for the mains consumption counter", + 1806 : "Error in data transfer from the PV gen. counter", + 1807 : "Error in data transfer from mains infeed counter", + 1808 : "Error in data transfer from mains consump. counter", + 1809 : "Invalid configuration for the load control", + 1810 : "The communiction key has been replaced", + 1811 : "System time has been reset", + 1812 : "Serial number of the Bluetooth root node is |u0|-Zu4|", + 1813 : "Bluetooth connection quality to root node: |d0|%", + 1814 : "There is no Bluetooth conn.", + 1815 : "Establishing Bluetooth connection to root node", + 1816 : "Bluetooth connection failed", + 1817 : "Bluetooth connection to radio socket faulty. Load control not possible.", + 1818 : "Power outage", + 1819 : "Number of DC current measurement units", + 1820 : "USB connection 1", + 1821 : "External measurement of the insulation resistance", + 1822 : "Alignment not homogeneous", + 1823 : "PV connection not configured", + 1824 : "Arc detection triggered", + 1826 : "Encryption", + 1827 : "SSH", + 1828 : "Trigger time synchronisation", + 1829 : "Always SSL/TLS", + 1830 : "SSL/TLS if possible", + 1831 : "No encryption", + 1832 : "Connection to mains establ.", + 1833 : "Connection to mains disconn.", + 1834 : "Connection to mains collapsed", + 1835 : "Fault sensor module temp.", + 1836 : "Fan life-time reached", + 1837 : "Error in pyranometer", + 1838 : "Pyranometer calibr. necessary", + 1839 : "Backup mode switched on", + 1840 : "Communication to the Sunny Portal was disrupted for |s0| hours", + 1841 : "An error occurred in communication to the Sunny Portal: Error Code |d0|", + 1842 : "Transfer of the Firmware update Version number |u/9/0| for device type |tn4|. The transfer can take some time.", + 1843 : "Error when transferring the Firmware update Version number |u/9/0| for device type |tn4|. Procedure aborted.", + 1844 : "IGMP switched on", + 1845 : "Speedwire Group", + 1846 : "|tn4|: network busy", + 1847 : "|tn4|: network overloaded", + 1848 : "|tn4|: faulty data packages", + 1849 : "|tn4|: communication status goes to |tn8|", + 1850 : "|tn4|: communication disrupted", + 1851 : "|tn4|: connection speed goes to |tn8|", + 1852 : "|tn4|: duplex mode goes to |tn8|", + 1853 : "Load reduced through device reduction or increase of query interval", + 1854 : "Standalone operation", + 1855 : "Standalone operation", + 1856 : "Analogue current input 4", + 1857 : "Analogue voltage input 2", + 1858 : "Analogue voltage input 3", + 1859 : "Analogue voltage input 4", + 1860 : "Energy consumed internally", + 1861 : "|tn4|: Network load OK", + 1862 : "high", + 1863 : "Hardware recognition failed", + 1864 : "Unknown hardware ID", + 1865 : "Update transport started", + 1866 : "Update transport successful", + 1867 : "Update transport failed", + 1868 : "Communication with device currently not possible. Device may be switched off.", + 1869 : "Encrypt communication", + 1870 : "Communication monitoring signal", + 1871 : "Last successful upload", + 1872 : "every 30 minutes", + 1873 : "every 2 hours", + 1874 : "every 4 hours", + 1875 : "every 6 hours", + 1876 : "every 8 hours", + 1877 : "every 12 hours", + 1878 : "BT chip has been reinitialised", + 1879 : "Hardware ID", + 1880 : "Monitoring server", + 1881 : "Remove device", + 1882 : "Remove all devices", + 1883 : "Device", + 1887 : "System environment", + 1888 : "STP208HW12", + 1889 : "STP208HW13", + 1890 : "STP208HW14", + 1891 : "STP208HW15", + 1892 : "SMTP", + 1893 : "Active power", + 1894 : "Reactive power", + 1895 : "Digital inputs", + 1896 : "Analogue inputs", + 1897 : "Modbus", + 1898 : "Active power limits are being sent to plant", + 1899 : "Reactive power spec. is being sent to plant", + 1900 : "Invalid spec. for |tn0| from |tn4|", + 1901 : "New active power limits from |tn0| parameter |tn4| value |d8|%", + 1902 : "New reactive power spec. from |tn0| parameter |tn4| value |f8|%", + 1903 : "New reactive power spec. from |tn0| parameter |tn4| value |f8|", + 1904 : "New reactive power spec. from |tn0| parameter |tn4| value |tn8|", + 1905 : "Default status for |tn0| active", + 1906 : "Wind direction", + 1908 : "Reset communication data", + 1909 : "Delete system parameters in SPI", + 1910 : "Delete ODB experts in SPI", + 1911 : "Delete HP images in SPI", + 1912 : "Delete all firmware blocks in SPI", + 1913 : "Clear SPI flashes", + 1914 : "Reset CP", + 1915 : "Webconnect update", + 1916 : "Webconnect update failed", + 1917 : "PID quick stop triggered", + 1918 : "Self-test failed", + 1919 : "Insulation monitoring error", + 1920 : "Check insulation monitoring", + 1921 : "First error stack", + 1922 : "Stack error", + 1923 : "Cluster controller registration failed: |tn0|", + 1924 : "Cluster controller registered", + 1925 : "New devices registered", + 1926 : "Registration of new devices failed", + 1927 : "Heartbeat signal sending failed", + 1928 : "Network latency too high", + 1929 : "Test at maximum voltage", + 1930 : "Alarm at active power limit", + 1931 : "Alarm at reactive power specification", + 1932 : "Filter settings", + 1933 : "E-mail sent", + 1934 : "E-mail sending failed", + 1935 : "Alarm triggered. Event |d0| at device |d4|:|d8|", + 1936 : "Result of the last e-mail dispatch", + 1937 : "Set NetID: |s0|", + 1938 : "NetID 1 not supported", + 1939 : "|tn0| logs in from |ip4|", + 1940 : "Login from |ip0| failed", + 1941 : "|ip0| successfully logged off", + 1942 : "Parameter '%s' set successfully. |d4| to |d8|", + 1943 : "Parameter '%s' set successfully. |tn4| to |tn8|", + 1944 : "Parameter '%s' set successfully. Old value: |s4|", + 1945 : "Parameter '%s' set successfully. New value: |s4|", + 1946 : "Setting of parameter '%s' failed. |d4| to |d8|", + 1947 : "Setting of parameter '%s' failed. |tn4| to |tn8|", + 1948 : "Setting of parameter '%s' failed. Old value: |s4|", + 1949 : "Setting of parameter '%s' failed. New value: |s4|", + 1950 : "Watchdog reset |s0|", + 1951 : "Memory card detected (capacity|d0| MB, free |d4| MB)", + 1952 : "Memory card removed", + 1953 : "Memory card nearly full (capacity|d0| MB, free |d4| MB)", + 1954 : "Memory card write-protected", + 1955 : "Memory card not detected or not readable", + 1956 : "Memory card full (capacity|d0| MB, free |d4| MB)", + 1957 : "Memory card full", + 1958 : "No memory card inserted", + 1959 : "Syslog service act. until: |ts0|", + 1960 : "Error occurred in the FTP Push conn. test: |tn0|", + 1961 : "Target computer does not respond", + 1962 : "Connection rejected by target computer", + 1963 : "Too many users logged in", + 1964 : "Incorrect user name or password", + 1965 : "File/directory not found or no access rights", + 1966 : "An error occurred during FTP upload: |tn0|", + 1967 : "Energy values for device |u0|:|u4| for time from |ts8| to |tsc| resent", + 1968 : "Active power limitation: |b0|%/|b1|%/|b2|% (|x9|-|x8|-|x7|-|x6|-|x5|-|x4|)", + 1969 : "Reactive power spec.: |b0|%/|b1|%/|b2|% (|x9|-|x8|-|x7|-|x6|-|x5|-|x4|)", + 1970 : "cosPhi spec.: |f0| (|x9|-|x8|-|x7|-|x6|-|x5|-|x4|)", + 1971 : "Unknown PRB attempted to change |tn0|", + 1972 : "Communication error: Contact to device |tn0|, SN: |u4| overdue (last contact: |ts8|)", + 1973 : "X-axes reference", + 1974 : "Y-axes reference", + 1975 : "Voltage in V", + 1976 : "Voltage in percentages of Un", + 1977 : "Var in percentages of Pmax", + 1978 : "Power in percentages of Pmax", + 1979 : "Power in percentages of frozen active power", + 1980 : "X value 7", + 1981 : "Y value 7", + 1982 : "X value 8", + 1983 : "Y value 8", + 1984 : "Reactive power/volt. char. Q(U) with meas. points", + 1985 : "Conf. of reactive power/volt. char. Q(U) with meas. points", + 1986 : "Activating active power", + 1987 : "Deactivating active power", + 1988 : "Min. time until activation of active power gradient after reset frequency", + 1989 : "Activating voltage", + 1990 : "Deactivating voltage", + 1991 : "Characteristic 2", + 1992 : "Plant switches to status |tn0|", + 1993 : "directly after 'Upload'", + 1994 : "Active power currently limited due to AC current limitation", + 1995 : "EPS relay fails to open or close", + 1996 : "Modbus |tn0| started", + 1997 : "Modbus |tn0| terminated", + 1998 : "An error occurred during startup of Modbus |tn0|: port |d4|", + 1999 : "Modbus configurations file |s0| successfully loaded", + 2000 : "Modbus configurations file |s0| corrupt", + 2001 : "SMACOM C", + 2002 : "SMACOM D", + 2003 : "Power control module", + 2004 : "Status of digital inputs", + 2005 : "Remote shut-down", + 2006 : "Input C defective", + 2007 : "Fault in wind sensor", + 2008 : "Lower frequ. deviation limit prior to active power release", + 2009 : "Upper frequ. dev. limit prior to active power release", + 2010 : "Application-ID", + 2011 : "Switchable maximum threshold", + 2012 : "Switchable maximum threshold tripping time", + 2013 : "Switchable minimum threshold", + 2014 : "Switchable minimum threshold tripping time", + 2015 : "Operating mode of frequency restriction", + 2016 : "Source of quick shut-down", + 2017 : "Frequency band narrowing", + 2018 : "IEC 61850 configuration", + 2019 : "GOOSE configuration", + 2020 : "61850 GOOSE", + 2021 : "Alarm upon warnings", + 2022 : "Alternative e-mail sender address (optional)", + 2023 : "Absolute", + 2024 : "In percentages", + 2025 : "File upload successful: |s0|", + 2026 : "File upload failed: |s0|", + 2027 : "File upload rejected: |s0|", + 2028 : "NSS quick stop: |tn0| through |tn4| is executed", + 2029 : "Webconnect enabled", + 2030 : "Webconnect disabled", + 2031 : "Webconnect error: no connection", + 2032 : "Default gateway not configured", + 2033 : "DNS server not configured", + 2034 : "No reply to DNS request", + 2035 : "SIP proxy DNS resolution failed", + 2036 : "Stun server DNS resolution failed", + 2037 : "No reply to request to Stun server", + 2038 : "No reply to SIP option packs", + 2039 : "Registration rejected by SIP registrar", + 2040 : "Storage medium detected at USB2; USB1 not assigned, thus no logging", + 2042 : "Power per phase", + 2043 : "Reactive power setpoint Q", + 2044 : "Change excitation type", + 2045 : "Do not change excitation type", + 2046 : "Internal SD card defective", + 2047 : "Data retention refresh started", + 2048 : "Data retention refresh completed", + 2049 : "NSS not forwarded, is deactivated", + 2050 : "Invalid NSS command", + 2051 : "Invalid RPC command", + 2052 : "E-mail could not be sent. SMTP error code: |d0|", + 2053 : "Digital input group 1 (DI1 .. DI4)", + 2054 : "Digital input group 2 (DI5 .. DI8)", + 2055 : "DI1", + 2056 : "DI1 DI2", + 2057 : "DI1 DI2 DI3", + 2058 : "DI1 DI2 DI3 DI4", + 2059 : "DI1 DI2 DI4", + 2060 : "DI1 DI3", + 2061 : "DI1 DI3 DI4", + 2062 : "DI1 DI4", + 2063 : "DI2", + 2064 : "DI2 DI3", + 2065 : "DI2 DI3 DI4", + 2066 : "DI2 DI4", + 2067 : "DI3", + 2068 : "DI3 DI4", + 2069 : "DI4", + 2070 : "DI5", + 2071 : "DI5 DI6", + 2072 : "DI5 DI6 DI7", + 2073 : "DI5 DI6 DI7 DI8", + 2074 : "DI5 DI6 DI8", + 2075 : "DI5 DI7", + 2076 : "DI5 DI7 DI8", + 2077 : "DI5 DI8", + 2078 : "DI6", + 2079 : "DI6 DI7", + 2080 : "DI6 DI7 DI8", + 2081 : "DI6 DI8", + 2082 : "DI7", + 2083 : "DI7 DI8", + 2084 : "DI8", + 2085 : "Unknown SIP registry", + 2086 : "Webconnect error: Default gateway not configured", + 2087 : "Webconnect error: DNS server not configured", + 2088 : "Webconnect error: No reply to DNS request |s0|", + 2089 : "Webconnect error: Unknown SIP proxy |s0|", + 2090 : "Webconnect error: Unknown STUN server", + 2091 : "Webconnect error: No reply to request to STUN server", + 2092 : "Webconnect error: No reply to SIP option packs", + 2093 : "Webconnect error: Registration rejected by SIP registrar", + 2094 : "Webconnect error: Unknown SIP registrar |s0|", + 2095 : "Webconnect error: Faulty communication", + 2096 : "STP208HW11", + 2097 : "Phase L2 gegen L3", + 2098 : "Phase L3 against L1", + 2099 : "Power unit supplies data", + 2100 : "Power limitation to prevent load unbalance", + 2101 : "Insulation measurement", + 2102 : "Frequency restr. due to timeout in |tn0|", + 2103 : "Inconsistent communication version", + 2104 : "Invalid device combination", + 2105 : "Max. permitted number of inverters exceeded", + 2106 : "Generator voltage too high", + 2107 : "Mirror new complete pack", + 2108 : "Remove inverter or delete removed inverters", + 2110 : "Unsupported data system to |s0|", + 2111 : "Voltage increase protection trigger time", + 2112 : "Goose-Mac address", + 2113 : "Grid connection point", + 2114 : "Active power specification", + 2115 : "Reactive power specification", + 2116 : "Active power limitation", + 2117 : "Generator control", + 2118 : "Configuration of generator control", + 2119 : "Derating", + 2120 : "RCD adjustment", + 2121 : "30 mA", + 2122 : "40 mA", + 2123 : "50 mA", + 2124 : "60 mA", + 2125 : "70 mA", + 2126 : "80 mA", + 2127 : "90 mA", + 2128 : "100 mA", + 2130 : "Connection establishment", + 2131 : "Data transfer from PV generation counter OK", + 2132 : "Data transfer from mains infeed counter OK", + 2133 : "Data transfer from mains consumption counter OK", + 2134 : "Nominal power", + 2135 : "Operating mode of active power limitation", + 2136 : "Dynamic active power limitation", + 2137 : "Fixed specification in Watt", + 2138 : "Fixed specification in percentages", + 2139 : "Characteristic curve", + 2140 : "Set active power limit", + 2141 : "Configuration of network switch failed", + 2142 : "Configuration of network switch successful", + 2143 : "Relay defect confirmed", + 2144 : "Dynamic active power limitation started.", + 2145 : "Dynamic active power limitation terminated.", + 2146 : "Act. power limitation deactivated due to incorr. configuration.", + 2147 : "Supplied power: |u0| W (permitted value: |u4| W)", + 2148 : "Active power was limited today for |u0| minutes.", + 2149 : "Fault: act. power limitation activated w/o regard to own consumption", + 2150 : "Own consumption again included for active power limitation.", + 2151 : "Permanent 0 W grid supply measured No active power limitation possible, if meter is faulty", + 2152 : "Inverter fails to respond to act. power limitation.", + 2153 : "Self-test started manually", + 2154 : "Manually started self-start completed successfully", + 2155 : "Supplied power has exceeded the permitted value several times.", + 2156 : "Registration of the SIP registry has not responded", + 2157 : "Webconnect error: registration of the SIP registry has not responded", + 2158 : "Permanent freq. restriction", + 2159 : "Condition test successful", + 2160 : "Condition test failed", + 2161 : "STP267HW07", + 2162 : "Number of section switches", + 2163 : "Time to open", + 2164 : "Time to close", + 2165 : "Section switches", + 2166 : "DC overvoltage because of power limit", + 2167 : "Max. operating cycles of the GFDI reached", + 2168 : "Replace GFDI", + 2169 : "React. power spec.: |d0|%/|d4|%/|d8|%", + 2170 : "cos Phi spec.: |f0|/|f4|/|f8| |tnc|", + 2171 : "Discharge cut-off voltage", + 2172 : "Maximum charge current", + 2173 : "Maximum discharge current", + 2174 : "Cell charge nominal voltage for boost charge", + 2175 : "Cell charge nominal voltage for full charging", + 2176 : "Cell charge nominal voltage for equalisation", + 2177 : "Cell charge nominal voltage for trickle charge", + 2178 : "Hysteresis minimum threshold", + 2179 : "Hysteresis maximum threshold", + 2180 : "Maximum reverse power", + 2181 : "Maximum reverse power tripping time", + 2182 : "Measurements of external power connection", + 2183 : "Mains oper. without feed back", + 2184 : "Energy saving in the network", + 2185 : "End energy saving in the network", + 2186 : "Start energy saving in the network", + 2187 : "|tn0| logs on", + 2188 : "Access to |x0||x1|:|x2||x3|:|x4||x5|:|x6||x7|:|x8||x9|:|xa||xb|:|xc||xd|:|xe||xf|", + 2189 : "Logging on |x0||x1|:|x2||x3|:|x4||x5|:|x6||x7|:|x8||x9|:|xa||xb|:|xc||xd|:|xe||xf| failed", + 2190 : "|x0||x1|:|x2||x3|:|x4||x5|:|x6||x7|:|x8||x9|:|xa||xb|:|xc||xd|:|xe||xf| was successfully logged off", + 2191 : "Minimum operating temperature not reached", + 2192 : "Update Zigbee", + 2193 : "Zigbee update failed", + 2194 : "Energy transfer not possible", + 2195 : "Number of UCP events", + 2196 : "Derating because of low DC voltage", + 2197 : "Derating because of power and DC voltage", + 2198 : "Bias voltage", + 2199 : "Relative", + 2200 : "Nominal apparent voltage", + 2201 : "Low-voltage powerline", + 2202 : "Medium-voltage powerline", + 2203 : "Transformer", + 2204 : "Ohmic resistive coating", + 2205 : "Inductive reactive coating", + 2206 : "Capacitive reactive coating", + 2207 : "Short-circuit voltage", + 2208 : "Nominal voltage of the high-voltage side", + 2210 : "Length of the low-voltage powerline", + 2211 : "Length of the medium-voltage powerline", + 2212 : "AC voltage measurement on the inverter side less than a mains voltage measurement", + 2213 : "AC voltage measurement on the mains side less than inverter side voltage measurement", + 2214 : "AC voltage measurement of the mains limit controller is defective", + 2215 : "AC elect. measurem. is defect.", + 2216 : "AC voltage measurement shows a whine on mains side", + 2217 : "Defective precharging circuit", + 2218 : "Check AC meas. cable and AC voltage sensing", + 2221 : "Check AC cable and AC power meas.", + 2222 : "Check parameter settings of the DSP", + 2223 : "Check precharg. circuit and fuses", + 2224 : "Precharging circuit active", + 2225 : "Pure reactive power operation, QonDemand active", + 2226 : "Reverse current detection (immed. separation from the PV field)", + 2227 : "General firmware error: |u0|", + 2228 : "General firmware warning: |u0|", + 2229 : "General error: |u0|/|u4|/|u8|/|uc|", + 2230 : "Capacitor self-test failed", + 2231 : "Check capacitor", + 2232 : "SMA energy meter purchase", + 2233 : "SMA energy meter supply", + 2234 : "PIC", + 2235 : "SMA energy meter purchase and supply", + 2236 : "PLC communication impaired", + 2237 : "PLC communication impaired", + 2238 : "Power line communication", + 2239 : "Package error rate", + 2240 : "Intermediate circuit voltages not permitted (SW)", + 2241 : "Wrong config: simultan. communication via bluetooth and speedwire", + 2242 : "Overvoltage suppression P (U)", + 2243 : "Overvoltage suppression Q (U)", + 2244 : "Serial Number", + 2245 : "Active power limitation", + 2246 : "D1: 0 | D2: 0 | D3: 0 | D4: 0", + 2247 : "D1: 1 | D2: 0 | D3: 0 | D4: 0", + 2248 : "D1: 0 | D2: 1 | D3: 0 | D4: 0", + 2249 : "D1: 1 | D2: 1 | D3: 0 | D4: 0", + 2250 : "D1: 0 | D2: 0 | D3: 1 | D4: 0", + 2251 : "D1: 1 | D2: 0 | D3: 1 | D4: 0", + 2252 : "D1: 0 | D2: 1 | D3: 1 | D4: 0", + 2253 : "D1: 1 | D2: 1 | D3: 1 | D4: 0", + 2254 : "D1: 0 | D2: 0 | D3: 0 | D4: 1", + 2255 : "D1: 1 | D2: 0 | D3: 0 | D4: 1", + 2256 : "D1: 0 | D2: 1 | D3: 0 | D4: 1", + 2257 : "D1: 1 | D2: 1 | D3: 0 | D4: 1", + 2258 : "D1: 0 | D2: 0 | D3: 1 | D4: 1", + 2259 : "D1: 1 | D2: 0 | D3: 1 | D4: 1", + 2260 : "D1: 0 | D2: 1 | D3: 1 | D4: 1", + 2261 : "D1: 1 | D2: 1 | D3: 1 | D4: 1", + 2262 : "Release time", + 2263 : "Release value", + 2264 : "Error tolerance time", + 2265 : "Oper. mode of the stat.volt.maint. for Q on demand", + 2266 : "Conf. of the static voltage maint. at Q on demand", + 2267 : "Gradient K of react. current stat. for undervolt.", + 2268 : "Gradient K of reactive current stat. for overvolt.", + 2269 : "Reactive power charact. curve", + 2270 : "cos Phi or Q specification through optimum plant control", + 2271 : "Active power as a percentage of Pmax", + 2272 : "cos Phi (EEI convention)", + 2273 : "Conf. of the reactive power characteristic curve", + 2274 : "Activation of the characteristic curve", + 2275 : "Activation threshold", + 2276 : "Deactivation threshold", + 2277 : "Threshold reference", + 2278 : "Supporting points of the characteristic curve 1", + 2279 : "Supporting points of the characteristic curve 2", + 2280 : "Supporting points of the characteristic curve 3", + 2281 : "X value", + 2282 : "Y value", + 7500 : "Enel-GUIDA", + 7501 : "RD1663/661-A", + 7502 : "IEC61727/MEA", + 7503 : "IEC61727/PEA", + 7504 : "SI4777-2", + 7505 : "CGC/GF001", + 7506 : "VDE0126-1-1/UTE", + 7507 : "KEMCO502/2009", + 7510 : "VDE-AR-N4105", + 7511 : "GB-T19939-2005", + 7512 : "G59/2", + 7513 : "VDE-AR-N4105-MP", + 7514 : "VDE-AR-N4105-HP", + 7515 : "KEMCO501/2009", + 7516 : "CEI 0-21", + 7517 : "CEI 0-21 internal", + 7518 : "CEI 0-21 external", + 7519 : "UL1741/2010/277", + 7520 : "UL1741/2010/120", + 7521 : "UL1741/2010/240", + 7522 : "NEN-EN50438", + 7523 : "C10/11/2012", + 7524 : "RD1699", + 8000 : "All Devices", + 8001 : "Solar Inverter", + 8002 : "Wind Turbine Inverter", + 8007 : "Battery Inverter", + 8033 : "Consumer", + 8064 : "Sensor System in General", + 8065 : "Electricity meter", + 8128 : "Communication products", + 8500 : "ROOT", + 8501 : "VMEM", + 8502 : "PWT", + 8503 : "COM", + 8504 : "FWCHK", + 8505 : "COND", + 8506 : "SET", + 8507 : "LOGDEV", + 8508 : "LOGOBJ", + 8509 : "TAGS", + 8510 : "PHYDEV", + 8511 : "PHYOBJ", + 8512 : "LEXE", + 8513 : "LEXN", + 8514 : "LEXA", + 8515 : "LS", + 8530 : "Counter object", + 8531 : "5-min value object", + 8532 : "Instantaneous value object", + 8533 : "Status object", + 8534 : "Parameter object", + 8535 : "PV string group mean values", + 8536 : "String list object", + 8537 : "PV string group parameter", + 8538 : "PV string group instantaneous values", + 8539 : "Adaptor information", + 8540 : "Data logger object energy", + 8541 : "Data logger object event", + 8542 : "Data logger object daily energy", + 8543 : "Event counter object", + 8544 : "Event object", + 8545 : "Taglist object", + 8546 : "Time setting object", + 8547 : "Plant control parameter", + 8548 : "PV voltage level mean values", + 8549 : "PV voltage level instantaneous values", + 8550 : "PV voltage level parameter", + 8551 : "Plant control object", + 8552 : "PV voltage level status", + 8553 : "Data logger object supplied power", + 8554 : "Data logger object consumed power", + 8555 : "Data logger object supplied daily power", + 8556 : "Data logger object consumed daily power", + 8557 : "External plant control object", + 9000 : "SWR 700", + 9001 : "SWR 850", + 9002 : "SWR 850E", + 9003 : "SWR 1100", + 9004 : "SWR 1100E", + 9005 : "SWR 1100LV", + 9006 : "SWR 1500", + 9007 : "SWR 1600", + 9008 : "SWR 1700E", + 9009 : "SWR 1800U", + 9010 : "SWR 2000", + 9011 : "SWR 2400", + 9012 : "SWR 2500", + 9013 : "SWR 2500U", + 9014 : "SWR 3000", + 9015 : "SB 700", + 9016 : "SB 700U", + 9017 : "SB 1100", + 9018 : "SB 1100U", + 9019 : "SB 1100LV", + 9020 : "SB 1700", + 9021 : "SB 1900TLJ", + 9022 : "SB 2100TL", + 9023 : "SB 2500", + 9024 : "SB 2800", + 9025 : "SB 2800i", + 9026 : "SB 3000", + 9027 : "SB 3000US", + 9028 : "SB 3300", + 9029 : "SB 3300U", + 9030 : "SB 3300TL", + 9031 : "SB 3300TL HC", + 9032 : "SB 3800", + 9033 : "SB 3800U", + 9034 : "SB 4000US", + 9035 : "SB 4200TL", + 9036 : "SB 4200TL HC", + 9037 : "SB 5000TL", + 9038 : "SB 5000TLW", + 9039 : "SB 5000TL HC", + 9040 : "Convert 2700", + 9041 : "SMC 4600A", + 9042 : "SMC 5000", + 9043 : "SMC 5000A", + 9044 : "SB 5000US", + 9045 : "SMC 6000", + 9046 : "SMC 6000A", + 9047 : "SB 6000US", + 9048 : "SMC 6000UL", + 9049 : "SMC 6000TL", + 9050 : "SMC 6500A", + 9051 : "SMC 7000A", + 9052 : "SMC 7000HV", + 9053 : "SB 7000US", + 9054 : "SMC 7000TL", + 9055 : "SMC 8000TL", + 9056 : "SMC 9000TL-10", + 9057 : "SMC 10000TL-10", + 9058 : "SMC 11000TL-10", + 9059 : "SB 3000 K", + 9060 : "Unknown device", + 9061 : "SensorBox", + 9062 : "SMC 11000TLRP", + 9063 : "SMC 10000TLRP", + 9064 : "SMC 9000TLRP", + 9065 : "SMC 7000HVRP", + 9066 : "SB 1200", + 9067 : "STP 10000TL-10", + 9068 : "STP 12000TL-10", + 9069 : "STP 15000TL-10", + 9070 : "STP 17000TL-10", + 9071 : "SB 2000HF-30", + 9072 : "SB 2500HF-30", + 9073 : "SB 3000HF-30", + 9074 : "SB 3000TL-21", + 9075 : "SB 4000TL-21", + 9076 : "SB 5000TL-21", + 9077 : "SB 2000HFUS-30", + 9078 : "SB 2500HFUS-30", + 9079 : "SB 3000HFUS-30", + 9080 : "SB 8000TLUS", + 9081 : "SB 9000TLUS", + 9082 : "SB 10000TLUS", + 9083 : "SB 8000US", + 9084 : "WB 3600TL-20", + 9085 : "WB 5000TL-20", + 9086 : "SB 3800US-10", + 9087 : "Sunny Beam BT11", + 9088 : "Sunny Central 500CP", + 9089 : "Sunny Central 630CP", + 9090 : "Sunny Central 800CP", + 9091 : "Sunny Central 250U", + 9092 : "Sunny Central 500U", + 9093 : "Sunny Central 500HEUS", + 9094 : "Sunny Central 760CP", + 9095 : "Sunny Central 720CP", + 9096 : "Sunny Central 910CP", + 9097 : "SMU8", + 9098 : "STP 5000TL-20", + 9099 : "STP 6000TL-20", + 9100 : "STP 7000TL-20", + 9101 : "STP 8000TL-10", + 9102 : "STP 9000TL-20", + 9103 : "STP 8000TL-20", + 9104 : "SB 3000TL-JP-21", + 9105 : "SB 3500TL-JP-21", + 9106 : "SB 4000TL-JP-21", + 9107 : "SB 4500TL-JP-21", + 9108 : "SCSMC", + 9109 : "SB 1600TL-10", + 9110 : "SSM US", + 9111 : "SMA radio-controlled socket", + 9112 : "WB 2000HF-30", + 9113 : "WB 2500HF-30", + 9114 : "WB 3000HF-30", + 9115 : "WB 2000HFUS-30", + 9116 : "WB 2500HFUS-30", + 9117 : "WB 3000HFUS-30", + 9118 : "VIEW-10", + 9119 : "Sunny Home Manager", + 9120 : "SMID", + 9121 : "Sunny Central 800HE-20", + 9122 : "Sunny Central 630HE-20", + 9123 : "Sunny Central 500HE-20", + 9124 : "Sunny Central 720HE-20", + 9125 : "Sunny Central 760HE-20", + 9126 : "SMC 6000A-11", + 9127 : "SMC 5000A-11", + 9128 : "SMC 4600A-11", + 9129 : "SB 3800-11", + 9130 : "SB 3300-11", + 9131 : "STP 20000TL-10", + 9132 : "SMA CT Meter", + 9133 : "SB 2000HFUS-32", + 9134 : "SB 2500HFUS-32", + 9135 : "SB 3000HFUS-32", + 9136 : "WB 2000HFUS-32", + 9137 : "WB 2500HFUS-32", + 9138 : "WB 3000HFUS-32", + 9139 : "STP 20000TLHE-10", + 9140 : "STP 15000TLHE-10", + 9141 : "SB 3000US-12", + 9142 : "SB 3800US-12", + 9143 : "SB 4000US-12", + 9144 : "SB 5000US-12", + 9145 : "SB 6000US-12", + 9146 : "SB 7000US-12", + 9147 : "SB 8000US-12", + 9148 : "SB 8000TLUS-12", + 9149 : "SB 9000TLUS-12", + 9150 : "SB 10000TLUS-12", + 9151 : "SB 11000TLUS-12", + 9152 : "SB 7000TLUS-12", + 9153 : "SB 6000TLUS-12", + 9154 : "SB 1300TL-10", + 9155 : "Sunny Backup 2200", + 9156 : "Sunny Backup 5000", + 9157 : "Sunny Island 2012", + 9158 : "Sunny Island 2224", + 9159 : "Sunny Island 5048", + 9160 : "SB 3600TL-20", + 9161 : "SB 3000TL-JP-22", + 9162 : "SB 3500TL-JP-22", + 9163 : "SB 4000TL-JP-22", + 9164 : "SB 4500TL-JP-22", + 9165 : "SB 3600TL-21", + 9167 : "Cluster Controller", + 9168 : "SC630HE-11", + 9169 : "SC500HE-11", + 9170 : "SC400HE-11", + 9171 : "WB 3000TL-21", + 9172 : "WB 3600TL-21", + 9173 : "WB 4000TL-21", + 9174 : "WB 5000TL-21", + 9175 : "SC 250", + 9176 : "SMA Meteo Station", + 9177 : "SB 240-10", + 9178 : "SB 240-US-10", + 9179 : "Multigate-10", + 9180 : "Multigate-US-10", + 9181 : "STP 20000TLEE-10", + 9182 : "STP 15000TLEE-10", + 9183 : "SB 2000TLST-21", + 9184 : "SB 2500TLST-21", + 9185 : "SB 3000TLST-21", + 9186 : "WB 2000TLST-21", + 9187 : "WB 2500TLST-21", + 9188 : "WB 3000TLST-21", + 9189 : "WTP 5000TL-20", + 9190 : "WTP 6000TL-20", + 9191 : "WTP 7000TL-20", + 9192 : "WTP 8000TL-20", + 9193 : "WTP 9000TL-20", + 9194 : "STP 12000TL-US-10", + 9195 : "STP 15000TL-US-10", + 9196 : "STP 20000TL-US-10", + 9197 : "STP 24000TL-US-10", + 9198 : "SB 3000TLUS-22", + 9199 : "SB 3800TLUS-22", + 9200 : "SB 4000TLUS-22", + 9201 : "SB 5000TLUS-22", + 9202 : "WB 3000TLUS-22", + 9203 : "WB 3800TLUS-22", + 9204 : "WB 4000TLUS-22", + 9205 : "WB 5000TLUS-22", + 9206 : "SC 500CP-JP", + 9207 : "SC 850CP", + 9208 : "SC 900CP", + 9209 : "SC 850HE-20", + 9210 : "SC 900HE-20", + 9211 : "SC 619CP", + 9212 : "SMA Meteo Station", + 9213 : "SC 800 CP-US", + 9214 : "SC 630 CP-US", + 9215 : "SC 500 CP-US", + 9216 : "SC 720 CP-US", + 9217 : "SC 750 CP-US", + 9218 : "SB 240 Dev", + 9219 : "SB 240-US BTF", + 9220 : "Grid Gate-20", + 9221 : "SC 500 CP-US/600V", + 9222 : "STP 10000TLEE-JP-10", + 9223 : "Sunny Island 6.0H", + 9224 : "Sunny Island 8.0H", + 9225 : "SB 5000SE-10", + 9226 : "SB 3600SE-10", + 9227 : "", + 9228 : "", + 9500 : "(UTC+04:30) Kabul", + 9501 : "(UTC-09:00) Alaska", + 9502 : "(UTC+03:00) Kuwait, Riyardh", + 9503 : "(UTC+04:00) Abu Dhabi, Muskat", + 9504 : "(UTC+03:00) Baghdad", + 9505 : "(UTC-04:00) Atlantic (Canada)", + 9506 : "(UTC+09:30) Darwin", + 9507 : "(UTC+10:00) Canberra, Melbourne, Sydney", + 9508 : "(UTC+04:00) Baku", + 9509 : "(UTC-01:00) Azores", + 9510 : "(UTC-06:00) Saskatchewan", + 9511 : "(UTC-01:00) Cape Verde Islands", + 9512 : "(UTC+04:00) Yerevan", + 9513 : "(UTC+09:30) Adelaide", + 9515 : "(UTC+06:00) Astana, Dhaka", + 9516 : "(UTC-04:00) Manaus", + 9517 : "(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague", + 9518 : "(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb", + 9519 : "(UTC+11:00) Magadan, Solomon Islands, New Caledonia", + 9520 : "(UTC-06:00) Central America", + 9521 : "(UTC-06:00) Guadalajara, Mexico City, Monterrary - Old", + 9522 : "(UTC+08:00) Peking, Chongqing, Hongkong, Urumchi", + 9523 : "(UTC-12:00) (Western) International Date Line", + 9524 : "(UTC+03:00) Nairobi", + 9525 : "(UTC+10:00) Brisbane", + 9526 : "(UTC+02:00) Minsk", + 9527 : "(UTC-03:00) Brasilia", + 9528 : "(UTC-05:00) New York, Miami, Atlanta, Detroit, Toronto", + 9529 : "(UTC+02:00) Cairo", + 9530 : "(UTC+05:00) Jekaterinburg", + 9531 : "(UTC+12:00) Fidschi, Marshall Islands", + 9532 : "(UTC+02:00) Helsinki, Kiev, Riga, Sofia, Tallin, Wilna", + 9533 : "(UTC+04:00) Tiflis", + 9534 : "(UTC) Dublin, Edinburgh, Lisbon, London", + 9535 : "(UTC-03:00) Greenland", + 9536 : "(UTC) Monrovia, Reykjavik", + 9537 : "(UTC+02:00) Athens, Bucharest, Istanbul", + 9538 : "(UTC-10:00) Hawaii", + 9539 : "(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi", + 9540 : "(UTC+03:30) Teheran", + 9541 : "(UTC+02:00) Jerusalem", + 9542 : "(UTC+02:00) Amman", + 9543 : "(UTC+09:00) Seoul", + 9544 : "(UTC+08:00) Kuala Lumpur, Singapore", + 9545 : "(UTC-02:00) Central Atlantic", + 9546 : "(UTC+02:00) Beirut", + 9547 : "(UTC-07:00) Denver, Salt Lake City, Calgary", + 9548 : "(UTC-07:00) Chihuahua, La Paz, Mazatlan - old", + 9549 : "(UTC+06:30) Yangon (Rangoon)", + 9550 : "(UTC+06:00) Novosibirsk", + 9551 : "(UTC+02:00) Windhoek", + 9552 : "(UTC+05:45) Katmandu", + 9553 : "(UTC+12:00) Auckland, Wellington", + 9554 : "(UTC-03:30) Newfoundland", + 9555 : "(UTC+08:00) Irkutsk", + 9556 : "(UTC+07:00) Krasnoyarsk", + 9557 : "(UTC-04:00) Santiago", + 9558 : "(UTC-08:00) Pacific (USA, Canada)", + 9559 : "(UTC-08:00) Tijuana, Baja California (Mexico)", + 9560 : "(UTC+01:00) Brussels, Copenhagen, Madrid, Paris", + 9561 : "(UTC+03:00) Moskow, St. Petersburg, Volgograd", + 9562 : "(UTC-03:00) Buenos Aires", + 9563 : "(UTC-05:00) Bogota, Lima, Quito", + 9564 : "(UTC-04:30) Caracas", + 9565 : "(UTC-11:00) Midway Islands, Samoa", + 9566 : "(UTC+07:00) Bangkok, Hanoi, Jakarta", + 9567 : "(UTC+02:00) Harare, Pretoria", + 9568 : "(UTC+05:30) Sri Jayawardenepura", + 9569 : "(UTC+08:00) Taipeh", + 9570 : "(UTC+10:00) Hobart", + 9571 : "(UTC+09:00) Osaka, Sapporo, Tokyo", + 9572 : "(UTC+13:00) Nuku'alofa", + 9573 : "(UTC-05:00) Indiana (East)", + 9574 : "(UTC-07:00) Arizona", + 9575 : "(UTC+10:00) Vladisvostok", + 9576 : "(UTC+08:00) Perth", + 9577 : "(UTC+01:00) West.Centr.Africa", + 9578 : "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna", + 9579 : "(UTC+05:00) Islamabad, Karatschi", + 9580 : "(UTC+10:00) Guam, Port Moresby", + 9581 : "(UTC+09:00) Yakutsk", + 9582 : "(UTC+04:00) Caucasian Standard Time", + 9583 : "(UTC-06:00) Chicago, Dallas, Kansas City, Winnipeg", + 9584 : "(UTC-06:00) Guadalajara, Mexico City, Monterrey - new", + 9585 : "(UTC) Casablanca", + 9586 : "(UTC+04:00) Port Louis", + 9587 : "(UTC-07:00) Chihuahua, La Paz, Mazatlan - new", + 9588 : "(UTC-03:00) Montevideo", + 9589 : "(UTC+05:00) Taschkent", + 9591 : "(UTC-04:00) Georgetown, La Paz, San Juan", + 9592 : "(UTC+08:00) Ulan-Bator", + 9593 : "(UTC-03:00) Cayenne", + 9594 : "(UTC-04:00) Asuncion", + 9595 : "(UTC+12:00) Petropavlovsk-Kamtschatski", + 10001 : "Total yield", + 10005 : "Absorbed energy", + 10009 : "Number of events for user", + 10010 : "Number of grid connections", + 10011 : "Operating time", + 10012 : "Feed-in time", + 10013 : "Number of events for installer", + 10014 : "Number of events for service", + 10015 : "Total energy absorbed from the grid by the device", + 10016 : "Total amount of energy fed into the grid by device", + 10017 : "Total operating time of device", + 10018 : "Total feed-in time of device", + 10019 : "Number of grid connections of the device", + 10020 : "Fan operating time", + 10021 : "Fan 2 operating time", + 10022 : "Fan heat sink operating time", + 10023 : "Fan transformer operating time", + 10024 : "Fan transformer 2 operating time", + 10025 : "Number of Flash write cycles", + 10026 : "Number of events for developer", + 10027 : "Current event number", + 10028 : "Waiting time until feed-in", + 10030 : "Power", + 10031 : "Power L1", + 10032 : "Power L2", + 10033 : "Power L3", + 10035 : "Grid voltage phase L1", + 10036 : "Grid voltage phase L2", + 10037 : "Grid voltage phase L3", + 10038 : "Grid current", + 10039 : "Grid current phase L1", + 10040 : "Grid current phase L2", + 10041 : "Grid current phase L3", + 10042 : "Grid frequency", + 10043 : "Reactive power", + 10044 : "Reactive power L1", + 10045 : "Reactive power L2", + 10046 : "Reactive power L3", + 10047 : "Apparent power", + 10048 : "Apparent power L1", + 10049 : "Apparent power L2", + 10050 : "Apparent power L3", + 10051 : "Operating condition residual current", + 10052 : "Operating condition insulation resistance", + 10053 : "Operating condition grid voltage", + 10054 : "Operating condition grid voltage L1", + 10055 : "Operating condition grid voltage L2", + 10056 : "Operating condition grid voltage L3", + 10057 : "Operating condition grid frequency", + 10058 : "Operating condition power", + 10061 : "Operating condition reactive power", + 10063 : "Operating condition current", + 10064 : "Operating condition current L1", + 10065 : "Operating condition current L2", + 10066 : "Operating condition current L3", + 10067 : "Operating condition temperatures", + 10068 : "Highest measured ambient temperature", + 10069 : "Ambient temperature", + 10070 : "Insolation", + 10071 : "Diffuse insolation", + 10072 : "Direct irradiation", + 10073 : "Insolation on external sensor", + 10074 : "Wind speed", + 10075 : "Wind direction", + 10076 : "Air humidity", + 10077 : "Atmospheric pressure", + 10078 : "Residual current", + 10079 : "Insulation resistance", + 10080 : "Serial number", + 10081 : "Nominal power in Ok Mode", + 10082 : "Nominal power in Warning Mode", + 10083 : "Nominal power in Fault Mode", + 10085 : "Recommended action 'contact installer'", + 10086 : "Recommended action 'contact manufacturer'", + 10087 : "External temperature", + 10088 : "Highest measured external temperature", + 10089 : "External temperature 2", + 10090 : "Highest measured external temperature 2", + 10091 : "Internal temperature", + 10092 : "Highest measured internal temperature", + 10093 : "Heat sink temperature", + 10094 : "Highest measured heat sink temperature", + 10095 : "Heat sink temperature 2", + 10096 : "Highest measured heat sink temperature 2", + 10097 : "Transformer temperature", + 10098 : "Highest measured transformer temperature", + 10101 : "Module temperature", + 10102 : "Highest measured module temperature", + 10103 : "Highest measured inverter temperature", + 10104 : "Inverter temperature", + 10105 : "Highest measured printed circuit board temperature", + 10106 : "Printed circuit board temperature", + 10107 : "Last GridGuard password", + 10108 : "Intermediate circuit voltage", + 10109 : "Current event number for manufacturer", + 10110 : "Condition", + 10111 : "Recommended action", + 10112 : "Message", + 10113 : "Fault correction measure", + 10114 : "Grid relay/contactor", + 10115 : "Derating", + 10116 : "DC voltage control type", + 10119 : "Acknowledge fault", + 10120 : "Remote control", + 10121 : "Maximum active power device", + 10122 : "Memory card status", + 10123 : "Operating condition", + 10124 : "Fan test", + 10125 : "Deactivation delay", + 10126 : "Country standard set", + 10127 : "Device name", + 10128 : "Device class", + 10129 : "Device type", + 10130 : "Manufacturer", + 10134 : "Set total operating time at grid connection point", + 10135 : "Set total feed-in time at the grid connection pt.", + 10136 : "Set fan operating time", + 10137 : "Set fan2 operating time", + 10138 : "Set heat sink fan operating time", + 10139 : "Set transfomer fan operating time", + 10140 : "Set transformer fan 2 operating time", + 10141 : "Reset operating data", + 10143 : "Load parameter", + 10144 : "Memory card function", + 10145 : "Start of derating based on external temperature", + 10146 : "Start of derating based on external temperature 2", + 10147 : "Start of derating based on interior temperature", + 10148 : "Setpoint of the interior temperature", + 10149 : "Start of derating based on heat sink temperature", + 10150 : "Setpoint of the heat sink temperature", + 10151 : "Start of derating based on heat sink temperature 2", + 10152 : "Setpoint of the heat sink temperature 2", + 10153 : "Start of derating based on transformer temperature", + 10154 : "Setpoint of transformer temperature", + 10155 : "Start of derating based on board temperature", + 10156 : "Start of derating based on inverter temperature", + 10159 : "Set active power limit", + 10160 : "Current power limit A", + 10161 : "Power limit B", + 10162 : "Power limit C", + 10163 : "Power limitation", + 10164 : "Phase assignment", + 10165 : "Start. point of the power control via frequency", + 10166 : "End point of the power control via frequency", + 10167 : "Tripping threshold DC current monitoring", + 10168 : "Tripping time DC current monitoring", + 10169 : "Maximum duration of a short interruption", + 10170 : "Reconnection time upon short interruption", + 10171 : "Reconnection time upon grid interruption", + 10172 : "Reconnection time upon restart", + 10173 : "Escalation factor", + 10174 : "Frequency monitoring upper maximum threshold", + 10175 : "Frq. monitoring upper max. threshold trip. time", + 10176 : "Frequency monitoring median maximum threshold", + 10177 : "Frq. monitoring median max. threshold trip. time", + 10178 : "Frequency monitoring lower maximum threshold", + 10179 : "Frq. monitoring lower max. threshold trip. time", + 10180 : "Frequency monitoring upper minimum threshold", + 10181 : "Frq. monitoring upper min. threshold trip. time", + 10182 : "Frequency monitoring median minimum threshold", + 10183 : "Frq. monitoring median min. threshold trip. time", + 10184 : "Frequency monitoring lower minimum threshold", + 10185 : "Frq. monitoring lower min. threshold trip. time", + 10186 : "Voltage monitoring upper maximum threshold", + 10187 : "Voltage monitoring upper max. threshold trip. time", + 10188 : "Voltage monitoring median maximum threshold", + 10189 : "Voltage monitoring median max. threshold trip.time", + 10190 : "Voltage monitoring lower maximum threshold", + 10191 : "Voltage monitoring lower max. threshold trip. time", + 10192 : "Voltage monitoring lower minimum threshold", + 10193 : "Voltage monitoring lower min. threshold trip. time", + 10194 : "Voltage monitoring of median minimum threshold", + 10195 : "Voltage monitoring median min. threshold trip.time", + 10196 : "Voltage monitoring upper minimum threshold", + 10197 : "Voltage monitoring upper min. threshold trip. time", + 10198 : "Voltage increase protection", + 10200 : "Maximum Bluetooth power", + 10201 : "Set country standard", + 10202 : "DC grounding monitoring", + 10203 : "Minimum insulation resistance", + 10204 : "Production test mode", + 10207 : "Data recording on memory card", + 10209 : "DC voltage input", + 10210 : "DC voltage A input", + 10211 : "DC voltage B input", + 10212 : "DC voltage C input", + 10213 : "DC current input", + 10214 : "DC current A input", + 10215 : "DC current B input", + 10216 : "DC current C input", + 10217 : "DC power input", + 10218 : "DC power A input", + 10219 : "DC power B input", + 10220 : "DC power C input", + 10221 : "Operating condition DC current input", + 10222 : "Operating condition DC voltage input", + 10223 : "Operating condition DC power input", + 10224 : "Highest measured DC voltage", + 10225 : "Maximum DC voltage A", + 10226 : "Maximum DC voltage B", + 10227 : "Maximum DC voltage C", + 10228 : "DC current A input", + 10229 : "DC current A input", + 10230 : "DC current A input", + 10231 : "DC current A input", + 10232 : "DC current A input", + 10233 : "DC current B input", + 10241 : "Start delay input", + 10242 : "Minimum voltage input", + 10243 : "Maximum allowed voltage input", + 10244 : "Maximum allowed voltage input A", + 10245 : "Maximum allowed voltage input B", + 10246 : "Maximum allowed voltage input C", + 10247 : "Constant voltage setpoint", + 10248 : "Constant voltage setpoint A", + 10249 : "Constant voltage setpoint B", + 10250 : "Constant voltage setpoint C", + 10257 : "Grid nominal voltage", + 10258 : "Communication version", + 10259 : "Set total yield", + 10260 : "Set absorbed energy", + 10261 : "Set highest measured ambient temperature", + 10263 : "Set highest measured internal temperature", + 10264 : "Set highest measured heat sink temperature", + 10265 : "Set highest measured heat sink temperature 2", + 10266 : "Set highest measured transformer temperature", + 10267 : "Set highest measured inverter temperature", + 10268 : "Set highest measured PCB temperature", + 10269 : "Set highest measured external temperature", + 10270 : "Set highest measured external temperature 2", + 10272 : "Day yield", + 10273 : "Set number of grid connections", + 10274 : "Temporal control of the power limitation", + 10275 : "Reset operating data (for Service)", + 10276 : "Set highest measured module temperature", + 10277 : "Reset measured values", + 10278 : "System time", + 10283 : "Checking firmware", + 10284 : "Set user password", + 10285 : "Set installer password", + 10286 : "Set service password", + 10287 : "Set developer password", + 10288 : "Number of plant detections", + 10289 : "Time of the last detection", + 10290 : "Hardware on Interface 1", + 10291 : "Hardware on Interface 2", + 10292 : "Hardware on Interface 3", + 10297 : "DHCP DNS Server IP", + 10298 : "DHCP Gateway IP", + 10299 : "DHCP IP address", + 10300 : "DHCP server configuration owner", + 10301 : "DHCP Subnet mask", + 10302 : "WAN IP", + 10303 : "Provider IP", + 10306 : "Daylight Saving Time on", + 10308 : "Date format of the user interface", + 10309 : "Language of the user interface", + 10310 : "Units of length of the user interface", + 10311 : "Number format of the user interface", + 10312 : "Unit of temperature of the user interface", + 10313 : "Time format of the user interface", + 10314 : "Standard/Daylight saving time conversion on", + 10315 : "Automatic time synchronization", + 10316 : "Time zone", + 10317 : "Automatic updates activated", + 10318 : "Time of the automatic update", + 10319 : "Device updates activated", + 10320 : "Ethernet DHCP activated", + 10321 : "Ethernet DNS server", + 10322 : "Ethernet gateway", + 10323 : "Ethernet IP address", + 10324 : "Ethernet subnet mask", + 10325 : "Ethernet NAT port", + 10326 : "Ethernet proxy activated", + 10327 : "Ethernet proxy server login", + 10328 : "Ethernet proxy server port", + 10329 : "Ethernet proxy server password", + 10330 : "Ethernet proxy server address", + 10331 : "Web service Port", + 10332 : "HTTP server port", + 10333 : "Test modem connection", + 10334 : "GPRS-APN", + 10335 : "Dial-in number for modem connection", + 10336 : "Password for modem connection", + 10337 : "Modem SIM Card PIN", + 10338 : "Test modem signal strength", + 10339 : "Upload data to the portal", + 10340 : "Result of the last upload", + 10341 : "Test the connection to the portal", + 10342 : "Result of the last connection test", + 10343 : "Register", + 10344 : "Result of the last registration", + 10345 : "Portal user name", + 10346 : "Email address of portal user", + 10347 : "Plant name in portal", + 10348 : "Plant ID in portal", + 10349 : "Use Sunny Portal", + 10350 : "Frequency of data uploads to the portal", + 10351 : "Status of the last SunnyDNS operation", + 10352 : "SunnyDNS active", + 10353 : "SunnyDNS Password", + 10354 : "FTP Push active", + 10355 : "FTP Push: Connection test", + 10356 : "FTP Push Login", + 10357 : "FTP Push server Port", + 10358 : "FTP Push Password", + 10359 : "FTP Push Server Address", + 10362 : "GPRS-Always-On active", + 10363 : "Firmware version of the module", + 10364 : "Hardware version of the module", + 10365 : "Revision status of the module", + 10366 : "Update version of the module", + 10367 : "Serial number of the module", + 10368 : "SUSyID of the module", + 10373 : "Software package", + 10374 : "Find device", + 10375 : "Firmware version of the central assembly", + 10376 : "Hardware version of the central assembly", + 10377 : "Revision status of the central assembly", + 10378 : "Update version of the central assembly", + 10379 : "Serial number of the central assembly", + 10380 : "SUSyID of the central assembly", + 10381 : "Firmware version of the communication assembly", + 10382 : "Hardware version of the communication assembly", + 10383 : "Revision status of the communication assembly", + 10384 : "Update version of the communication assembly", + 10385 : "Serial number of the communication assembly", + 10386 : "SUSyID of the communication assembly", + 10387 : "Firmware version of the residual current mon. unit", + 10388 : "Hardware version of the residual current mon. unit", + 10389 : "Revision status of the residual current mon. unit", + 10390 : "Update version of the residual current mon. unit", + 10391 : "Serial number of residual current monitoring unit", + 10392 : "SUSyID of the residual current monitoring unit", + 10393 : "Firmware version of the display", + 10394 : "Hardware version of the display", + 10395 : "Revision status of the display", + 10396 : "Update version of the display", + 10397 : "Serial number of the display", + 10398 : "SUSyID of the display", + 10399 : "Firmware version of the logic component", + 10400 : "Hardware version of the logic component", + 10401 : "Revision status of the logic component", + 10402 : "Update version of the logic component", + 10403 : "Serial number of the logic component", + 10404 : "SUSyID of the logic component", + 10405 : "Firmware version of the RS485 Module", + 10406 : "Hardware version of the RS485 module", + 10407 : "Revision status of the RS485 module", + 10408 : "Update version of the RS485 module", + 10409 : "Serial number of the RS485 module", + 10410 : "SUSyID of the RS485 module", + 10411 : "Firmware version of the Zigbee components", + 10412 : "Hardware version of the Zigbee components", + 10413 : "Revision status of the Zigbee components", + 10414 : "Update version of the Zigbee components", + 10415 : "Serial number of the Zigbee components", + 10416 : "SUSyID of the Zigbee components", + 10417 : "Firmware version of the Bluetooth component", + 10418 : "Hardware version of the Bluetooth component", + 10419 : "Revision status of the Bluetooth component", + 10420 : "Update version of the Bluetooth component", + 10421 : "Serial number of the Bluetooth component", + 10422 : "SUSyID of the Bluetooth component", + 10423 : "Firmware version of the operating system", + 10424 : "Hardware version of the operating system", + 10425 : "Revision status of the operating system", + 10426 : "Update version of the operating system", + 10427 : "Serial number of the operating system", + 10428 : "SUSyID of the operating system", + 10429 : "Plant name", + 10430 : "Sunny Portal server address", + 10431 : "Time difference Plant/System", + 10432 : "Number of S0 impulses", + 10433 : "Consumed energy", + 10434 : "Number of Resets", + 10435 : "Power absorbed", + 10436 : "Reset operating data for string failure detection", + 10437 : "Operating mode of string failure detection", + 10438 : "Op. mode of stat.V stab., stat.V stab. config.", + 10439 : "Operating mode of feed-in management", + 10440 : "Operating mode of active power reduction in case of overfrequency P(f)", + 10441 : "Status, plant control", + 10443 : "Difference until next characteristic curve value", + 10444 : "Difference until next charact. curve value Time", + 10445 : "Nominal voltage offset", + 10446 : "Gradient of reactive power characteristic curve", + 10448 : "Reactive power setpoint in var", + 10449 : "Reactive power set value as a %", + 10451 : "Reactive power value starting point", + 10452 : "Active power value starting point", + 10453 : "Reactive power value end point", + 10454 : "Active power value end point", + 10455 : "cosPhi setpoint, cosPhi config., direct specif.", + 10456 : "cosPhi excit.type, cosPhi config., direct spec.", + 10457 : "Excit. type at start point, cosPhi(P) char. conf.", + 10458 : "cosPhi at start point, cosPhi(P) char. config.", + 10459 : "Active power starting point", + 10460 : "Excit. type at end point, cosPhi(P) char. config.", + 10461 : "cosPhi at end point, cosPhi(P) char. config.", + 10462 : "Active power end point", + 10463 : "Active power limitation P, active power configuration", + 10464 : "Active power limitation P, active power configuration", + 10466 : "Difference between starting frequency and grid frequency, linear instantaneous power gradient configuration", + 10467 : "Active power gradient, linear instantaneous power gradient configuration", + 10468 : "Difference between reset frequency and grid frequency, linear instantaneous power gradient configuration", + 10469 : "Activation of stay-set indicator function, linear instantaneous power gradient configuration", + 10471 : "Current reactive power limit", + 10472 : "Max. reactive power", + 10473 : "Currently set apparent power limit", + 10474 : "Maximum apparent power device", + 10475 : "Current cosPhi limit", + 10476 : "Min. cosPhi", + 10477 : "Reconnect gradient after grid fault", + 10478 : "Activation of active power gradient for reconnection after grid fault", + 10479 : "Active power gradient", + 10480 : "Activation of active power gradient", + 10481 : "Firmware version of string protection", + 10482 : "Hardware version of string protection", + 10483 : "Revision status of the string protection", + 10484 : "Update version of the string protection", + 10485 : "String protection serial number", + 10486 : "SUSyID of string protection", + 10487 : "Status, device control", + 10488 : "Normalized total apparent power", + 10489 : "Boost converter temperature", + 10490 : "Highest measured boost converter temperature", + 10491 : "Boost converter 2 temperature", + 10492 : "Highest measured boost converter 2 temperature", + 10493 : "Start boost converter temperature derating", + 10494 : "Set highest measured boost converter temperature", + 10495 : "Set highest measured boost converter temperature 2", + 10496 : "Maximum allowable phase shift", + 10497 : "Tripping time when exceeding max. phase shift", + 10498 : "Maximum allowable frequency drift", + 10499 : "Tripping time when exceeding max. frequency drift", + 10500 : "Lower frequency for reconnection", + 10501 : "Upper frequency for reconnection", + 10502 : "Nominal frequency", + 10503 : "Calibration voltage", + 10504 : "Calibration status", + 10505 : "Selection of the voltage(s) to be calibrated", + 10506 : "Maximum module ground current", + 10507 : "Tripping time maximum module ground current", + 10508 : "Module grounding prescribed?", + 10509 : "Prescribed module grounding type", + 10510 : "Module grounding status", + 10511 : "Firmware version of protocol converter", + 10512 : "Hardware version of protocol converter", + 10513 : "Revision status of the protocol converter", + 10514 : "Update version of the protocol converter", + 10515 : "Serial number of the protocol converter", + 10516 : "SUSyID of protocol converter", + 10517 : "Set plant time", + 10518 : "Synchronize time with portal", + 10519 : "Syslog service activated until", + 10583 : "Reactive power limitation by plant control", + 10584 : "Normalized reactive power limitation by plant ctrl", + 10585 : "Active power limitation by plant control", + 10586 : "Normalized active power limitation by plant ctrl", + 10587 : "Dis.pow.factor that can be changed via plant ctrl", + 10588 : "Excitation type that can be changed by plant ctrl", + 10589 : "Time-out for communication fault indication", + 10590 : "Measurement name in local language", + 10591 : "Events in local language", + 10610 : "Cycle time of the OptiTrac Global Peak algorithm", + 10611 : "Power limit of the OptiTrac Global Peak", + 10612 : "OptiTrac Global Peak switched on", + 10637 : "Setpoint for PV voltage", + 10638 : "Derating status", + 10639 : "Setpoint for intermediate circuit voltage", + 10640 : "Path for update file", + 10641 : "DAA type ID", + 10647 : "RAA type ID", + 10648 : "Min. voltage for reconnection", + 10649 : "Max. voltage for reconnection", + 10650 : "Specified voltage UQ0, reactive power/voltage characteristic curve configuration Q(U)", + 10651 : "Symmetrical limit for maximum reactive power, reactive power/voltage characteristic curve configuration Q(U)", + 10652 : "Voltage spread, reactive power/voltage characteristic curve configuration Q(U)", + 10653 : "Reactive power gradient, reactive power/voltage characteristic curve configuration Q(U)", + 10654 : "Adjustment time for characteristic operating point, reactive power/voltage characteristic curve configuration Q(U)", + 10655 : "Act. power at start point, cosPhi(P) char. config.", + 10656 : "Act. power at end point, cosPhi(P) char. config.", + 10660 : "Reference voltage, plant control", + 10661 : "Reactive power gradient", + 10662 : "Reference correction voltage, plant control", + 10663 : "PWM inverse voltage, dynamic grid support configuration", + 10664 : "PWM inversion delay, dynamic grid support configuration", + 10665 : "Reactive current droop, full dynamic grid support configuration", + 10666 : "Gradient K or reactive current droop, full dynamic grid support configuration", + 10667 : "Check for update and install it", + 10668 : "Hysteresis voltage, dynamic grid support configuration", + 10669 : "CPU monitor", + 10670 : "CPU load", + 10671 : "FTP Push server path", + 10672 : "IRE status", + 10673 : "Number of DC disconnects", + 10674 : "Lower limit, voltage dead band, full dynamic grid support configuration", + 10675 : "Upper limit, voltage dead band, full dynamic support configuration", + 10676 : "Result of the last FTP-Push connection test", + 10677 : "Operating mode of dynamic grid support, dynamic grid support configuration", + 10678 : "Target version of the software package", + 10679 : "Update version of the software package", + 10680 : "Measures in the case of a grounding error", + 10681 : "FTP Push: measurement name in local language", + 10682 : "FTP Push: data export in CSV format", + 10683 : "FTP Push: data export in XML format", + 10684 : "Data export in CSV format", + 10685 : "Data export in XML format", + 10686 : "Displacement power factor", + 10687 : "Reactive power droop mode, reactive power/voltage characteristic curve configuration Q(U)", + 10688 : "Status of islanding detection frequency monitor", + 10689 : "Tripping time of islanding detection frq. monitor", + 10690 : "Status of islanding detection unbalance detection", + 10691 : "Permissible grid unbalance of islanding detection", + 10692 : "Trip.time of islanding detection unbalance detect.", + 10693 : "Operating mode of multifunction relay", + 10694 : "Minimum On time for MFR self-consumption", + 10695 : "Minimum On power for MFR self-consumption", + 10696 : "Minimum power On time, MFR self-consumption", + 10697 : "Initiate device restart", + 10698 : "Status of MFR with control via communication", + 10699 : "Constant deviation from power calculation", + 10700 : "Pre-expon. factor of power consumption acc. to Udc", + 10701 : "Pre-expon. factor of power cons. acc. to Udc^2", + 10702 : "Pre-expon. factor of power cons. Acc. to Udc^3", + 10703 : "Proportionate factor of power control", + 10704 : "Integral factor of power control", + 10705 : "Differential factor of power control", + 10706 : "Critical voltage to end feed-in", + 10707 : "Minimum On power for MFR battery bank", + 10708 : "Minimum time before reconnection of MFR battery bank", + 10709 : "Login required for GridGuard parameter", + 10710 : "Displacement power factor", + 10711 : "Excitation type of cosPhi", + 10712 : "Energy counter type", + 10713 : "PV generation counter reading", + 10714 : "Grid feed-in counter reading", + 10715 : "Grid reference counter reading", + 10716 : "PV power generated", + 10717 : "Power grid feed-in", + 10718 : "Power grid reference", + 10719 : "Perm. grid-frequency deviation of islanding detection", + 10720 : "Current pulse height of islanding detection", + 10721 : "Max. impedance gradient of islanding detection", + 10722 : "Status of impedance of islanding detection", + 10723 : "Amplitude of islanding detection", + 10724 : "Frequency of islanding detection", + 10725 : "Status of passive islanding detection", + 10726 : "Voltage monitoring normalized lower maximum threshold", + 10727 : "Voltage monitoring normalized upper minimum threshold", + 10728 : "Norm. max. voltage for reconnecting", + 10729 : "Norm. min. voltage for reconnecting", + 10730 : "Correction voltage, voltage increase protection", + 10731 : "CO2-saving", + 10732 : "Factor for CO2-saving", + 10733 : "Grid type", + 10734 : "Grid impedance", + 10735 : "Central point of P-coordinates turbine mode", + 10736 : "Lower voltage limit feed-in, turbine mode", + 10737 : "Central point of U-coordinates turbine mode", + 10739 : "Start point of U-coordinates turbine mode", + 10740 : "Power gradient for start-up, turbine mode", + 10741 : "Ripple control signal detection threshold", + 10742 : "Ripple control signal detection frequency", + 10743 : "Ripple control signal detection operating mode", + 10744 : "Ground voltage", + 10745 : "DC converter HW version", + 10746 : "DC converter SW version", + 10747 : "Backup mode operating mode", + 10748 : "Backup mode status", + 10749 : "PowerBalancer operating mode", + 10750 : "Max. load unbalance for PowerBalancer", + 10751 : "S0-pulses PV feed-in counter", + 10752 : "S0-pulses grid feed-in counter", + 10753 : "S0-pulses grid reference counter", + 10754 : "GridGuard version", + 10756 : "Consumer power", + 10757 : "Rise in self-consumption", + 10758 : "Rise in self-consumption today", + 10759 : "PV mains connection", + 10760 : "Power supply status", + 10761 : "Number of battery charge throughputs", + 10764 : "SSM Id for the comm. fault has occurred", + 10765 : "SMU warning code for string fault", + 10766 : "Status of signal contact 1", + 10767 : "Status of signal contact 2", + 10768 : "Operating mode of the device update", + 10769 : "Update source of the device update", + 10770 : "Status of the device update", + 10771 : "Transmission attempts of a device update", + 10772 : "Time of last transm. attempt of a device update", + 10773 : "S0 pulses per kWh for PV generation counter", + 10774 : "S0 pulses per kWh for feed counter", + 10775 : "S0 pulses per kWh for reference counter", + 10776 : "Update operating mode", + 10777 : "Status after loss of communication in autom. mode", + 10778 : "Status after switching on automatic", + 10779 : "Status of the grid switch", + 10781 : "Automatic mode of the grid switch", + 10782 : "Timeout after loss of communication", + 10783 : "Starting current detection", + 10784 : "Gradient K of the reactive current droop, full dynamic grid support configuration", + 10785 : "Automatic mode can be activated", + 10786 : "Meter reading consumption meter", + 10788 : "Grid voltage phase L1 against L2", + 10792 : "Logical flow direction phase L1", + 10793 : "Logical flow direction phase L2", + 10795 : "Grid disconn. at 0% specif. by feeding management", + 10796 : "Characteristic curve number of the plant control procedure P(U)", + 10797 : "Adjustment time of characteristic operating point, conf. of grid integr. char. 1", + 10798 : "Decrease ramp, conf. of grid integr. char. 1", + 10799 : "Increase ramp, conf. of grid integr. char. 1", + 10800 : "Number of points to be used, conf. of grid integr. char. 1", + 10801 : "X value 1, conf. of grid integr. char. 1", + 10802 : "Y value 1, conf. of grid integr. char. 1", + 10803 : "X value 2, conf. of grid integr. char. 1", + 10804 : "Y value 2, conf. of grid integr. char. 1", + 10805 : "X value 3, conf. of grid integr. char. 1", + 10806 : "Y value 3, conf. of grid integr. char. 1", + 10807 : "X value 4, conf. of grid integr. char. 1", + 10808 : "Y value 4, conf. of grid integr. char. 1", + 10809 : "X value 5, conf. of grid integr. char. 1", + 10810 : "Y value 5, conf. of grid integr. char. 1", + 10811 : "X value 6, conf. of grid integr. char. 1", + 10812 : "Y value 6, conf. of grid integr. char. 1", + 10813 : "Active power gradient after reset frequency, linear instantaneous power gradient configuration", + 10814 : "Active power gradient connection", + 10818 : "E-mail alert function on", + 10820 : "E-mail address(es) for alert", + 10821 : "Test e-mail for e-mail alert", + 10824 : "Modbus TCP server on", + 10825 : "Modbus TCP server port", + 10826 : "Modbus UDP server on", + 10827 : "Modbus UDP server port", + 10828 : "DHCP server switched on", + 10829 : "Speedwire DHCP server start IP address", + 10830 : "Speedwire DHCP server end IP address", + 10831 : "Speedwire DHCP server subnet mask", + 10832 : "Speedwire DHCP server IP address", + 10833 : "Time synchronisation source", + 10834 : "NTP server", + 10837 : "Analogue current input 1 (AI1)", + 10838 : "Analogue current input 2 (AI2)", + 10839 : "Analogue current input 3 (AI3)", + 10840 : "Analogue voltage input 1 (AI1)", + 10842 : "AFCI switched on", + 10843 : "Set energy taken up by consumers", + 10844 : "Conn. point of counter on measuring interface 1", + 10845 : "Conn. point of counter on measuring interface 2", + 10846 : "Conn. point of counter on measuring interface 3", + 10847 : "Type of counter on measuring interface 1", + 10848 : "Type of counter on measuring interface 2", + 10849 : "Type of counter on measuring interface 3", + 10850 : "Logical flow direction phase L3", + 10852 : "Char. of insolation sensor - min. insolation", + 10853 : "Char. of insolation sensor - max. insolation", + 10854 : "Char. of insolation sensor - min. measurement", + 10855 : "Char. of insolation sensor - max. measurement", + 10856 : "Free memory on USB medium", + 10857 : "Free memory on USB medium 2", + 10858 : "Fast shut-down", + 10861 : "Type of central assembly", + 10862 : "Type of communication assembly", + 10863 : "Type of residual current monitoring", + 10864 : "Type of display", + 10865 : "Type of logic component", + 10866 : "Type of RS485 module", + 10867 : "Type of Zigbee components", + 10868 : "Type of Bluetooth component", + 10869 : "Type of operating system", + 10870 : "Type of string protection", + 10871 : "Type of protocol converter", + 10872 : "Type of module in module slot", + 10873 : "Key switch", + 10874 : "Local time", + 10875 : "DC switch", + 10876 : "Error message DC switch", + 10877 : "String status", + 10878 : "Speedwire connection status of SMACOM A", + 10879 : "Connection speed of SMACOM A", + 10880 : "Speedwire connection status of SMACOM B", + 10881 : "Connection speed of SMACOM B", + 10882 : "Current speedwire IP address", + 10883 : "Current speedwire subnet mask", + 10884 : "Current speedwire gateway address", + 10885 : "Current speedwire DNS server address", + 10886 : "Status of the Webconnect functionality", + 10887 : "Webconnect functionality switched on", + 10888 : "MAC address", + 10889 : "Automatic speedwire configureation switched on", + 10890 : "Speedwire IP address", + 10891 : "Speedwire subnet mask", + 10892 : "Speedwire gateway address", + 10893 : "Speedwire DNX server address", + 10894 : "Power PV generation (caluclated)", + 10895 : "Counter status PV generation counter (calculated)", + 10896 : "Duplex mode of SMACOM A", + 10897 : "Duplex mode of SMACOM B", + 10898 : "Amp hours counter for battery charge", + 10899 : "Amp hours counter for battery discharge", + 10900 : "Active battery charging mode", + 10901 : "Current generator power", + 10902 : "Battery maint. charge status", + 10903 : "Current battery charge status", + 10904 : "Battery temperature", + 10905 : "Battery voltage", + 10906 : "Absorbed energy", + 10907 : "Released energy", + 10908 : "Automatic generator start", + 10909 : "Reason for generator request", + 10910 : "Released generator power", + 10911 : "Manual generator control", + 10912 : "Generator operating hours", + 10913 : "Generator shutdown battery charge limit", + 10914 : "Generator startup battery charge limit", + 10915 : "Number of generator starts", + 10916 : "Grid creating generator", + 10917 : "Multifunction relay status", + 10918 : "Energy consumed internally", + 10919 : "Current rise in self-consumption", + 10920 : "Current self-consumption", + 10921 : "Current battery capacity", + 10922 : "Battery current", + 10923 : "Battery quick charge time", + 10924 : "Battery compensation charge time", + 10925 : "Battery full charge time", + 10926 : "Max. start attempts after error", + 10927 : "Max. battery charging current", + 10928 : "Current battery charging set voltage", + 10929 : "Rated battery capacity", + 10930 : "Max. battery temperature", + 10931 : "Battery type", + 10932 : "Rated battery voltage", + 10933 : "Acknowledge generator errors", + 10934 : "Rated generator current", + 10935 : "Generator request via power on", + 10936 : "Generator shutdown load limit", + 10937 : "Generator startup load limit", + 10938 : "Generator status", + 10939 : "Rise in self-consumption switched on", + 10940 : "Grid feed-in today", + 10941 : "Power outage", + 10942 : "Number of DC current measurement units", + 10943 : "Free memory on USB medium 1", + 10944 : "External measurement of the insulation resistance", + 10945 : "IP address of the SMTP server", + 10946 : "SMTP server port", + 10947 : "User name for registration on the SMTP server", + 10948 : "Password for registration on SMTP server", + 10949 : "Type of password encryption", + 10950 : "Synchronise with time synchronisation source", + 10951 : "SSH switched on", + 10952 : "IGMP switched on", + 10953 : "Speedwire Group", + 10954 : "Analogue current input 4", + 10955 : "Analogue voltage input 2", + 10956 : "Analogue voltage input 3", + 10957 : "Analogue voltage input 4", + 10958 : "Encrypt portal communication", + 10959 : "Portal communication monitoring signal", + 10960 : "Last successful portal upload", + 10961 : "Hardware ID", + 10962 : "Data logging on monitoring server", + 10963 : "Remove device", + 10964 : "Remove all devices", + 10965 : "WebConnect system environment", + 10966 : "Wind direction", + 10967 : "Reset communication data", + 10968 : "Alarm at active power limit", + 10969 : "Alarm at reactive power specification", + 10970 : "Result of the last e-mail dispatch", + 10971 : "X value 7, conf. of grid integr. char. 1", + 10972 : "Y value 7, conf. of grid integr. char. 1", + 10973 : "X value 8, conf. of grid integr. char. 1", + 10974 : "Y value 8, conf. of grid integr. char. 1", + 10975 : "Characteristic number, conf. of reactive power/voltage char. Q(U)", + 10976 : "Activating active power, conf. of reactive power/voltage char. Q(U)", + 10977 : "Deactivating active power, conf. of reactive power/voltage char. Q(U)", + 10978 : "Waiting time of active power grad. after reset frequency, conf. of linear instantaneous power gradient", + 10979 : "Activating voltage, cosPhi(P) char. conf.", + 10980 : "Deactivating voltage, cosPhi(P) char. conf.", + 10981 : "X-axes reference, conf. of grid integration char. 1", + 10982 : "Y-axes reference, conf. of grid integration char. 1", + 10983 : "Adjustment time of char. operating point, conf. of grid integration char. 2", + 10984 : "Decrease ramp, conf. of grid integration char. 2", + 10985 : "Increase ramp, conf. of grid integration char. 2", + 10986 : "Number of points to be used, conf. of grid integr. char. 2", + 10987 : "Input unit, conf. of grid integration char. 2", + 10988 : "Output frequency, conf. of grid integration char. 2", + 10989 : "X value 1, conf. of grid integr. char. 2", + 10990 : "X value 2, conf. of grid integr. char. 2", + 10991 : "X value 3, conf. of grid integr. char. 2", + 10992 : "X value 4, conf. of grid integr. char. 2", + 10993 : "X value 5, conf. of grid integr. char. 2", + 10994 : "X value 6, conf. of grid integr. char. 2", + 10995 : "X value 7, conf. of grid integr. char. 2", + 10996 : "X value 8, conf. of grid integr. char. 2", + 10997 : "Y value 1, conf. of grid integr. char. 2", + 10998 : "Y value 2, conf. of grid integr. char. 2", + 10999 : "Y value 3, conf. of grid integr. char. 2", + 11000 : "Y value 4, conf. of grid integr. char. 2", + 11001 : "Y value 5, conf. of grid integr. char. 2", + 11002 : "Y value 6, conf. of grid integr. char. 2", + 11003 : "Y value 7, conf. of grid integr. char. 2", + 11004 : "Y value 8, conf. of grid integr. char. 2", + 11005 : "Speedwire connection status of SMACOM C", + 11006 : "Connection speed of SMACOM C", + 11007 : "Speedwire connection status of SMACOM D", + 11008 : "Connection speed of SMACOM D", + 11009 : "Duplex mode of SMACOM C", + 11010 : "Duplex mode of SMACOM D", + 11011 : "Operating mode of power control module", + 11012 : "Status of digital inputs of power control module", + 11013 : "Lower frequency limit prior to end of active power reduction, linear instant. power gradient conf.", + 11014 : "Upper frequency limit prior to end of active power reduction, linear instant. power gradient conf.", + 11015 : "Goose application ID", + 11016 : "Frequency monitoring switchable max. threshold", + 11017 : "Frequency monit. switchable max. threshold tripping time", + 11018 : "Frequency monitoring switchable min. threshold", + 11019 : "Frequency monit. switchable min. threshold tripping time", + 11020 : "Operating mode of frequency restriction", + 11021 : "Source of quick shut-down", + 11022 : "Frequency band narrowing", + 11023 : "Alarm upon warnings", + 11024 : "Alarm e-mail languages", + 11025 : "Alternative e-mail sender address (optional)", + 11027 : "Normalized active power limitation by plant ctrl", + 11028 : "Normalized reactive power limitation by plant ctrl", + 11029 : "Digital input group 1 (DI1 .. DI4)", + 11030 : "Digital input group 2 (DI5 .. DI8)", + 11031 : "Grid voltage phase L2 against L3", + 11032 : "Grid voltage phase L3 against L1", + 11033 : "Power unit supplies data", + 11034 : "Voltage increase protection trigger time", + 11035 : "Goose-Mac address", + 11036 : "Grid connection point rated current", + 11037 : "Active power specif. by plant control", + 11038 : "Reactive power specif. by plant control", + 11039 : "Active power limitation by plant control", + 11040 : "Operating status", + 11041 : "RCD adjustment", + 11042 : "Sunny portal port", + 11043 : "Nominal plant power", + 11044 : "Mode of act. power limit. at grid connection pt", + 11045 : "Dynamic act. power limitat. at grid connection pt", + 11046 : "Set active power limit at grid connection point", + 11047 : "Set active power limit at grid connection point", + 11048 : "Plant control ON", + 11050 : "Number of section switches", + 11051 : "Time to open the section switches", + 11052 : "Time to close section switches", + 11053 : "Generator operating hours", + 11054 : "Generator output energy", + 11055 : "Current generator performance", + 11056 : "Battery discharge cut-off voltage", + 11057 : "Maximum battery charging current", + 11058 : "Maximum battery discharge current", + 11059 : "Status of utility grid", + 11060 : "Cell charge nominal voltage for boost charge", + 11061 : "Cell charge nominal voltage for full charging", + 11062 : "Cell charge nominal voltage for equalisation", + 11063 : "Cell charge nominal voltage for trickle charge", + 11064 : "Voltage monitoring hysteresis minimum threshold", + 11065 : "Voltage monitoring hysteresis maximum threshold", + 11066 : "Frequency monitoring hysteresis minimum threshold", + 11067 : "Frequency monitoring hysteresis maximum threshold", + 11068 : "Voltage monit. gener. lower minimum threshold", + 11069 : "Voltage monit. gener. upper maximum threshold", + 11070 : "Voltage monit. gener. hysteresis min. threshold", + 11071 : "Voltage monit. hysteresis generator max. threshold", + 11072 : "Frequency monit. generator lower minimum threshold", + 11073 : "Frequency monit. generator without max. threshold", + 11074 : "Frequency monit. gener. hysteresis min. threshold", + 11075 : "Freq. Monit. generator hysteresis max. threshold", + 11076 : "Voltage monitoring generator maximum reverse power", + 11077 : "Volt. monit. gener. max. reverse power trip. time", + 11078 : "Output external power connection", + 11079 : "Output external power connection phase A", + 11080 : "Output external power connection phase B", + 11081 : "Output external power connection phase C", + 11082 : "Reactive power external power connection", + 11083 : "Reactive power external power connection phase A", + 11084 : "Reactive power external power connection phase B", + 11085 : "Reactive power external power connection phase C", + 11086 : "Grid frequency of external power connection", + 11087 : "Voltage external power connection phase A", + 11088 : "Voltage external power connection phase B", + 11089 : "Voltage external power connection phase C", + 11090 : "Electricity external power connection phase A", + 11091 : "Electricity external power connection phase B", + 11092 : "Electricity external power connection phase C", + 11093 : "Operating state grid voltage L1 against L2", + 11094 : "Operating state grid voltage L2 against L3", + 11095 : "Operating state grid voltage L3 against L1", + 11096 : "Number of UCP events", + 11097 : "Derating because of low DC voltage", + 11098 : "Derating because of power and DC voltage", + 11099 : "Bias voltage phase L1", + 11100 : "Bias voltage phase L2", + 11101 : "Relative bias voltage", + 11102 : "Rated apparent power of all inverters", + 11103 : "Ohmic resistive coating of the low-voltage line", + 11104 : "Induc. reactive coating of the low-volt.power line", + 11105 : "Length of the low-voltage cabling", + 11106 : "Ohmic resistive coating of the medium-voltage line", + 11107 : "Capac. resistive coating of the medium-volt. line", + 11108 : "Length of the medium-voltage cabling", + 11109 : "Nominal apparent power of the transformer", + 11110 : "Short-circuit voltage of the transformer", + 11111 : "Nominal voltage of the high-voltage side", + 11112 : "Serial number of the counter on meas. interface 1", + 11113 : "Serial number of the counter on meas. interface 2", + 11114 : "Serial number of the counter on meas. interface 3", + 11115 : "PLC communication package error rate", + 11116 : "Firmware version of the module", + 11117 : "Hardware version of the module", + 11118 : "Revision status of the module", + 11119 : "Update version of the module", + 11120 : "Serial number of the module", + 11121 : "SUSyID of module", + 11122 : "Type of module in the module shaft", + 11123 : "Power control module D1: 0 | D2:0 | D3: 0 | D4: 0", + 11124 : "Power control module D1: 1 | D2:0 | D3: 0 | D4: 0", + 11125 : "Power control module D1: 0 | D2:1 | D3: 0 | D4: 0", + 11126 : "Power control module D1: 1 | D2:1 | D3: 0 | D4: 0", + 11127 : "Power control module D1: 0 | D2:0 | D3: 1 | D4: 0", + 11128 : "Power control module D1: 1 | D2:0 | D3: 1 | D4: 0", + 11129 : "Power control module D1: 0 | D2:1 | D3: 1 | D4: 0", + 11130 : "Power control module D1: 1 | D2:1 | D3: 1 | D4: 0", + 11131 : "Power control module D1: 0 | D2:0 | D3: 0 | D4: 1", + 11132 : "Power control module D1: 1 | D2:0 | D3: 0 | D4: 1", + 11133 : "Power control module D1: 0 | D2:1 | D3: 0 | D4: 1", + 11134 : "Power control module D1: 1 | D2:1 | D3: 0 | D4: 1", + 11135 : "Power control module D1: 0 | D2:0 | D3: 1 | D4: 1", + 11136 : "Power control module D1: 1 | D2:0 | D3: 1 | D4: 1", + 11137 : "Power control module D1: 0 | D2:1 | D3: 1 | D4: 1", + 11138 : "Power control module D1: 1 | D2:1 | D3: 1 | D4: 1", + 11139 : "Power control module release time", + 11140 : "Power control module release value", + 11141 : "Power control module fault tolerance time", + 11142 : "Oper.st.vol.maint.at Q on Dem., st.vol.maint.conf.", + 11143 : "Reactive power setpoint in var at Q on demand", + 11144 : "Reactive power setup as a % at Q on demand", + 11145 : "Grad.K react.curr.stat.for UV for dyn.grid support", + 11146 : "Grad.K reac.curr.stat.for dyn.grid support OV", + 11147 : "Char. curve number, conf. of the react.power mode", + 11148 : "Act.of char.curve,conf.of react.pow.char.curve mo.", + 11149 : "Act.threshold,conf.of react.power char. curve mode", + 11150 : "Deact.threshold,conf.of react.power char.curve mo.", + 11151 : "Threshold ref.,conf.of react.power char.line mode", + 11152 : "X values charact. curve 1", + 11153 : "Y values charact. curve 1", + 11154 : "X values charact. curve 2", + 11155 : "Y values charact. curve 2", + 11156 : "X values charact. curve 3", + 11157 : "Y values charact. curve 3", + 16777213 : "Information not available", + } + diff --git a/src/python/tHome/sma/test/FakeSocket.py b/src/python/tHome/sma/test/FakeSocket.py new file mode 100644 index 0000000..3a3e567 --- /dev/null +++ b/src/python/tHome/sma/test/FakeSocket.py @@ -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 + diff --git a/src/python/tHome/sma/test/acMaxPower.py b/src/python/tHome/sma/test/acMaxPower.py new file mode 100644 index 0000000..9a59695 --- /dev/null +++ b/src/python/tHome/sma/test/acMaxPower.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/acPower.py b/src/python/tHome/sma/test/acPower.py new file mode 100644 index 0000000..9963210 --- /dev/null +++ b/src/python/tHome/sma/test/acPower.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/acTotalEnergy.py b/src/python/tHome/sma/test/acTotalEnergy.py new file mode 100644 index 0000000..091a4c5 --- /dev/null +++ b/src/python/tHome/sma/test/acTotalEnergy.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/acTotalPower.py b/src/python/tHome/sma/test/acTotalPower.py new file mode 100644 index 0000000..592a4ea --- /dev/null +++ b/src/python/tHome/sma/test/acTotalPower.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/acVoltage.py b/src/python/tHome/sma/test/acVoltage.py new file mode 100644 index 0000000..0b9fa75 --- /dev/null +++ b/src/python/tHome/sma/test/acVoltage.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/dcPower.py b/src/python/tHome/sma/test/dcPower.py new file mode 100644 index 0000000..9225256 --- /dev/null +++ b/src/python/tHome/sma/test/dcPower.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/dcVoltage.py b/src/python/tHome/sma/test/dcVoltage.py new file mode 100644 index 0000000..3d0640a --- /dev/null +++ b/src/python/tHome/sma/test/dcVoltage.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/gridFrequency.py b/src/python/tHome/sma/test/gridFrequency.py new file mode 100644 index 0000000..575d994 --- /dev/null +++ b/src/python/tHome/sma/test/gridFrequency.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/gridRelayStatus.py b/src/python/tHome/sma/test/gridRelayStatus.py new file mode 100644 index 0000000..57de820 --- /dev/null +++ b/src/python/tHome/sma/test/gridRelayStatus.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/info.py b/src/python/tHome/sma/test/info.py new file mode 100644 index 0000000..5849753 --- /dev/null +++ b/src/python/tHome/sma/test/info.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/operationTime.py b/src/python/tHome/sma/test/operationTime.py new file mode 100644 index 0000000..a994a9f --- /dev/null +++ b/src/python/tHome/sma/test/operationTime.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/real/energy.py b/src/python/tHome/sma/test/real/energy.py new file mode 100755 index 0000000..a83e7ad --- /dev/null +++ b/src/python/tHome/sma/test/real/energy.py @@ -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 diff --git a/src/python/tHome/sma/test/real/full.py b/src/python/tHome/sma/test/real/full.py new file mode 100755 index 0000000..8d15b56 --- /dev/null +++ b/src/python/tHome/sma/test/real/full.py @@ -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 diff --git a/src/python/tHome/sma/test/real/power.py b/src/python/tHome/sma/test/real/power.py new file mode 100755 index 0000000..34fe17b --- /dev/null +++ b/src/python/tHome/sma/test/real/power.py @@ -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 diff --git a/src/python/tHome/sma/test/status.py b/src/python/tHome/sma/test/status.py new file mode 100644 index 0000000..3562d6b --- /dev/null +++ b/src/python/tHome/sma/test/status.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/temp.py b/src/python/tHome/sma/test/temp.py new file mode 100644 index 0000000..2273805 --- /dev/null +++ b/src/python/tHome/sma/test/temp.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/sma/test/version.py b/src/python/tHome/sma/test/version.py new file mode 100644 index 0000000..d8828bc --- /dev/null +++ b/src/python/tHome/sma/test/version.py @@ -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 ) + +#=========================================================================== diff --git a/src/python/tHome/test/config.py b/src/python/tHome/test/config.py new file mode 100644 index 0000000..494bcbb --- /dev/null +++ b/src/python/tHome/test/config.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import tHome as T + +d = T.config.parse( "/home/ted/proj/tHome/conf" ) +print d diff --git a/src/python/tHome/thermostat/Thermostat.py b/src/python/tHome/thermostat/Thermostat.py new file mode 100644 index 0000000..85efa17 --- /dev/null +++ b/src/python/tHome/thermostat/Thermostat.py @@ -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 ) + + #------------------------------------------------------------------------ diff --git a/src/python/tHome/thermostat/__init__.py b/src/python/tHome/thermostat/__init__.py new file mode 100644 index 0000000..84ccbae --- /dev/null +++ b/src/python/tHome/thermostat/__init__.py @@ -0,0 +1,22 @@ +#=========================================================================== +# +# Radio thermostat module. +# +#=========================================================================== + +__doc__ = """Radio Thermostat package. + + +Used for reading radio-thermostat brand WIFI thermostats. +Logging object name: tHome.thermostat +""" + +#=========================================================================== + + +#=========================================================================== + +from . import config +from .Thermostat import Thermostat + +#=========================================================================== diff --git a/src/python/tHome/thermostat/config.py b/src/python/tHome/thermostat/config.py new file mode 100644 index 0000000..9838aca --- /dev/null +++ b/src/python/tHome/thermostat/config.py @@ -0,0 +1,64 @@ +#=========================================================================== +# +# Config file +# +#=========================================================================== + +__doc__ = """Config file parsing. +""" + +from .. import util +from ..util import config as C +from .Thermostat import Thermostat + +#=========================================================================== + +# Config file section name and defaults. +configEntries = [ + # ( name, converter function, default value ) + C.Entry( "logFile", util.path.expand ), + C.Entry( "logLevel", int, 20 ), # INFO + C.Entry( "thermostats", list ), + ] + +thermostatEntries = [ + # ( name, converter function, default value ) + C.Entry( "host", str ), + C.Entry( "label", str ), + C.Entry( "mqttTempTopic", str ), + C.Entry( "mqttModeTopic", str ), + C.Entry( "mqttStateTopic", str ), + C.Entry( "mqttSetTopic", str ), + ] + +#=========================================================================== +def parse( configDir, configFile='thermostat.py' ): + m = C.readAndCheck( configDir, configFile, configEntries ) + + # Replace the thermostat dict inputs with Thermostat objecdts. + m.thermostats = parseThermostats( m.thermostats ) + + return m + +#=========================================================================== +def parseThermostats( entries ): + assert( len( entries ) > 0 ) + + thermostats = [] + for e in entries: + C.check( e, thermostatEntries ) + thermostats.append( Thermostat( **e ) ) + + return thermostats + +#=========================================================================== +def log( config, logFile=None ): + if not logFile: + logFile = config.logFile + + return util.log.get( "thermostat", config.logLevel, logFile ) + +#=========================================================================== + + + diff --git a/src/python/tHome/util/Data.py b/src/python/tHome/util/Data.py new file mode 100644 index 0000000..f1f197c --- /dev/null +++ b/src/python/tHome/util/Data.py @@ -0,0 +1,87 @@ +#============================================================================= +import StringIO + +#============================================================================= +class Data: + def __init__( self, dict=None, **kwargs ): + if dict: + self.__dict__.update( dict ) + if kwargs: + self.__dict__.update( kwargs ) + + #-------------------------------------------------------------------------- + def keys( self ): + return self.__dict__.keys() + + #-------------------------------------------------------------------------- + def update( self, rhs ): + return self.__dict__.update( rhs.__dict__ ) + + #-------------------------------------------------------------------------- + def __setitem__( self, key, value ): + self.__dict__[key] = value + + #-------------------------------------------------------------------------- + def __getitem__( self, key ): + return self.__dict__[key] + + #-------------------------------------------------------------------------- + def __contains__( self, key ): + return key in self.__dict__ + + #-------------------------------------------------------------------------- + def __str__( self ): + out = StringIO.StringIO() + self._formatValue( self, out, 3 ) + return out.getvalue() + + #-------------------------------------------------------------------------- + def __repr__( self ): + return self.__str__() + + #-------------------------------------------------------------------------- + def _formatValue( self, value, out, indent ): + if isinstance( value, Data ): + out.write( "%s(\n" % self.__class__.__name__ ) + for k, v in sorted( value.__dict__.iteritems() ): + if k[0] == "_": + continue + + out.write( "%*s%s" % ( indent, '', k ) ) + out.write( " = " ) + self._formatValue( v, out, indent+3 ) + out.write( ",\n" ) + + out.write( "%*s)" % ( indent, '' ) ) + + elif isinstance( value, dict ): + out.write( "{\n" ) + for k, v in sorted( value.iteritems() ): + if k[0] == "_": + continue + + out.write( "%*s" % ( indent, '' ) ) + self._formatValue( k, out, 0 ) + out.write( " : " ) + self._formatValue( v, out, indent+3 ) + out.write( ",\n" ) + + out.write( "%*s}" % ( indent, '' ) ) + + elif isinstance( value, list ): + out.write( "[\n" ) + for i in value: + out.write( "%*s" % ( indent, '' ) ) + self._formatValue( i, out, indent+3 ) + out.write( ",\n" ) + + out.write( "%*s]" % ( indent, '' ) ) + + elif isinstance( value, str ): + out.write( "'%s'" % ( value ) ) + + else: + out.write( "%s" % ( value ) ) + + +#============================================================================= diff --git a/src/python/tHome/util/Error.py b/src/python/tHome/util/Error.py new file mode 100644 index 0000000..3ea6280 --- /dev/null +++ b/src/python/tHome/util/Error.py @@ -0,0 +1,61 @@ +#=========================================================================== +# +# Error +# +#=========================================================================== + +""": Stack based error message exception class""" + +#=========================================================================== +import sys + +#=========================================================================== +class Error ( Exception ): + """: Stack based error message exception class. + """ + + #----------------------------------------------------------------------- + @staticmethod + def raiseException( exception, msg ): + excType, excValue, trace = sys.exc_info() + + if not isinstance( exception, Error ): + exception = Error( str( exception ) ) + + exception.add( msg ) + + raise exception, None, trace + + #----------------------------------------------------------------------- + @staticmethod + def fromException( exception, msg ): + excType, excValue, trace = sys.exc_info() + + newError = Error( str( exception ) ) + newError.add( msg ) + + raise newError, None, trace + + #----------------------------------------------------------------------- + def __init__( self, msg ): + """: Constructor + """ + self._msg = [ msg ] + + Exception.__init__( self ) + + #----------------------------------------------------------------------- + def add( self, msg ): + self._msg.append( msg ) + + #----------------------------------------------------------------------- + def __str__( self ): + s = "\n" + for msg in reversed( self._msg ): + s += "- %s\n" % msg + + return s + + #----------------------------------------------------------------------- + +#=========================================================================== diff --git a/src/python/tHome/util/NamedStruct.py b/src/python/tHome/util/NamedStruct.py new file mode 100644 index 0000000..8e31ad7 --- /dev/null +++ b/src/python/tHome/util/NamedStruct.py @@ -0,0 +1,51 @@ +#=========================================================================== +# +# Named structure field class +# +#=========================================================================== +import struct +from .Data import Data + +#============================================================================== +class NamedStruct: + + #--------------------------------------------------------------------------- + def __init__( self, endian, elems ): + """Constructr + + endian == BIG_ENDIAN or LITTLE_ENDIAN + elems = [ ( struct_format_code, name ), ... ] + """ + assert( endian == "BIG_ENDIAN" or endian == "LITTLE_ENDIAN" ) + + if endian == "BIG_ENDIAN": + self.format = ">" + elif endian == "LITTLE_ENDIAN": + self.format = "<" + + self.format += "".join( [ i[0] for i in elems ] ) + self.names = [ i[1] for i in elems ] + + self.struct = struct.Struct( self.format ) + + #--------------------------------------------------------------------------- + def __len__( self ): + return self.struct.size + + #--------------------------------------------------------------------------- + def pack( self, obj ): + data = [ getattr( obj, i ) for i in self.names ] + return self.struct.pack( *data ) + + #--------------------------------------------------------------------------- + def unpack( self, obj, bytes, offset=0 ): + if obj is None: + obj = Data() + + data = self.struct.unpack_from( bytes, offset ) + for i in range( len( self.names ) ): + setattr( obj, self.names[i], data[i] ) + + return obj + +#============================================================================== diff --git a/src/python/tHome/util/__init__.py b/src/python/tHome/util/__init__.py new file mode 100644 index 0000000..70cd8b6 --- /dev/null +++ b/src/python/tHome/util/__init__.py @@ -0,0 +1,19 @@ +#============================================================================= +# +# General utilities +# +#============================================================================= + +from . import config +from .Data import Data +from .Error import Error +from .fimport import fimport +from . import hex +from . import jsonUtil as json +from . import log +from .NamedStruct import NamedStruct +from . import path +from . import process +from . import test + +#============================================================================= diff --git a/src/python/tHome/util/config.py b/src/python/tHome/util/config.py new file mode 100644 index 0000000..da982db --- /dev/null +++ b/src/python/tHome/util/config.py @@ -0,0 +1,68 @@ +#=========================================================================== +# +# Config file utilities. +# +#=========================================================================== + +__doc__ = """Config file utilities. +""" + +from . import path +from . import fimport + +#=========================================================================== +class Entry: + def __init__( self, name, cvt, default=None ): + self.name = name + self.cvt = cvt + self.default = default + +#=========================================================================== +def readAndCheck( configDir, configFile, entries ): + # Combine the dir and file, expand any variables, and read the + # python code into a module. + configPath = path.expand( configDir, configFile ) + m = fimport.fimport( configPath ) + + # Check the input values for the correc types and assign any + # default values. + check( m, entries ) + return m + +#=========================================================================== +def check( input, entries ): + if isinstance( input, dict ): + checkDict( input, entries ) + return + + # Use the sections to do error checking. + for e in entries: + if not hasattr( input, e.name ): + value = e.default + + # Run the converter function on the input. This validates the + # input type and can do any other manipulations it wants. + elif e.cvt: + inputValue = getattr( input, e.name ) + value = e.cvt( inputValue ) + + setattr( input, e.name, value ) + +#=========================================================================== +def checkDict( input, entries ): + assert( isinstance( input, dict ) ) + + # Use the sections to do error checking. + for e in entries: + if not e.name in input: + value = e.default + + # Run the converter function on the input. This validates the + # input type and can do any other manipulations it wants. + elif e.cvt: + inputValue = input[e.name] + value = e.cvt( inputValue ) + + input[e.name] = value + +#=========================================================================== diff --git a/src/python/tHome/util/fimport.py b/src/python/tHome/util/fimport.py new file mode 100644 index 0000000..67b7650 --- /dev/null +++ b/src/python/tHome/util/fimport.py @@ -0,0 +1,28 @@ +#=========================================================================== +# +# Arbitrary file importing utility. Does NOT modify sys.modules +# +#=========================================================================== +import imp +import os + +def fimport( filePath ): + # Read the file and compile the code. This will fail if the file + # doesn't exist or there are problems w/ the syntax in the file. + with open( filePath, 'r' ) as f: + code = compile( f.read(), filePath, "exec", dont_inherit=True ) + + # Get the absolute path and the file name w/o the directory or + # extension to set into the module variables. + absPath = os.path.abspath( filePath ) + d, fileName = os.path.split( filePath ) + rootName, ext = os.path.splitext( fileName ) + + # Create a new module and exec the code in it's context. + m = imp.new_module( rootName ) + m.__file__ = absPath + exec code in m.__dict__ + + # Return the module object. + return m + diff --git a/src/python/tHome/util/hex/__init__.py b/src/python/tHome/util/hex/__init__.py new file mode 100644 index 0000000..f8205b8 --- /dev/null +++ b/src/python/tHome/util/hex/__init__.py @@ -0,0 +1,10 @@ +#============================================================================= +# +# Hex string/byte utilities +# +#============================================================================= + +from .dump import dump +from .toBytes import toBytes + +#============================================================================= diff --git a/src/python/tHome/util/hex/dump.py b/src/python/tHome/util/hex/dump.py new file mode 100644 index 0000000..c72f406 --- /dev/null +++ b/src/python/tHome/util/hex/dump.py @@ -0,0 +1,32 @@ +#=========================================================================== +# +# Dump hex bytes to a table. +# +#=========================================================================== +import StringIO + +#=========================================================================== +def dump( buf ): + """Input is bytes buffer, + + Returns a string w/ the hex values in a table + """ + # Convert to hex characters + h = [ i.encode( "hex" ).upper() for i in buf ] + + f = StringIO.StringIO() + f.write( "---: 00 01 02 03 04 05 06 07 08 09\n" ) + + for i in range( len( h ) ): + if i % 10 == 0: + if i > 0: + f.write( "\n" ) + f.write( "%03d: " % i ) + + f.write( "%2s " % h[i] ) + + f.write( "\n" ) + + return f.getvalue() + +#=========================================================================== diff --git a/src/python/tHome/util/hex/toBytes.py b/src/python/tHome/util/hex/toBytes.py new file mode 100644 index 0000000..3bc2d48 --- /dev/null +++ b/src/python/tHome/util/hex/toBytes.py @@ -0,0 +1,23 @@ +#=========================================================================== +# +# Convert a hex string to bytes +# +#=========================================================================== +import math + +#=========================================================================== +def toBytes( hexStr ): + """Input is a string containing hex values (w/ or w/o spaces) + + Return is the same value in a bytes array. + """ + s = hexStr.strip().replace( "\n", " " ) + s = ''.join( s.split(" ") ) + + bytes = [] + for i in range( 0, len( s ), 2 ): + bytes.append( chr( int( s[i:i+2], 16 ) ) ) + + return ''.join( bytes ) + +#=========================================================================== diff --git a/src/python/tHome/util/jsonUtil.py b/src/python/tHome/util/jsonUtil.py new file mode 100644 index 0000000..fe52966 --- /dev/null +++ b/src/python/tHome/util/jsonUtil.py @@ -0,0 +1,54 @@ +#============================================================================= +# +# JSON utility that turns unicode strings to ascii +# +# NOTE: this file should be named json.py but Python's stupid import +# rules look in the current directory first instead of using absolute +# paths all the time. So we can't import the global json module if we +# do that. +# +# Code from: +# http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-of-unicode-ones-from-json-in-python +# +#============================================================================= +import json + +#============================================================================= +# For completeness, add the save API's +dump = json.dump +dumps = json.dumps + +#============================================================================= +def load( file ): + """Same as json.load() but turns unicode to ascii strings. + """ + return _toStr( json.load( file, object_hook=_toStr ), ignoreDicts=True ) + +#============================================================================= +def loads( text ): + """Same as json.loads() but turns unicode to ascii strings. + """ + return _toStr( json.loads( text, object_hook=_toStr ), ignoreDicts=True ) + +#============================================================================= +def _toStr( data, ignoreDicts=False ): + # Convert unicode to string. + if isinstance( data, unicode ): + return data.encode( 'utf-8' ) + + # For lists, process each item. + if isinstance( data, list ): + return [ _toStr( i, ignoreDicts=True ) for i in data ] + + # For dicts, process keys and values, but only if we haven't + # already byteified it + if isinstance( data, dict ) and not ignoreDicts: + return { + _toStr( k, ignoreDicts=True ) : _toStr( v, ignoreDicts=True ) + for k, v in data.iteritems() + } + + # Otherwise return the original object. + return data + +#============================================================================= diff --git a/src/python/tHome/util/log.py b/src/python/tHome/util/log.py new file mode 100644 index 0000000..0e8062d --- /dev/null +++ b/src/python/tHome/util/log.py @@ -0,0 +1,82 @@ +#============================================================================= +# +# Logging utilities +# +#============================================================================= +import logging +import logging.handlers +import platform +import sys +import types +from .Error import Error +from . import path + +#============================================================================= +_logs = {} + +#============================================================================= +def get( name, level=None, output=None ): + """ TODO: doc + """ + log = _logs.get( name, None ) + if not log: + log = create( name ) + + if level is not None: + log.setLevel( level ) + + if output is not None: + writeTo( log, output ) + + return log + +#============================================================================= +def create( name, level=logging.ERROR ): + """ Create a logging object for the package name. + + The full logging name will be 'tHome.NAME'. + """ + log = logging.getLogger( 'tHome.%s' % name ) + log.setLevel( level ) + + handler = logging.NullHandler() + handler.setFormatter( _formatter() ) + log.addHandler( handler ) + + # Monkey patch a method onto the log class. + log.writeTo = types.MethodType( writeTo, log ) + + # Save a handle to the log. + _logs[name] = log + return log + +#============================================================================= +def writeTo( log, fileName ): + """ TODO: doc + """ + if fileName == "stdout": + handler = logging.StreamHandler( sys.stdout ) + else: + try: + path.makeDirs( fileName ) + + # Use the watcher on linux so that logrotate will work + # properly. It will close and reopen the log file if the OS + # moves it. Not supported on windows. + if platform.system() == "Windows": + handler = logging.FileHandler( fileName ) + else: + handler = logging.handlers.WatchedFileHandler( fileName ) + + except ( Error, IOError ) as e: + msg = "Error trying to open the log file '%s' for writing." % fileName + Error.raiseException( e, msg ) + + handler.setFormatter( _formatter() ) + log.addHandler( handler ) + +#============================================================================= +def _formatter(): + return logging.Formatter( '%(asctime)s : %(levelname)s: %(message)s' ) + +#============================================================================= diff --git a/src/python/tHome/util/net/Poll.py b/src/python/tHome/util/net/Poll.py new file mode 100644 index 0000000..98dfe6a --- /dev/null +++ b/src/python/tHome/util/net/Poll.py @@ -0,0 +1,229 @@ +#=========================================================================== +# +# Poll +# +#=========================================================================== + +import select +import errno + +#=========================================================================== +class Poll: + """: Posix poll manager. + + This classes manages the polling behavior for normal posix (non-gui) + network code. See the Python select module or UNIX poll + documentation for details. + + Server and Link objects are added to the Poll and it will manage + the I/O calls for each of them. Instead of having a poll() method + like the Python module, this class has a select() method to kee the + API identical to the mpy.net.Select class. + + = EXAMPLE + + # MPY_NO_TEST + # # Setup server and/or links here. + # + # # Create the select and add the servers/links to it. + # mgr = Poll() + # for c in clients: + # mgr.add( c ) + # + # # Loop to process. + # while True: + # mgr.select() + """ + #----------------------------------------------------------------------- + def __init__( self ): + """: Constructor.""" + # Create the polling object. + self.poll = select.poll() + + # Key is a file descripter for the socket of the object being + # watched. Value is a Link or Server object for that socket. + self.clients = {} + + # Bit flags to watch for when registering a socket for read or + # read/write. + self.READ = select.POLLIN | select.POLLPRI | select.POLLHUP | \ + select.POLLERR + self.READ_WRITE = self.READ | select.POLLOUT + + # Bit flags reported for events. + self.EVENT_READ = select.POLLIN | select.POLLPRI + self.EVENT_WRITE = select.POLLOUT + self.EVENT_CLOSE = select.POLLHUP + self.EVENT_ERROR = select.POLLERR + + # NOTE: These bit flags were originally class variables + # (accessed via Poll.READ, etc) but a weird race condition in + # Python multi-threaded code (mpy.parallel) would sometimes + # throw an exception saying Poll was None. No idea why that + # could happen but making these member variables seems to fix + # the problem. + + #----------------------------------------------------------------------- + def active( self ): + """: Return the number of sockets being watched. + + Used for single point connections to automatically exit a select + function when connections close. + + = EXAMPLE + + # MPY_NO_TEST + # select = Poll() + # select.add( client1 ) + # select.add( client12 ) + # + # while select.active(): + # select.select() + """ + return len( self.clients ) + + #----------------------------------------------------------------------- + def add( self, obj ): + """: Add a link or server to the select. + + Call obj.close() to remove the object. + + = INPUT VARIABLES + - obj The Link or Server object to add to the select. + """ + # All sockets are checked for reading since a read of 0 means a + # dropped connection. Errors are also checked even though they + # rarely occur. Sockets for writing is handled in the + # clientCanWrite() callback that is triggered through the + # watcher mechanism below. + self.poll.register( obj.fileno, self.READ ) + + # Save the object for later use. + self.clients[ obj.fileno ] = obj + + # Tell the object it's part of this Poll. This allows it to + # close the connection and tell the Poll about it. In + # addition, the client can notify us if there is data to write + # or not. + obj.sigClosing.connect( self.closeLink ) + obj.sigCanWrite.connect( self.linkCanWrite ) + + #----------------------------------------------------------------------- + def remove( self, obj ): + """: Remove an object from the select. + + Normally this is called by the object being removed when + obj.close() is called (i.e. this should not normally be called). + + = INPUT VARIABLES + - obj The object that is closing. This should be the + same object that was passed to add(). + """ + # Remove the object from our structures. + self.poll.unregister( obj.fileno ) + del self.clients[ obj.fileno ] + + # Tell the object to remove ourselves from it's watch list. + obj.sigClosing.disconnect( self.closeLink ) + obj.sigCanWrite.disconnect( self.linkCanWrite ) + + #----------------------------------------------------------------------- + def closeAll( self ): + """: Close all clients attached to the select. + + This will close any Link and Server objects that are part of + the select. + """ + # As we close clients, they will notify the select to remove + # themselves. So make a copy of the dict value list list + # containing all the objects so we're not modifying the client + # dictionary as we loop over it. + clients = self.clients.values()[:] + for c in clients: + c.close() + + #----------------------------------------------------------------------- + def select( self, timeOut=None ): + """: Watch the sockets. + + This will call poll.poll() (normal posix poll) and process the + results when it returns. All the clients and servers in the + poll (see the add() method) are checked for reading or errors. + Any client that returns True when client.canWrite() is called + will be checked for writing. + + If a client has an error, then client.close() is called. If a + client can read, then client.read() is called. If a client can + write, then client.write() is called. + + See the class documentation for an example. + + = INPUT VARIABLES + - timeOut Optional floating point time out for the select call + in seconds. See select.select for more information. + """ + timeOut_msec = None if timeOut is None else 1000*timeOut + + while True: + try: + # Returns tuple (fileDescriptor, bitFlag) of the files + # that can act. + events = self.poll.poll( timeOut_msec ) + except select.error, v: + # Not sure why, but sometimes with a timeout value, you can + # get an "interrupted system call" error thrown. Web + # searches indicate this isn't really an error and should be + # ignored so test for it here. + if v[0] != errno.EINTR: + raise + else: + break + + for fd, flag in events: + # Find the object for this file descriptor. Certain + # multi-threaded test cases seem to have a problem with the + # clientClosing() method not being called (which unregisters + # the fd) so handle that with a None return here. + obj = self.clients.get( fd, None ) + if obj is None: + continue + + # Object has data to read. If an error occurred during the + # read, clear the flag so we don't try to do anything else. + if flag & self.EVENT_READ: + if obj.readFromSocket() == -1: + flag = 0 + + # Object can write data. + if flag & self.EVENT_WRITE: + obj.writeToSocket() + + # Test for errors or closing connections. Only call close once. + + # Connection going down. + if flag & self.EVENT_CLOSE: + obj.close() + + # Error - close the connection - use else here + elif flag & self.EVENT_ERROR: + obj.close() + + #----------------------------------------------------------------------- + def linkCanWrite( self, obj, writeActive ): + # Writing should be active. + if writeActive: + # Add the socket if it's not already in the write list. + self.poll.modify( obj.fileno, self.READ_WRITE ) + + # Writing should not be active: + else: + # Remove the socket if it's in the write list. + self.poll.modify( obj.fileno, self.READ ) + + #----------------------------------------------------------------------- + def closeLink( self, obj ): + self.remove( obj ) + + #----------------------------------------------------------------------- + +#=========================================================================== diff --git a/src/python/tHome/util/path.py b/src/python/tHome/util/path.py new file mode 100644 index 0000000..7bee425 --- /dev/null +++ b/src/python/tHome/util/path.py @@ -0,0 +1,44 @@ +#=========================================================================== +# +# File and directory utilities. +# +#=========================================================================== + +import os +import os.path +from .Error import Error + +#=========================================================================== +def makeDirs( fileName ): + """TODO: docs + """ + try: + d = os.path.dirname( fileName ) + if d and not os.path.exists( d ): + os.makedirs( d ) + + except ( IOError, OSError ) as e: + msg = "Error trying to create intermediate directories for the file: " \ + "'%s'" % ( fileName ) + Error.raiseException( e, msg ) + +#=========================================================================== +def expand( filePath, fileName=None ): + """Combine a directory and file name and expand env variables and ~. + + A full path can be input in filePath. Or a directory can be input + in filePath and a file name input in fileName. + """ + if fileName: + filePath = os.path.join( filePath, fileName ) + + filePath = str( filePath ) + if "$" in filePath: + filePath = os.path.expandvars( filePath ) + + if "~" in filePath: + filePath = os.path.expanduser( filePath ) + + return filePath + +#=========================================================================== diff --git a/src/python/tHome/util/process/__init__.py b/src/python/tHome/util/process/__init__.py new file mode 100644 index 0000000..022e6d8 --- /dev/null +++ b/src/python/tHome/util/process/__init__.py @@ -0,0 +1,5 @@ +#============================================================================= + +from .simple import simple + +#============================================================================= diff --git a/src/python/tHome/util/process/simple.py b/src/python/tHome/util/process/simple.py new file mode 100644 index 0000000..4f562ee --- /dev/null +++ b/src/python/tHome/util/process/simple.py @@ -0,0 +1,22 @@ + +#============================================================================= + +import subprocess + +#============================================================================= + +def simple( cmd, cwd=None ): + """Runs a command and returns stdout and stderr mixed together. + + Throws an Exception w/ the output if it fails. + """ + # Set stderr to also send output to stdout (i.e. combine them). + p = subprocess.Popen( cmd, cwd=cwd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT ) + ( stdout, stderr ) = p.communicate() + if p.returncode: + raise Exception( stdout ) + + return stdout + +#============================================================================= diff --git a/src/python/tHome/util/test.py b/src/python/tHome/util/test.py new file mode 100644 index 0000000..031e9a1 --- /dev/null +++ b/src/python/tHome/util/test.py @@ -0,0 +1,30 @@ +#============================================================================= +# +# Testing utilites. +# +#============================================================================= + +import unittest + +#============================================================================= +class Case( unittest.TestCase ): + def __init__( self, cls ): + unittest.TestCase.__init__( self, cls ) + + def setup( self ): + pass + + def teardown( self ): + pass + + def eq( self, a, b, msg=None ): + if not a == b: + msg = msg if msg else "" + #self._errors.append( "%s %r != %r" % ( msg, a, b ) ) + self.fail( "%s %r != %r" % ( msg, a, b ) ) + + def neq( self, a, b, msg=None ): + if not a != b: + msg = msg if msg else "" + #self._errors.append( "%s %r == %r" % ( msg, a, b ) ) + self.fail( "%s %r == %r" % ( msg, a, b ) ) diff --git a/src/python/tHome/util/test/Data.py b/src/python/tHome/util/test/Data.py new file mode 100644 index 0000000..722c288 --- /dev/null +++ b/src/python/tHome/util/test/Data.py @@ -0,0 +1,19 @@ + +from tHome.util import Data + +d = Data( a=1, b="asdf", c=2 ) +print d +print "----" + +d = Data( a=1, b="asdf", c=[2,3,4] ) +print d +print "----" + +d = Data( a=1, b="asdf", c={'a':3, 'b':4} ) +print d +print "----" + +d = Data( a=1, b=[ Data(a=1,b=2) ], c={'a':3, 'b':[1,2,3]} ) +print d +print "----" + diff --git a/src/python/tHome/weatherUnderground/__init__.py b/src/python/tHome/weatherUnderground/__init__.py new file mode 100644 index 0000000..dd85bb5 --- /dev/null +++ b/src/python/tHome/weatherUnderground/__init__.py @@ -0,0 +1,19 @@ +#=========================================================================== +# +# Weather Underground web site access +# +#=========================================================================== + +__doc__ = """Communicate with the Weather Underground web site. + +Allows upload of weather data to a personal weather station. + +""" + +#=========================================================================== + +from . import cmdLine +from . import config +from . import start + +#=========================================================================== diff --git a/src/python/tHome/weatherUnderground/cmdLine.py b/src/python/tHome/weatherUnderground/cmdLine.py new file mode 100644 index 0000000..23596ff --- /dev/null +++ b/src/python/tHome/weatherUnderground/cmdLine.py @@ -0,0 +1,52 @@ +#=========================================================================== +# +# Command line processing +# +#=========================================================================== + +import argparse +import numpy as np +from .. import broker +from . import config +from . import start + +#=========================================================================== + +def run( args ): + """Parse command line arguments to upload weather data. + + = INPUTS + - args [str]: List of command line arguments. [0] should be the + program name. + """ + p = argparse.ArgumentParser( prog=args[0], + description="SMA inverter reader" ) + p.add_argument( "-c", "--configDir", metavar="configDir", + default="/var/config/tHome", + help="T-Home configuration directory." ) + p.add_argument( "-l", "--log", metavar="logFile", + default=None, help="Logging file to use. Input 'stdout' " + "to log to the screen." ) + p.add_argument( "--debug", default=False, action="store_true", + help="Debugging - no sending, just print" ) + c = p.parse_args( args[1:] ) + + if c.debug: + c.log = "stdout" + + # Parse the sma and broker config files. + cfg = config.parse( c.configDir ) + log = config.log( cfg, c.log ) + + if c.debug: + log.setLevel( 10 ) + + # Create the MQTT client and connect it to the broker. + client = broker.connect( c.configDir, log ) + + # Numpy reports invalid errors when dealing w/ nans which don't + # matter to this algorithm. + with np.errstate( invalid='ignore' ): + start.start( cfg, client, debug=c.debug ) + +#=========================================================================== diff --git a/src/python/tHome/weatherUnderground/config.py b/src/python/tHome/weatherUnderground/config.py new file mode 100644 index 0000000..fca5979 --- /dev/null +++ b/src/python/tHome/weatherUnderground/config.py @@ -0,0 +1,47 @@ +#=========================================================================== +# +# Config file +# +#=========================================================================== + +__doc__ = """Config file parsing. +""" + +from .. import util +from ..util import config as C + +#=========================================================================== + +# Config file section name and defaults. +configEntries = [ + # ( name, converter function, default value ) + C.Entry( "uploadUrl", str ), + C.Entry( "id", str ), + C.Entry( "password", str ), + C.Entry( "poll", int, 120 ), + C.Entry( "maxRate", int, 10 ), + C.Entry( "digits", int, 2 ), + C.Entry( "mqttWind", str, None ), + C.Entry( "mqttTemp", list, [] ), + C.Entry( "mqttRain", str, None ), + C.Entry( "mqttBarometer", str, None ), + C.Entry( "mqttHumidity", str, None ), + C.Entry( "logFile", util.path.expand ), + C.Entry( "logLevel", int, 20 ), # INFO + ] + +#=========================================================================== +def parse( configDir, configFile='weatherUnderground.py' ): + return C.readAndCheck( configDir, configFile, configEntries ) + +#=========================================================================== +def log( config, logFile=None ): + if not logFile: + logFile = config.logFile + + return util.log.get( "weatherUnderground", config.logLevel, logFile ) + +#=========================================================================== + + + diff --git a/src/python/tHome/weatherUnderground/start.py b/src/python/tHome/weatherUnderground/start.py new file mode 100644 index 0000000..73a67df --- /dev/null +++ b/src/python/tHome/weatherUnderground/start.py @@ -0,0 +1,403 @@ +#=========================================================================== +# +# Main report processing +# +#=========================================================================== + +import logging +import requests +import datetime +import threading +import numpy as np +import time +from StringIO import StringIO +from .. import util + +#=========================================================================== +class CircularTimeBuf: + """Circular buffer class. + + This stores data in a numpy array as a circular buffer that covers + a set amount of time. When data is added (with a time tag), it + will automatically erase any data older than the input time length. + This allows for fast and easy computations involving the last n + seconds of data regardless of the rate that data is seen. + """ + def __init__( self, timeLen, maxRate, label=None, log=None ): + self.label = label if label is not None else "" + self.log = log + + numEntries = int( timeLen / maxRate ) + + self._dt = timeLen + self._len = numEntries + + # Index of the last entry added to the buffer. -1 is used to + # indicate that no data is present. + self._lastIdx = -1 + + # Create an array of NaN values. v[0] is the array of times, + # v[1] is the array of values. + self.v = np.full( ( 2, self._len ), np.nan ) + + #-------------------------------------------------------------------------- + def __nonzero__( self ): + return self._lastIdx != -1 + + #-------------------------------------------------------------------------- + def append( self, t, v ): + # Remove any data older than t-self._dt + self.updateTo( t ) + + idx = self._nextIdx( self._lastIdx ) + + self.v[0][idx] = t + self.v[1][idx] = v + + self._lastIdx = idx + + # Debugging output + if self.log and self.log.isEnabledFor( logging.DEBUG ): + s = StringIO() + print >> s, "%s record time: %.1f\n" % ( self.label, t ) + if self._lastIdx != -1: + i = self._lastIdx + while not np.isnan( self.v[0][i] ): + print >> s, " %.1f %.1f " % ( self.v[0][i], self.v[1][i] ) + i = self._prevIdx( i ) + + self.log.debug( s.getvalue().strip() ) + + #-------------------------------------------------------------------------- + def mean( self, t=None ): + # Remove any data older than t-self._dt + if t: + self.updateTo( t ) + + # No data in the buffer. + if self._lastIdx == -1: + return None + + # Compute the mean value and ignore any nans. + return np.nanmean( self.v[1] ) + + #-------------------------------------------------------------------------- + def sum( self, t=None ): + # Remove any data older than t-self._dt + if t: + self.updateTo( t ) + + # No data in the buffer. + if self._lastIdx == -1: + return None + + # Sum all the values and ignore any nans. + return np.nansum( self.v[1] ) + + #-------------------------------------------------------------------------- + def max( self, t=None ): + # Remove any data older than t-self._dt + if t: + self.updateTo( t ) + + # No data in the buffer. + if self._lastIdx == -1: + return None + + # Compute the max value and ignore any nans. + return np.nanmax( self.v[1] ) + + #-------------------------------------------------------------------------- + def min( self, t=None ): + # Remove any data older than t-self._dt + if t: + self.updateTo( t ) + + # No data in the buffer. + if self._lastIdx == -1: + return None + + # Compute the max value and ignore any nans. + return np.nanmin( self.v[1] ) + + #-------------------------------------------------------------------------- + def updateTo( self, time ): + if self._lastIdx == -1: + return + + # Delete any entries that are more than self._dt before the + # input time. + tBeg = time - self._dt + + # Find the indeces where the time is too old. This returns a + # tuple of len 1 so it can be used as an array index below. + i = np.where( self.v[0] < tBeg ) + + # Reset those values to nan + if i: + # Get an index of the current nan values. + nans = np.where( np.isnan( self.v[0] ) == True ) + + self.v[0][i] = np.nan + self.v[1][i] = np.nan + + # If the number of existing nan values and old values is the + # length of the array, reset lastIdx to indicate there is no + # data. This eliminates warnings about all-nan axis when + # doing computations. + if ( len( nans[0] ) + len( i[0] ) ) == self._len: + self._lastIdx = -1 + + #-------------------------------------------------------------------------- + def _nextIdx( self, index ): + nextIdx = index + 1 + if nextIdx == self._len: + nextIdx = 0 + + return nextIdx + + #-------------------------------------------------------------------------- + def _prevIdx( self, index ): + prevIdx = index - 1 + if prevIdx < 0: + prevIdx = self._len - 1 + + return prevIdx + + #-------------------------------------------------------------------------- + +#=========================================================================== +class Reader: + def __init__( self, config, log ): + self.config = config + self.log = log + + # Fields that use config.poll are used to return the average + # value over the poll interval. That decouples the interval we + # get messages in and the upload reporting interval. Some + # fields have fixed intervals that weather underground requires + # (like rain). + self.temps = [] + self.tempKeys = [] + for i in range( len( config.mqttTemp ) ): + self.temps.append( CircularTimeBuf( config.poll, config.maxRate, + "Temp %d" % i, self.log ) ) + if i == 0: + self.tempKeys.append( 'tempf' ) + else: + self.tempKeys.append( 'temp%df' % i ) + + self.humidity = CircularTimeBuf( config.poll, config.maxRate, + "Humidity", self.log ) + self.barometer = CircularTimeBuf( config.poll, config.maxRate, + "Barometer", self.log ) + + # Uploaded wind speed/dir are the average over the poll interval + # Gust values will be the maximum value over the poll interval. + self.windSpeed = CircularTimeBuf( config.poll, config.maxRate, + "Wind Speed", self.log ) + + # Average direction has to be computed by averaging the vector + # directions and then turning that back to an angle. See: + # http://www.webmet.com/met_monitoring/622.html + # A weighted version is shown here - but I'm not doing that + # because the speed and direction messages are separate - so + # this is just the averaged direction. + # east = speed * sin( dir ) + # north = speed * cos( dir ) + # ve = -1/n sum( east ) + # vn = -1/n sum( north ) + # Average dir = arctan( ve/vn ) +/- 180 + self.windEast = CircularTimeBuf( config.poll, config.maxRate, + "Wind Dir East", self.log ) + self.windNorth = CircularTimeBuf( config.poll, config.maxRate, + "Wind Dir North", self.log ) + + # Accumulate rain for the day. Store the current date when the + # first reading shows up. If the date changes, then we reset + # the accumulation. + self.rainDate = None + self.rainDayTotal = 0.0 + + # Accumulate rain for the last hour and allow at least 15 + # seconds between messages (acurite publishes every 36 seconds + # so this is fine). + self.rainHour = CircularTimeBuf( 3600.0, 15, "Rain Hour", self.log ) + + # MQTT will be pushing data to use and we'll be publishing it + # out so we need to lock when accessing the numpy matrix. + self.lock = threading.Lock() + + #-------------------------------------------------------------------------- + def readMsg( self, client, userData, msg ): + with self.lock: + topic = msg.topic + data = util.json.loads( msg.payload ) + self.log.info( "Read %s: %s" % ( topic, data ) ) + + time = data['time'] + + for i in range( len( self.config.mqttTemp ) ): + if topic == self.config.mqttTemp[i]: + self.temps[i].append( time, data['temperature' ] ) + break + + else: + if topic == self.config.mqttHumidity: + self.humidity.append( time, data['humidity' ] ) + + elif topic == self.config.mqttBarometer: + self.barometer.append( time, data['pressure' ] ) + + elif topic == self.config.mqttRain: + today = datetime.date.today() + + # Reset the rain if the local day changes. + if self.rainDate != today: + self.rainDayTotal = 0.0 + self.rainDate = today + self.log.info( "Update rain date to %s" % today ) + + self.rainDayTotal += data['rain'] + self.rainHour.append( time, data['rain'] ) + + elif topic == self.config.mqttWindSpeed: + self.windSpeed.append( time, data['speed'] ) + + elif topic == self.config.mqttWindDir: + # Note: if we multiple the trig terms by the speed, it + # would give us a weighted direction. But that + # requires matching the direction msg time with the + # speed msg time and requires they both exist. So it's + # easier to just average the directions. + angle = np.deg2rad( data['direction'] ) + self.windEast.append( time, np.sin( angle ) ) + self.windNorth.append( time, np.cos( angle ) ) + + #-------------------------------------------------------------------------- + def updatePayload( self, payload ): + with self.lock: + t = time.time() + dt = datetime.datetime.utcnow() + payload['dateutc'] = dt.strftime( '%Y-%m-%d %H:%M:%S' ) + + # Only update the payload dictionary if the value from the + # circular buffer isn't None. That can happen if we don't + # have any data for the requested interval. Round the result + # to a few digits to keep the upload message smaller. + def updateDict( key, value, digits=self.config.digits ): + if value is not None: + payload[key] = round( value, digits ) + + haveData = False + + if self.windSpeed: + haveData = True + updateDict( 'windspeedmph', self.windSpeed.mean( t ) ) + updateDict( 'windgustmph', self.windSpeed.max( t ) ) + + if self.windEast: + # Average the wind direction using vector math. + # See: http://www.webmet.com/met_monitoring/622.html + avgEast = - self.windEast.mean( t ) + avgNorth = - self.windNorth.mean( t ) + windDir = np.rad2deg( np.arctan2( avgEast, avgNorth ) ) + # Shift to the correct quadrant + if windDir < 180: + windDir += 180 + else: + windDir -= 180 + + updateDict( 'winddir', windDir ) + + if self.humidity: + haveData = True + updateDict( 'humidity', self.humidity.mean( t ) ) + + if self.barometer: + haveData = True + updateDict( 'baromin', self.barometer.mean( t ) ) + + if self.rainDate is not None: + haveData = True + updateDict( 'dailyrainin', self.rainDayTotal, 3 ) + updateDict( 'rainin', self.rainHour.sum( t ), 3 ) + + for i in range( len( self.temps ) ): + if self.temps[i]: + haveData = True + updateDict( self.tempKeys[i], self.temps[i].mean( t ) ) + + return haveData + + #-------------------------------------------------------------------------- + +#=========================================================================== +def start( config, client, debug=False ): + fromts = datetime.datetime.fromtimestamp + + log = util.log.get( "weatherUnderground" ) + + # URL arguments to send. + payloadBase = { + 'action' : 'updateraw', + 'ID' : config.id, + 'PASSWORD' : config.password, + 'dateutc' : None, # 'YYYY-MM-DD HH:MM:SS' + # 'winddir' : 0-360 + # 'windspeedmph' : speed in mph + # 'windgustmph' : gust in mph + # 'humidity' : 0-100% + # 'tempf' : temperature F + # 'temp2f' : temperature F + # 'rainin' : inches of rain over the last hour + # 'dailyrainin' : inches of rain over the local day + # 'baromin' : barometric pressure in inches + } + payload = payloadBase.copy() + + # Create a reader to process weather mes sages. + reader = Reader( config, log ) + client.on_message = reader.readMsg + + # Subscribe to all the weather topics we need to upload. + for topic in config.mqttTemp: + client.subscribe( topic ) + + client.subscribe( config.mqttHumidity ) + client.subscribe( config.mqttBarometer ) + client.subscribe( config.mqttRain ) + client.subscribe( config.mqttWindSpeed ) + client.subscribe( config.mqttWindDir ) + + # Start the MQTT as a background thread. This way we can run our + # upload process in the rest of the code. + client.loop_start() + + while True: + # Fill in the current values into the payload dict. + haveData = reader.updatePayload( payload ) + if haveData: + try: + log.info( "Uploading: %s" % payload ) + + # Send the HTTP request. + r = requests.get( config.uploadUrl, params=payload ) + log.debug( "URL: %s" % r.url ) + + if r.text.strip() != "success": + log.error( "WUG response: '%s'" % r.text ) + + except: + log.exception( "Upload failed to run" ) + + # Clear the payload back to the minimum set of fields. + payload = payloadBase.copy() + + else: + log.info( "Ignoring send opportunity - no data" ) + + # Sleep until the next time we should upload. + time.sleep( config.poll ) + +#=========================================================================== diff --git a/src/python/tHome/weatherUnderground/test/buf.py b/src/python/tHome/weatherUnderground/test/buf.py new file mode 100644 index 0000000..7768939 --- /dev/null +++ b/src/python/tHome/weatherUnderground/test/buf.py @@ -0,0 +1,71 @@ +import numpy as np +import warnings +import tHome.weatherUnderground.start as S + +buf = S.CircularTimeBuf( 60, 10 ) + +print "=================" +print "Empty" +print "=================" +print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() ) + +print "=================" +buf.append( 10, 1 ) +print buf.v +print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() ) +print + +buf.append( 20, 2 ) +print buf.v +print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() ) +print + +buf.append( 30, 3 ) +print buf.v +print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() ) +print + +buf.append( 40, 4 ) +print buf.v +print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() ) +print + +buf.append( 50, 5 ) +print buf.v +print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() ) +print + +buf.append( 60, 6 ) +print buf.v +print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() ) +print + +buf.append( 70, 7 ) +print buf.v +print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() ) +print +print "=================" + +buf.append( 120, 12 ) +print buf.v +print "Min/Max: %s / %s Avg: %s" % ( buf.min(), buf.max(), buf.mean() ) + +print "=================" +buf.append( 130, 13 ) +buf.append( 140, 14 ) +buf.append( 150, 15 ) +buf.append( 160, 16 ) +buf.append( 170, 17 ) +print buf.v +print +print "Update to t=200" +print "Min/Max: %s / %s Avg: %s" % ( buf.min(200), buf.max(200), buf.mean(200) ) +print buf.v + +print "=================" +print "Update to t=500" +buf.updateTo( 500 ) +print buf.v + +print "Min:",buf.min( 500 ) + diff --git a/src/requirements b/src/requirements new file mode 100644 index 0000000..55f5307 --- /dev/null +++ b/src/requirements @@ -0,0 +1,4 @@ +pip install astral +pip install paho-mqtt + + diff --git a/src/systemd/README.txt b/src/systemd/README.txt new file mode 100644 index 0000000..e2d60ad --- /dev/null +++ b/src/systemd/README.txt @@ -0,0 +1,22 @@ +Systemd notes: + +Install in: +/etc/systemd/system + +probably use sym links there. + +Then run: + systemctl start [name] + +If there are errors, run: + + systemctl status [name] + +After editing a service file, run: + + systemctl daemon-reload [name] + + +To enable auto-start at boot time: + + systemctl enable [name].service diff --git a/src/systemd/tHome-acurite.service b/src/systemd/tHome-acurite.service new file mode 100644 index 0000000..d52c1e4 --- /dev/null +++ b/src/systemd/tHome-acurite.service @@ -0,0 +1,14 @@ +[Unit] +Description=Acu-rite weather bridge reader +After=network.target + +[Service] +EnvironmentFile=/home/ted/proj/tHome/systemd/tHome.env +Type=simple +User=ted +ExecStart=/home/ted/proj/tHome/bin/tHome-acurite.py --log /var/log/tHome/acurite.log --configDir /home/ted/proj/tHome/conf + +[Install] +WantedBy=multi-user.target + + diff --git a/src/systemd/tHome-eagle.service b/src/systemd/tHome-eagle.service new file mode 100644 index 0000000..9e27760 --- /dev/null +++ b/src/systemd/tHome-eagle.service @@ -0,0 +1,13 @@ +[Unit] +Description=T-Home electric meter reader process +After=network.target + +[Service] +EnvironmentFile=/home/ted/proj/tHome/systemd/tHome.env +Type=simple +User=ted +ExecStart=/home/ted/proj/tHome/bin/tHome-eagle.py --log /var/log/tHome/eagle.log --configDir /home/ted/proj/tHome/conf + +[Install] +WantedBy=multi-user.target + diff --git a/src/systemd/tHome-sma.service b/src/systemd/tHome-sma.service new file mode 100644 index 0000000..bccfec4 --- /dev/null +++ b/src/systemd/tHome-sma.service @@ -0,0 +1,12 @@ +[Unit] +Description=T-Home SMA solar inverter reader +After=network.target + +[Service] +EnvironmentFile=/home/ted/proj/tHome/systemd/tHome.env +Type=simple +User=ted +ExecStart=/home/ted/proj/tHome/bin/tHome-sma.py --log /var/log/tHome/sma.log --configDir /home/ted/proj/tHome/conf + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/tHome-thermostat.service b/src/systemd/tHome-thermostat.service new file mode 100644 index 0000000..db085fd --- /dev/null +++ b/src/systemd/tHome-thermostat.service @@ -0,0 +1,15 @@ +[Unit] +Description=T-Home WIFI thermostat reader +After=network.target + +[Service] +EnvironmentFile=/home/ted/proj/tHome/systemd/tHome.env +Type=simple +User=ted +ExecStart=/home/ted/proj/tHome/bin/tHome-thermostat.py --log /var/log/tHome/thermostat.log --configDir /home/ted/proj/tHome/conf + +[Install] +WantedBy=multi-user.target + + + diff --git a/src/systemd/tHome.env b/src/systemd/tHome.env new file mode 100644 index 0000000..47771fb --- /dev/null +++ b/src/systemd/tHome.env @@ -0,0 +1,2 @@ +# Environment variables needed by systemd to use the services. +PYTHONPATH=/home/ted/python diff --git a/src/upstart/README.txt b/src/upstart/README.txt new file mode 100644 index 0000000..e5441c0 --- /dev/null +++ b/src/upstart/README.txt @@ -0,0 +1,6 @@ +Upstart notes: + +For problems, see /var/log/upstart/SERVICE.log + +sudo cat /var/log/upstart/tHome-msgHub.log + diff --git a/src/upstart/tHome-acurite.conf b/src/upstart/tHome-acurite.conf new file mode 100644 index 0000000..254cba6 --- /dev/null +++ b/src/upstart/tHome-acurite.conf @@ -0,0 +1,16 @@ +description "Acu-rite bridge reader" +author "Ted Drain" + +start on filesystem or runlevel [2345] +stop on shutdown + +env USER=ted +env CMD=/home/ted/proj/tHome/bin/tHome-acurite.py +env LOG=/var/log/tHome/acurite.log +env CONFIG=/home/ted/proj/tHome/conf + +script + exec start-stop-daemon --start -c $USER --exec $CMD -- $CONFIG $LOG +end script + + diff --git a/src/upstart/tHome-eagle.conf b/src/upstart/tHome-eagle.conf new file mode 100644 index 0000000..b786f15 --- /dev/null +++ b/src/upstart/tHome-eagle.conf @@ -0,0 +1,16 @@ +description "T-Home electric meter reader process" +author "Ted Drain" + +start on filesystem or runlevel [2345] +stop on shutdown + +env USER=ted +env CMD=/home/ted/proj/tHome/bin/tHome-eagle.py +env LOG=/var/log/tHome/eagle.log +env CONFIG=/home/ted/proj/tHome/conf + +script + exec start-stop-daemon --start -c $USER --exec $CMD -- -l $LOG -c $CONFIG +end script + + diff --git a/src/upstart/tHome-sma.conf b/src/upstart/tHome-sma.conf new file mode 100644 index 0000000..2a1f330 --- /dev/null +++ b/src/upstart/tHome-sma.conf @@ -0,0 +1,16 @@ +description "T-Home SMA solar inverter reader" +author "Ted Drain" + +start on filesystem or runlevel [2345] +stop on shutdown + +env USER=ted +env CMD=/home/ted/proj/tHome/bin/tHome-sma.py +env LOG=/var/log/tHome/sma.log +env CONFIG=/home/ted/proj/tHome/conf + +script + exec start-stop-daemon --start -c $USER --exec $CMD -- -l $LOG -c $CONFIG +end script + + diff --git a/src/upstart/tHome-thermostat.conf b/src/upstart/tHome-thermostat.conf new file mode 100644 index 0000000..542ce1e --- /dev/null +++ b/src/upstart/tHome-thermostat.conf @@ -0,0 +1,16 @@ +description "T-Home WIFI thermostat reader" +author "Ted Drain" + +start on filesystem or runlevel [2345] +stop on shutdown + +env USER=ted +env CMD=/home/ted/proj/tHome/bin/tHome-thermostat.py +env LOG=/var/log/tHome/thermostat.log +env CONFIG=/home/ted/proj/tHome/conf + +script + exec start-stop-daemon --start -c $USER --exec $CMD -- -l $LOG -c $CONFIG +end script + + diff --git a/src/upstart/tHome-wug.conf b/src/upstart/tHome-wug.conf new file mode 100644 index 0000000..c6987ee --- /dev/null +++ b/src/upstart/tHome-wug.conf @@ -0,0 +1,17 @@ +description "T-Home Weather Underground uploader" +author "Ted Drain" + +start on filesystem or runlevel [2345] +stop on shutdown + +env USER=ted +env CMD=/home/ted/proj/tHome/bin/tHome-wug.py +env LOG=/var/log/tHome/weatherUnderground.log +env CONFIG=/home/ted/proj/tHome/conf +env PYTHONPATH=/home/ted/python + +script + exec start-stop-daemon --start -c $USER --exec $CMD -- -l $LOG -c $CONFIG +end script + +