Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

## Python
*.py[cod]
__pycache__/


## App specific
Expand Down
11 changes: 9 additions & 2 deletions InverterMsg.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ def __get_string(self, begin, end):
Returns:
str: String in the message from start to end
"""
return self.raw_msg[begin:end]
try:
decoded_str = self.raw_msg[begin:end].decode('utf-8')
except UnicodeDecodeError:
decoded_str = self.raw_msg[begin:end].decode('utf-16')
except:
raise

return decoded_str

def __get_short(self, begin, divider=10):
"""Extract short from message.
Expand Down Expand Up @@ -70,7 +77,7 @@ def temperature(self):
@property
def power(self):
"""Power output"""
print self.__get_short(59)
return self.__get_short(59)

@property
def e_total(self):
Expand Down
2 changes: 1 addition & 1 deletion LiveStats.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
"""OmnikExport LiveStats

Get data from the omniksol inverter and output to console. This is a small
Expand Down
48 changes: 38 additions & 10 deletions OmnikExport.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
"""OmnikExport program.

Get data from an omniksol inverter with 602xxxxx - 606xxxx ans save the data in
Expand All @@ -8,10 +8,12 @@
import sys
import logging
import logging.config
import ConfigParser
import configparser
import os
from PluginLoader import Plugin
import InverterMsg # Import the Msg handler
import codecs
import re


class OmnikExport(object):
Expand All @@ -29,7 +31,7 @@ def __init__(self, config_file):
config_files = [self.__expand_path('config-default.cfg'),
self.__expand_path(config_file)]

self.config = ConfigParser.RawConfigParser()
self.config = configparser.RawConfigParser()
self.config.read(config_files)

def run(self):
Expand Down Expand Up @@ -70,15 +72,33 @@ def run(self):

wifi_serial = self.config.getint('inverter', 'wifi_sn')
inverter_socket.sendall(OmnikExport.generate_string(wifi_serial))
data = inverter_socket.recv(1024)
try:
data = inverter_socket.recv(1024)
except socket.timeout as msg:
self.logger.error('socket timed out')
self.logger.error(msg)
sys.exit(1)
inverter_socket.close()

msg = InverterMsg.InverterMsg(data)

self.logger.info("ID: {0}".format(msg.id))
# At this point the Inverter could be starting up or shutting down
# What appears to be common at this point is the ID isn't of the expected form
# when in error: some Chinese characters
# expecting something like: SF5K016008677
if re.match(r'^[A-Z0-9]+ *$', msg.id) == None:
self.logger.error('Inverter not in correct state - found "{}" - expecting \'^[A-Z0-9]+ *$\''.format(msg.id))
sys.exit(1)

try:
self.logger.info("ID: {0}".format(msg.id))
except UnicodeDecodeError:
print('some issue with data or InverterMsg decoding')
print('data == {}'.format(data))
print('msg == {}'.format(msg))

for plugin in Plugin.plugins:
self.logger.debug('Run plugin' + plugin.__class__.__name__)
self.logger.info('Run plugin' + plugin.__class__.__name__)
plugin.process_message(msg)

def build_logger(self, config):
Expand Down Expand Up @@ -153,15 +173,23 @@ def generate_string(serial_no):
Returns:
str: Information request string for inverter
"""
response = '\x68\x02\x40\x30'
# changed by python3
response = b'\x68\x02\x40\x30'

double_hex = hex(serial_no)[2:] * 2
hex_list = [double_hex[i:i + 2].decode('hex') for i in

# changed for python3
hex_list = [codecs.decode(double_hex[i:i + 2], 'hex') for i in
reversed(range(0, len(double_hex), 2))]

cs_count = 115 + sum([ord(c) for c in hex_list])
checksum = hex(cs_count)[-2:].decode('hex')
response += ''.join(hex_list) + '\x01\x00' + checksum + '\x16'

# changed for python3
checksum = codecs.decode(hex(cs_count)[-2:], 'hex')

# changed for python3
response += b''.join(hex_list) + b'\x01\x00' + checksum + b'\x16'

return response


Expand Down
3 changes: 1 addition & 2 deletions PluginLoader.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ def register_plugin(cls, new_plugin):
cls.plugins.append(instance)


class Plugin(object):
class Plugin(object, metaclass=PluginMount):
"""A plugin which must provide a process_message() method"""
__metaclass__ = PluginMount

config = None
logger = None
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
This fork is an attempt to change this project to be Python3 compatible.
There is an additional library (`solar_inverter_homie`) required for the MQTTOutput output type.
You will need to install this in a location Python look at or create an `PYTHONPATH` environment variable
that points to where you install that library folder.

# Omnik Data Logger
=====
Omnik Data Logger is a small script for uploading data from a Omniksol Solar
inverter, equipped with a wifi module, to a database and/or to PVoutput.org.

Expand Down Expand Up @@ -42,6 +46,6 @@ config.cfg, you can preserve your settings when upgrading.
## Development
To help with development when no sun is present a small simulator script can be
found in the folder Development. This script works by reading values from to
database used by de MysqlOutput, but with the time shifted 6 hours back. To use
database used by the MysqlOutput, but with the time shifted 6 hours back. To use
the simulator, you should use the MysqlOutput to fill the database and configure
database settings in de sim-config.cfg file.
database settings in the sim-config.cfg file.
15 changes: 14 additions & 1 deletion config-org.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[general]
# General:enabled_plugins
# Choose which outputs to use
# Possible options: MysqlOutput,PVoutputOutput,ConsoleOutput,CSVOutput
# Possible options: MysqlOutput,PVoutputOutput,ConsoleOutput,CSVOutput,MQTTOutput
enabled_plugins =

[inverter]
Expand All @@ -29,6 +29,10 @@ database =
# These two can be found at http://pvoutput.org/account.jsp
apikey = NOTAREALAPIKEY86e2258d4e29169fb79cf18b00
sysid = 12345
# Sometimes if you provide this and the precision isn't big enough, the change in energy isn't shown and
# then the average calculations in PVoutput are 0, causing the graph to "step". Set to true to provide
# your inverter energy value to PVoutput.
provide_energy_value = true

[csv]
disable_header = false
Expand All @@ -46,3 +50,12 @@ level = debug
# Log:filename
# Output file for file logger
filename = omnik-export.log

[mqtt]
# Host where the MQTT server is, including username and password
host =
port =
user =
passwd =
device_id =
name =
8 changes: 4 additions & 4 deletions outputs/CSVOutput.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ def process_message(self, msg):
msg (InverterMsg.InverterMsg): Message to process
"""
if not self.config.getboolean('csv', 'disable_header'):
print "DateTime,Id,Temp,VPV1,VPV2,VPV3,IPV1,IPV2,IPV3,IAC1,IAC2,IAC3," \
print("DateTime,Id,Temp,VPV1,VPV2,VPV3,IPV1,IPV2,IPV3,IAC1,IAC2,IAC3," \
"VAC1,VAC2,VAC3,FAC1,PAC1,FAC2,PAC2,FAC3,PAC3," \
"ETODAY,ETOTAL,HTOTAL"
"ETODAY,ETOTAL,HTOTAL")

timestamp = datetime.now().strftime('%Y-%m-%d %H:%M');
print ("{0},{1},{2},{3},{4},{5},{6},{7},{8},{9}," +
print(("{0},{1},{2},{3},{4},{5},{6},{7},{8},{9}," +
"{10},{11},{12},{13},{14},{15},{16},{17},{18},{19}," +
"{20},{21},{22},{23}")\
.format(timestamp, msg.id, msg.temperature,
Expand All @@ -28,4 +28,4 @@ def process_message(self, msg):
msg.f_ac(1), msg.p_ac(1),
msg.f_ac(2), msg.p_ac(2),
msg.f_ac(3), msg.p_ac(3),
msg.e_today, msg.e_total, msg.h_total)
msg.e_today, msg.e_total, msg.h_total))
26 changes: 13 additions & 13 deletions outputs/ConsoleOutput.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ def process_message(self, msg):
Args:
msg (InverterMsg.InverterMsg): Message to process
"""
print "ID: {0}".format(msg.id)
print("ID: {0}".format(msg.id))

print "E Today: {0:>5} Total: {1:<5}".format(msg.e_today, msg.e_total)
print "H Total: {0:>5} Temp: {1:<5}"\
.format(msg.h_total, msg.temperature)
print("E Today: {0:>5} Total: {1:<5}".format(msg.e_today, msg.e_total))
print("H Total: {0:>5} Temp: {1:<5}"\
.format(msg.h_total, msg.temperature))

print "PV1 V: {0:>5} I: {1:>4}".format(msg.v_pv(1), msg.i_pv(1))
print "PV2 V: {0:>5} I: {1:>4}".format(msg.v_pv(2), msg.i_pv(2))
print "PV3 V: {0:>5} I: {1:>4}".format(msg.v_pv(3), msg.i_pv(3))
print("PV1 V: {0:>5} I: {1:>4}".format(msg.v_pv(1), msg.i_pv(1)))
print("PV2 V: {0:>5} I: {1:>4}".format(msg.v_pv(2), msg.i_pv(2)))
print("PV3 V: {0:>5} I: {1:>4}".format(msg.v_pv(3), msg.i_pv(3)))

print "L1 P: {0:>5} V: {1:>5} I: {2:>4} F: {3:>5}"\
.format(msg.p_ac(1), msg.v_ac(1), msg.i_ac(1), msg.f_ac(1))
print "L2 P: {0:>5} V: {1:>5} I: {2:>4} F: {3:>5}"\
.format(msg.p_ac(2), msg.v_ac(2), msg.i_ac(2), msg.f_ac(2))
print "L3 P: {0:>5} V: {1:>5} I: {2:>4} F: {3:>5}"\
.format(msg.p_ac(3), msg.v_ac(3), msg.i_ac(3), msg.f_ac(3))
print("L1 P: {0:>5} V: {1:>5} I: {2:>4} F: {3:>5}"\
.format(msg.p_ac(1), msg.v_ac(1), msg.i_ac(1), msg.f_ac(1)))
print("L2 P: {0:>5} V: {1:>5} I: {2:>4} F: {3:>5}"\
.format(msg.p_ac(2), msg.v_ac(2), msg.i_ac(2), msg.f_ac(2)))
print("L3 P: {0:>5} V: {1:>5} I: {2:>4} F: {3:>5}"\
.format(msg.p_ac(3), msg.v_ac(3), msg.i_ac(3), msg.f_ac(3)))
51 changes: 51 additions & 0 deletions outputs/MQTTOutput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
##
# ----------------------------------------------------------------------------
# "THE BEER-WARE LICENSE" (Revision 42):
# <[email protected]> wrote this file. As long as you retain this notice you
# can do whatever you want with this stuff. If we meet some day, and you think
# this stuff is worth it, you can buy me a beer in return. Widmar
# ----------------------------------------------------------------------------
# Heavily updated by Christopher McAvaney <[email protected]>
# Now uses the Homie Convention library (https://github.com/mjcumming/homie4) with
# a locally defined "Solar Inverter Device".
##

import PluginLoader

from solar_inverter_homie import Device_Solar_Inverter
import time
#import logging


#logging.basicConfig(format='%(asctime)s %(message)s', level=logging.DEBUG)
#log = logging

class MQTTOutput(PluginLoader.Plugin):

def __init__(self):
# Translate config items to what homie mqtt library expects for mqtt settings
mqtt_settings = {
'MQTT_BROKER' : self.config.get('mqtt', 'host'),
'MQTT_PORT' : int(self.config.get('mqtt', 'port')),
'MQTT_USERNAME' : self.config.get('mqtt', 'user'),
'MQTT_PASSWORD' : self.config.get('mqtt', 'passwd'),
}

self.logger.info('{}: creating Device_Solar_Inverter() homie instance'.format(self.__class__.__name__))
self.solar_inverter_device = Device_Solar_Inverter( device_id=self.config.get('mqtt', 'device_id'), name=self.config.get('mqtt', 'name'), mqtt_settings=mqtt_settings )
time.sleep(1)

def process_message(self, msg):
self.logger.debug('process_message(): publishing')

pv_v_array = [msg.v_pv(1), msg.v_pv(2), msg.v_pv(3)]
pv_c_array = [msg.i_pv(1), msg.i_pv(2), msg.i_pv(3)]
ac_v_array = [msg.v_ac(1), msg.v_ac(2), msg.v_ac(3)]
ac_c_array = [msg.i_ac(1), msg.i_ac(2), msg.i_ac(3)]
ac_f_array = [msg.f_ac(1), msg.f_ac(2), msg.f_ac(3)]

self.solar_inverter_device.update_pv_voltage(pv_v_array, pv_c_array, ac_v_array, ac_c_array, ac_f_array)
self.solar_inverter_device.update_energy(msg.e_total, msg.e_today, msg.p_ac(1))
self.solar_inverter_device.update_status(True)

time.sleep(3)
36 changes: 20 additions & 16 deletions outputs/MysqlOutput.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,23 @@ def process_message(self, msg):
with con:
cur = con.cursor()
self.logger.debug('Executing SQL statement on database')
cur.execute("""INSERT INTO minutes
(InvID, timestamp, ETotal, EToday, Temp, HTotal, VPV1, VPV2, VPV3,
IPV1, IPV2, IPV3, VAC1, VAC2, VAC3, IAC1, IAC2, IAC3, FAC1, FAC2,
FAC3, PAC1, PAC2, PAC3)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
%s, %s, %s, %s, %s, %s, %s);""",
(msg.id, datetime.datetime.now(), msg.e_total,
msg.e_today, msg.temperature, msg.h_total,
msg.v_pv(1), msg.v_pv(2), msg.v_pv(3),
msg.i_pv(1), msg.i_pv(2), msg.i_pv(3),
msg.v_ac(1), msg.v_ac(2), msg.v_ac(3),
msg.i_ac(1), msg.i_ac(2), msg.i_ac(3),
msg.f_ac(1), msg.f_ac(2), msg.f_ac(3),
msg.p_ac(1), msg.p_ac(2), msg.p_ac(3)
))
try:
cur.execute("""INSERT INTO minutes
(InvID, timestamp, ETotal, EToday, Temp, HTotal, VPV1, VPV2, VPV3,
IPV1, IPV2, IPV3, VAC1, VAC2, VAC3, IAC1, IAC2, IAC3, FAC1, FAC2,
FAC3, PAC1, PAC2, PAC3)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
%s, %s, %s, %s, %s, %s, %s);""",
(msg.id, datetime.datetime.now(), msg.e_total,
msg.e_today, msg.temperature, msg.h_total,
msg.v_pv(1), msg.v_pv(2), msg.v_pv(3),
msg.i_pv(1), msg.i_pv(2), msg.i_pv(3),
msg.v_ac(1), msg.v_ac(2), msg.v_ac(3),
msg.i_ac(1), msg.i_ac(2), msg.i_ac(3),
msg.f_ac(1), msg.f_ac(2), msg.f_ac(3),
msg.p_ac(1), msg.p_ac(2), msg.p_ac(3)
))
except:
print('error with msg')
print('msg == {}'.format(msg))
Loading