First commit of ProStar Solar Panel driver.

This commit is contained in:
Patrick McDonagh
2017-11-15 21:07:23 -06:00
commit eae688c5fd
16 changed files with 989 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.pyc
.vscode/settings.json

View File

@@ -0,0 +1 @@
<module>Alerts</module>

View File

@@ -0,0 +1,42 @@
<div class="row row-flex box-me">
<div class="col-md-6 text-center">
<h2 class="uppercase">Public IP Address</h2>
<p><%= channels["prostarsolar.public_ip_address"].value %><p>
</div>
</div>
<style>
.uppercase {
text-transform: uppercase;
font-size: 14px;
color: #666;
font-weight: 400;
letter-spacing: 1px;
z-index: 100;
}
.box-me {
position: relative;
padding: 0.5em;
padding-bottom: 1.5em;
border: 1px solid #eee;
/*margin: 1em 0;*/
}
.row-flex {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
flex-wrap: wrap;
}
.row-flex > [class*='col-'] {
display: flex;
flex-direction: column;
}
p {
font-size: 1.25em;
}
</style>

View File

@@ -0,0 +1,10 @@
<div class='col-xs-1'>
<div class="<%= nodecolors.statuscolor %> nodecolor"></div>
</div>
<div class='col-xs-6'>
<h3><%= node.vanityname %></h3>
</div>
<div class='col-xs-5'>
<h4>Charge State: <%= channels['prostarsolar.charge_state'].value %></h4>
<h4>Array Fault: <%= channels['prostarsolar.array_fault'].value %></h4>
</div>

View File

@@ -0,0 +1,40 @@
<style>
.header h4 {
position: relative;
top: 0.9em;
}
.header h2 {
text-transform: uppercase;
font-size: 14px;
color: #aaa;
margin: 0.75em 0;
}
.header p {
font-size: 24px;
color: black;
font-weight: 600;
}
</style>
<div class="row header">
<div class="col-xs-1">
<div class="<%= nodecolors.statuscolor %> nodecolor"></div>
</div>
<div class="col-xs-2">
<img src="<%= nodeimgurl %>" />
</div>
<div class="col-xs-4">
<h4><%= node.vanityname %></h4>
</div>
<div class="col-xs-2">
<h2>Battery Voltage</h2>
<p><%= Math.round(channels['prostarsolar.adc_vbterm'].value * 100) / 100 %> V</p>
</div>
<div class="col-xs-2">
<h2>Charge Current</h2>
<p><%= Math.round(channels['prostarsolar.adc_ia'].value * 100) / 100 %> A</p>
</div>
</div>

View File

@@ -0,0 +1,273 @@
<div class="row box-me">
<div class="col-xs-4 text-center">
<h2>Array Current</h2>
<div class="gauge-box">
<div data-labelheight="10"
style="height: 170px; background: transparent; margin: 0 auto;"
id="gauge-adc_ia"
data-chart="solidgauge"
data-nodename="prostarsolar.adc_ia"
data-units="A"
data-min="0"
data-max="10"
data-decimalplaces="2"
data-colors="0.1:#DF5353,0.5:#DDDF0D,0.9:#55BF3B"
data-valuefontsize="18px">
</div>
<div class- "timestamp-box">
<a href="#" data-channelId="<%= channels['prostarsolar.adc_ia'].channelId %>" class="data-table" title="Download Channel History">
<i class="fa fa-download"></i>
</a>
</div>
<span data-timeupdate="adc_ia">
<%= channels["prostarsolar.adc_ia"].timestamp %>
</span>
</div>
</div>
<div class='col-xs-8'>
<div style="height:300px" id="chart-adc_ia" data-chart="chart" data-nodename1="prostarsolar.adc_ia" data-datalabel1="Array Current" data-daysofhistory="2" data-chartlabel="Array Current" data-ylabel="" data-xlabel="Date" data-units=" A"></div>
</div>
</div>
<div class="row box-me">
<div class="col-xs-4 text-center">
<h2>Load Current</h2>
<div class="gauge-box">
<div data-labelheight="10"
style="height: 170px; background: transparent; margin: 0 auto;"
id="gauge-adc_il"
data-chart="solidgauge"
data-nodename="prostarsolar.adc_il"
data-units="A"
data-min="0"
data-max="10"
data-decimalplaces="2"
data-colors="0.1:#DF5353,0.5:#DDDF0D,0.9:#55BF3B"
data-valuefontsize="18px">
</div>
<div class- "timestamp-box">
<a href="#" data-channelId="<%= channels['prostarsolar.adc_il'].channelId %>" class="data-table" title="Download Channel History">
<i class="fa fa-download"></i>
</a>
</div>
<span data-timeupdate="adc_il">
<%= channels["prostarsolar.adc_il"].timestamp %>
</span>
</div>
</div>
<div class='col-xs-8'>
<div style="height:300px" id="chart-adc_il" data-chart="chart" data-nodename1="prostarsolar.adc_il" data-datalabel1="Load Current" data-daysofhistory="2" data-chartlabel="Load Current" data-ylabel="" data-xlabel="Date" data-units=" A"></div>
</div>
</div>
<div class="row box-me">
<div class="col-xs-4 text-center">
<h2>Battery Voltage</h2>
<div class="gauge-box">
<div data-labelheight="10"
style="height: 170px; background: transparent; margin: 0 auto;"
id="gauge-adc_vbterm"
data-chart="solidgauge"
data-nodename="prostarsolar.adc_vbterm"
data-units="V"
data-min="0"
data-max="25"
data-decimalplaces="2"
data-colors="0.1:#DF5353,0.5:#DDDF0D,0.9:#55BF3B"
data-valuefontsize="18px">
</div>
<div class- "timestamp-box">
<a href="#" data-channelId="<%= channels['prostarsolar.adc_vbterm'].channelId %>" class="data-table" title="Download Channel History">
<i class="fa fa-download"></i>
</a>
</div>
<span data-timeupdate="adc_vbterm">
<%= channels["prostarsolar.adc_vbterm"].timestamp %>
</span>
</div>
</div>
<div class='col-xs-8'>
<div style="height:300px" id="chart-adc_vbterm" data-chart="chart" data-nodename1="prostarsolar.adc_vbterm" data-datalabel1="Battery Voltage" data-daysofhistory="2" data-chartlabel="Battery Voltage" data-ylabel="" data-xlabel="Date" data-units=" V"></div>
</div>
</div>
<div class="row box-me">
<div class="col-xs-4 text-center">
<h2>Array Voltage</h2>
<div class="gauge-box">
<div data-labelheight="10"
style="height: 170px; background: transparent; margin: 0 auto;"
id="gauge-adc_va"
data-chart="solidgauge"
data-nodename="prostarsolar.adc_va"
data-units="V"
data-min="0"
data-max="25"
data-decimalplaces="2"
data-colors="0.1:#DF5353,0.5:#DDDF0D,0.9:#55BF3B"
data-valuefontsize="18px">
</div>
<div class- "timestamp-box">
<a href="#" data-channelId="<%= channels['prostarsolar.adc_va'].channelId %>" class="data-table" title="Download Channel History">
<i class="fa fa-download"></i>
</a>
</div>
<span data-timeupdate="adc_va">
<%= channels["prostarsolar.adc_va"].timestamp %>
</span>
</div>
</div>
<div class='col-xs-8'>
<div style="height:300px" id="chart-adc_va" data-chart="chart" data-nodename1="prostarsolar.adc_va" data-datalabel1="Array Voltage" data-daysofhistory="2" data-chartlabel="Array Voltage" data-ylabel="" data-xlabel="Date" data-units=" V"></div>
</div>
</div>
<div class="row box-me">
<div class="col-xs-4 text-center">
<h2>Load Voltage</h2>
<div class="gauge-box">
<div data-labelheight="10"
style="height: 170px; background: transparent; margin: 0 auto;"
id="gauge-adc_vl"
data-chart="solidgauge"
data-nodename="prostarsolar.adc_vl"
data-units="V"
data-min="0"
data-max="25"
data-decimalplaces="2"
data-colors="0.1:#DF5353,0.5:#DDDF0D,0.9:#55BF3B"
data-valuefontsize="18px">
</div>
<div class- "timestamp-box">
<a href="#" data-channelId="<%= channels['prostarsolar.adc_vl'].channelId %>" class="data-table" title="Download Channel History">
<i class="fa fa-download"></i>
</a>
</div>
<span data-timeupdate="adc_vl">
<%= channels["prostarsolar.adc_vl"].timestamp %>
</span>
</div>
</div>
<div class='col-xs-8'>
<div style="height:300px" id="chart-adc_vl" data-chart="chart" data-nodename1="prostarsolar.adc_vl" data-datalabel1="Load Voltage" data-daysofhistory="2" data-chartlabel="Load Voltage" data-ylabel="" data-xlabel="Date" data-units=" V"></div>
</div>
</div>
<div class="row box-me">
<div class="col-xs-4 text-center">
<h2>Ambient Temp</h2>
<div class="gauge-box">
<div data-labelheight="10"
style="height: 170px; background: transparent; margin: 0 auto;"
id="gauge-t_amb"
data-chart="solidgauge"
data-nodename="prostarsolar.t_amb"
data-units="deg C"
data-min="0"
data-max="50"
data-decimalplaces="2"
data-colors="0.1:#DF5353,0.5:#DDDF0D,0.9:#55BF3B"
data-valuefontsize="18px">
</div>
<div class- "timestamp-box">
<a href="#" data-channelId="<%= channels['prostarsolar.t_amb'].channelId %>" class="data-table" title="Download Channel History">
<i class="fa fa-download"></i>
</a>
</div>
<span data-timeupdate="t_amb">
<%= channels["prostarsolar.t_amb"].timestamp %>
</span>
</div>
</div>
<div class='col-xs-8'>
<div style="height:300px" id="chart-t_amb" data-chart="chart" data-nodename1="prostarsolar.t_amb" data-datalabel1="Ambient Temp" data-daysofhistory="2" data-chartlabel="Ambient Temp" data-ylabel="" data-xlabel="Date" data-units=" deg C"></div>
</div>
</div>
<style>
.box-me {
position: relative;
padding: 0.5em;
padding-bottom: 1.5em;
border: 1px solid #eee;
/*margin: 1em 0;*/
}
.box-me .gauge-box {
margin-top: -0.25em;
}
.pad15 {
margin: 15px 15px;
}
.box-me h2 {
text-transform: uppercase;
font-size: 14px;
color: #666;
font-weight: 400;
letter-spacing: 1px;
z-index: 100;
}
.dynamic-chart-form {
background-color: whiteSmoke;
padding: 1em 0.5em;
margin-top: 1em;
}
.row-flex {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
flex-wrap: wrap;
}
.row-flex > [class*='col-'] {
display: flex;
flex-direction: column;
}
#systemStatusTimelineContainer h2 {
text-transform: uppercase;
font-size: 14px;
color: #666;
font-weight: 400;
letter-spacing: 1px;
z-index: 100;
}
.slice.node-detail hr {
border-color: #ccc;
}
.slice.node-detail #alarms li {
margin-bottom: 1em;
padding: 0.5em;
}
.slice.node-detail #alarms li:nth-child(even){
background-color: whiteSmoke;
}
.slice.node-detail #alarms li span {
margin-left: 1em;
color: #aaa;
}
</style>
<script>
$('.val_box').each(function(topLevel){
$(this).change(function(){
var id = "#" + $(this).closest(".entry-top-level").attr('id');
if (id !== "#undefined"){
var val = $(id).find('.val_box').val();
var tag = $(id).find('.setstatic').attr('data-staticsend', val);
console.log($(id).find('.setstatic').attr('data-staticsend'));
}
});
});
</script>

View File

@@ -0,0 +1,15 @@
<a href="#"
data-channelId="<%= channels["prostarsolar.log"].channelId %>"
class="data-table btn-block btn btn-theme animated"
title="Device Log"><i style='margin-left: 0.5em; cursor: pointer' class="fa fa-th-list icon-theme"></i> Device Log</a>
<a href="#"
data-refreshpause="1"
data-staticsend="1"
data-channelId="<%= channels["prostarsolar.sync"].channelId %>"
data-techname="<%=channels["prostarsolar.sync"].techName %>"
data-name="<%= channels["prostarsolar.sync"].name%>"
data-nodechannelcurrentId="<%= channels["prostarsolar.sync"].nodechannelcurrentId %>"
id="<%= channels["prostarsolar.sync"].channelId %>"
class="btn btn-large btn-block btn-theme animated setstatic mqtt">
<i class="icon-repeat icon-white mqtt" ></i>Sync All Data</a>

View File

@@ -0,0 +1,37 @@
<div class='col-xs-12' style="padding-top: 1em; margin-bottom: 1em;">
<div class="input-daterange input-group" id="datepicker">
<input data-chartid="dynamicChart" id="fromDate" data-daysofhistory="7" type="text" class="form-control" name="start">
<span class="input-group-addon">to</span>
<input class="form-control" data-chartid="dynamicChart" id="toDate" type="text" name="end">
<span class='input-group-btn'>
<a href="#!" data-chartid="dynamicChart" data-otherchartids="statusTimeline" class="btn chart-update btn-theme">Run</a>
</span>
</div>
</div>
<hr>
<div class='clearfix col-xs-12'
style='height: 450px'
id="dynamicChart"
data-chart="dynamicchart"
data-daysofhistory="7"
data-chartlabel="Data"
data-ylabel=""
data-xlabel="Date"
data-units=""
data-channelnames="prostarsolar.adc_ia,prostarsolar.adc_vbterm,prostarsolar.adc_va,prostarsolar.adc_vl,prostarsolar.adc_il,prostarsolar.t_amb,prostarsolar.vb_min_daily,prostarsolar.vb_max_daily">
</div>
<style>
.dynamic-chart-form {
background-color: whiteSmoke;
padding: 1em 0.5em;
margin-top: 1em;
}
#systemStatusTimelineContainer h2 {
text-transform: uppercase;
font-size: 14px;
color: #666;
font-weight: 400;
letter-spacing: 1px;
z-index: 100;
}
</style>

BIN
logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

276
python-driver/Channel.py Normal file
View File

@@ -0,0 +1,276 @@
"""Define Meshify channel class."""
from pycomm.ab_comm.clx import Driver as ClxDriver
from pycomm.cip.cip_base import CommError, DataError
import time
def binarray(intval):
"""Split an integer into its bits."""
bin_string = '{0:08b}'.format(intval)
bin_arr = [i for i in bin_string]
bin_arr.reverse()
return bin_arr
def read_tag(addr, tag):
"""Read a tag from the PLC."""
c = ClxDriver()
try:
if c.open(addr):
try:
v = c.read_tag(tag)
return v
except DataError:
c.close()
print("Data Error during readTag({}, {})".format(addr, tag))
except CommError:
# err = c.get_status()
c.close()
print("Could not connect during readTag({}, {})".format(addr, tag))
# print err
except AttributeError as e:
c.close()
print("AttributeError during readTag({}, {}): \n{}".format(addr, tag, e))
c.close()
return False
def read_array(addr, tag, start, end):
"""Read an array from the PLC."""
c = ClxDriver()
if c.open(addr):
arr_vals = []
try:
for i in range(start, end):
tag_w_index = tag + "[{}]".format(i)
v = c.read_tag(tag_w_index)
# print('{} - {}'.format(tag_w_index, v))
arr_vals.append(round(v[0], 4))
# print(v)
if len(arr_vals) > 0:
return arr_vals
else:
print("No length for {}".format(addr))
return False
except Exception:
print("Error during readArray({}, {}, {}, {})".format(addr, tag, start, end))
err = c.get_status()
c.close()
print err
pass
c.close()
def write_tag(addr, tag, val):
"""Write a tag value to the PLC."""
c = ClxDriver()
if c.open(addr):
try:
cv = c.read_tag(tag)
wt = c.write_tag(tag, val, cv[1])
return wt
except Exception:
print("Error during writeTag({}, {}, {})".format(addr, tag, val))
err = c.get_status()
c.close()
print err
c.close()
class Channel(object):
"""Holds the configuration for a Meshify channel."""
def __init__(self, mesh_name, data_type, chg_threshold, guarantee_sec, map_=False, write_enabled=False):
"""Initialize the channel."""
self.mesh_name = mesh_name
self.data_type = data_type
self.last_value = None
self.value = None
self.last_send_time = 0
self.chg_threshold = chg_threshold
self.guarantee_sec = guarantee_sec
self.map_ = map_
self.write_enabled = write_enabled
def __str__(self):
"""Create a string for the channel."""
return "{}\nvalue: {}, last_send_time: {}".format(self.mesh_name, self.value, self.last_send_time)
def check(self, new_value, force_send=False):
"""Check to see if the new_value needs to be stored."""
send_needed = False
send_reason = ""
if self.data_type == 'BOOL' or self.data_type == 'STRING':
if self.last_send_time == 0:
send_needed = True
send_reason = "no send time"
elif self.value is None:
send_needed = True
send_reason = "no value"
elif not (self.value == new_value):
if self.map_:
if not self.value == self.map_[new_value]:
send_needed = True
send_reason = "value change"
else:
send_needed = True
send_reason = "value change"
elif (time.time() - self.last_send_time) > self.guarantee_sec:
send_needed = True
send_reason = "guarantee sec"
elif force_send:
send_needed = True
send_reason = "forced"
else:
if self.last_send_time == 0:
send_needed = True
send_reason = "no send time"
elif self.value is None:
send_needed = True
send_reason = "no value"
elif abs(self.value - new_value) > self.chg_threshold:
send_needed = True
send_reason = "change threshold"
elif (time.time() - self.last_send_time) > self.guarantee_sec:
send_needed = True
send_reason = "guarantee sec"
elif force_send:
send_needed = True
send_reason = "forced"
if send_needed:
self.last_value = self.value
if self.map_:
try:
self.value = self.map_[new_value]
except KeyError:
print("Cannot find a map value for {} in {} for {}".format(new_value, self.map_, self.mesh_name))
self.value = new_value
else:
self.value = new_value
self.last_send_time = time.time()
print("Sending {} for {} - {}".format(self.value, self.mesh_name, send_reason))
return send_needed
def read(self):
"""Read the value."""
pass
def identity(sent):
"""Returns exactly what was sent to it."""
return sent
class ModbusChannel(Channel):
"""Modbus channel object."""
def __init__(self, mesh_name, register_number, data_type, chg_threshold, guarantee_sec, map_=False, write_enabled=False, transformFn=identity):
"""Initialize the channel."""
super(ModbusChannel, self).__init__(mesh_name, data_type, chg_threshold, guarantee_sec, map_, write_enabled)
self.mesh_name = mesh_name
self.register_number = register_number
self.data_type = data_type
self.last_value = None
self.value = None
self.last_send_time = 0
self.chg_threshold = chg_threshold
self.guarantee_sec = guarantee_sec
self.map_ = map_
self.write_enabled = write_enabled
self.transformFn = transformFn
def read(self, mbsvalue):
return self.transformFn(mbsvalue)
class PLCChannel(Channel):
"""PLC Channel Object."""
def __init__(self, ip, mesh_name, plc_tag, data_type, chg_threshold, guarantee_sec, map_=False, write_enabled=False):
"""Initialize the channel."""
super(PLCChannel, self).__init__(mesh_name, data_type, chg_threshold, guarantee_sec, map_, write_enabled)
self.plc_ip = ip
self.mesh_name = mesh_name
self.plc_tag = plc_tag
self.data_type = data_type
self.last_value = None
self.value = None
self.last_send_time = 0
self.chg_threshold = chg_threshold
self.guarantee_sec = guarantee_sec
self.map_ = map_
self.write_enabled = write_enabled
def read(self):
"""Read the value."""
plc_value = None
if self.plc_tag and self.plc_ip:
read_value = read_tag(self.plc_ip, self.plc_tag)
if read_value:
plc_value = read_value[0]
return plc_value
class BoolArrayChannels(Channel):
"""Hold the configuration for a set of boolean array channels."""
def __init__(self, ip, mesh_name, plc_tag, data_type, chg_threshold, guarantee_sec, map_=False, write_enabled=False):
"""Initialize the channel."""
self.plc_ip = ip
self.mesh_name = mesh_name
self.plc_tag = plc_tag
self.data_type = data_type
self.last_value = None
self.value = None
self.last_send_time = 0
self.chg_threshold = chg_threshold
self.guarantee_sec = guarantee_sec
self.map_ = map_
self.write_enabled = write_enabled
def compare_values(self, new_val_dict):
"""Compare new values to old values to see if the values need storing."""
send = False
for idx in new_val_dict:
try:
if new_val_dict[idx] != self.last_value[idx]:
send = True
except KeyError:
print("Key Error in self.compare_values for index {}".format(idx))
send = True
return send
def read(self, force_send=False):
"""Read the value and check to see if needs to be stored."""
send_needed = False
send_reason = ""
if self.plc_tag:
v = read_tag(self.plc_ip, self.plc_tag)
if v:
bool_arr = binarray(v[0])
new_val = {}
for idx in self.map_:
try:
new_val[self.map_[idx]] = bool_arr[idx]
except KeyError:
print("Not able to get value for index {}".format(idx))
if self.last_send_time == 0:
send_needed = True
send_reason = "no send time"
elif self.value is None:
send_needed = True
send_reason = "no value"
elif self.compare_values(new_val):
send_needed = True
send_reason = "value change"
elif (time.time() - self.last_send_time) > self.guarantee_sec:
send_needed = True
send_reason = "guarantee sec"
elif force_send:
send_needed = True
send_reason = "forced"
if send_needed:
self.value = new_val
self.last_value = self.value
self.last_send_time = time.time()
print("Sending {} for {} - {}".format(self.value, self.mesh_name, send_reason))
return send_needed

47
python-driver/Maps.py Normal file
View File

@@ -0,0 +1,47 @@
"""Holds map values for prostarsolar."""
def charge_state(inp_state):
"""Map function for charge state."""
states = {
0: "Start",
1: "Night Check",
2: "Disconnect",
3: "Night",
4: "Fault",
5: "Bulk",
6: "Absorption",
7: "Float",
8: "Equalize"
}
if inp_state in range(0,9):
return states[inp_state]
else:
return inp_state
def array_faults(inp_array_faults):
"""Form a string for the array_faults."""
fault_string = ""
faults = {
0: "Overcurrent Phase 1",
1: "FETs Shorted",
2: "Software Bug",
3: "Battery HVD (High Voltage Disconnect)",
4: "Array HVD (High Voltage Disconnect)",
5: "EEPROM Setting Edit (reset required)",
6: "RTS Shorted",
7: "RTS was valid now disconnected",
8: "Local temp. sensor failed",
9: "Battery LVD (Low Voltage Disconect)",
10: "DIP Switch Changed (excl. DIP 8)",
11: "Processor Supply Fault"
}
bit_string = ("0" * 16 + "{0:b}".format(inp_array_faults))[-16:]
for i in range(0, 12):
if int(bit_string[i]) == 1:
fault_string += faults[i] + ", "
if fault_string:
return fault_string[:-2]
else:
return "None"

14
python-driver/config.txt Normal file
View File

@@ -0,0 +1,14 @@
{
"driverFileName":"prostarsolar.py",
"deviceName":"prostarsolar",
"driverId":"0160",
"releaseVersion":"1",
"files": {
"file1":"prostarsolar.py",
"file2":"Channel.py",
"file3":"Maps.py",
"file4":"Scheduler.py"
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "prostarsolar",
"driverFilename": "prostarsolar.py",
"driverId": "0000",
"additionalDriverFiles": [
"utilities.py",
"persistence.py",
"Channel.py",
"Maps.py"
],
"version": 1,
"s3BucketName": "prostarsolar"
}

View File

@@ -0,0 +1,21 @@
"""Data persistance functions."""
# if more advanced persistence is needed, use a sqlite database
import json
def load(filename="persist.json"):
"""Load persisted settings from the specified file."""
try:
with open(filename, 'r') as persist_file:
return json.load(persist_file)
except Exception:
return False
def store(persist_obj, filename="persist.json"):
"""Store the persisting settings into the specified file."""
try:
with open(filename, 'w') as persist_file:
return json.dump(persist_obj, persist_file)
except Exception:
return False

View File

@@ -0,0 +1,157 @@
"""Driver for prostarsolar."""
import threading
from device_base import deviceBase
from Channel import read_tag, write_tag
from Channel import ModbusChannel
from Maps import charge_state, array_faults
import persistence
from random import randint
from utilities import get_public_ip_address, int_to_float16
import json
import time
import minimalmodbus
import minimalmodbusM1
_ = None
# GLOBAL VARIABLES
WATCHDOG_SEND_PERIOD = 3600 # Seconds, the longest amount of time before sending the watchdog status
PLC_IP_ADDRESS = "192.168.1.10"
CHANNELS = [
ModbusChannel("adc_ia", 17, "REAL", 0.5, 3600, transformFn=int_to_float16),
ModbusChannel("adc_vbterm", 18, "REAL", 0.5, 3600, transformFn=int_to_float16),
ModbusChannel("adc_va", 19, "REAL", 0.5, 3600, transformFn=int_to_float16),
ModbusChannel("adc_vl", 20, "REAL", 0.5, 3600, transformFn=int_to_float16),
ModbusChannel("adc_il", 22, "REAL", 0.5, 3600, transformFn=int_to_float16),
ModbusChannel("t_amb", 28, "REAL", 2.0, 3600, transformFn=int_to_float16),
ModbusChannel("vb_min_daily", 65, "REAL", 2.0, 3600, transformFn=int_to_float16),
ModbusChannel("vb_max_daily", 66, "REAL", 2.0, 3600, transformFn=int_to_float16),
ModbusChannel('charge_state', 33, "STRING", 1, 3600, transformFn=charge_state),
ModbusChannel('array_fault', 34, "STRING", 1, 3600, transformFn=array_faults)
]
# PERSISTENCE FILE
persist = persistence.load()
class start(threading.Thread, deviceBase):
"""Start class required by Meshify."""
def __init__(self, name=None, number=None, mac=None, Q=None, mcu=None, companyId=None, offset=None, mqtt=None, Nodes=None):
"""Initialize the driver."""
threading.Thread.__init__(self)
deviceBase.__init__(self, name=name, number=number, mac=mac, Q=Q, mcu=mcu, companyId=companyId, offset=offset, mqtt=mqtt, Nodes=Nodes)
self.daemon = True
self.version = "1"
self.finished = threading.Event()
self.forceSend = False
threading.Thread.start(self)
# this is a required function for all drivers, its goal is to upload some piece of data
# about your device so it can be seen on the web
def register(self):
"""Register the driver."""
# self.sendtodb("log", "BOOM! Booted.", 0)
pass
def run(self):
"""Actually run the driver."""
global persist
wait_sec = 60
for i in range(0, wait_sec):
print("prostarsolar driver will start in {} seconds".format(wait_sec - i))
time.sleep(1)
print("BOOM! Starting prostarsolar driver...")
self.nodes["prostarsolar_0199"] = self
public_ip_address = get_public_ip_address()
self.sendtodbDev(1, 'public_ip_address', public_ip_address, 0, 'prostarsolar')
# watchdog = self.prostarsolar_watchdog()
# self.sendtodbDev(1, 'watchdog', watchdog, 0, 'prostarsolar')
# watchdog_send_timestamp = time.time()
connected_to_485 = False
while connected_to_485 is False:
connected_to_485 = self.mcu.set485Baud(9600)
serial_485 = self.mcu.rs485
instrument_485 = minimalmodbusM1.Instrument(1, serial_485)
instrument_485.address = 1
send_loops = 0
watchdog_loops = 0
watchdog_check_after = 5000
while True:
if self.forceSend:
print "FORCE SEND: TRUE"
for chan in CHANNELS:
try:
val = chan.read(instrument_485.read_register(chan.register_number, functioncode=4))
if chan.check(val, self.forceSend):
self.sendtodbDev(1, chan.mesh_name, chan.value, 0, 'prostarsolar')
time.sleep(0.1)
except IOError as e:
print("IO Error: {}".format(e))
print("Attempting to reconnect to rs485 device")
connected_to_485 = False
while connected_to_485 is False:
connected_to_485 = self.mcu.set485Baud(9600)
serial_485 = self.mcu.rs485
instrument_485 = minimalmodbusM1.Instrument(1, serial_485)
instrument_485.address = 1
# print("prostarsolar driver still alive...")
if self.forceSend:
if send_loops > 2:
print("Turning off forceSend")
self.forceSend = False
send_loops = 0
else:
send_loops += 1
watchdog_loops += 1
if (watchdog_loops >= watchdog_check_after):
# test_watchdog = self.prostarsolar_watchdog()
# if not test_watchdog == watchdog or (time.time() - watchdog_send_timestamp) > WATCHDOG_SEND_PERIOD:
# self.sendtodbDev(1, 'watchdog', test_watchdog, 0, 'prostarsolar')
# watchdog = test_watchdog
test_public_ip = get_public_ip_address()
if not test_public_ip == public_ip_address:
self.sendtodbDev(1, 'public_ip_address', test_public_ip, 0, 'prostarsolar')
public_ip_address = test_public_ip
watchdog_loops = 0
time.sleep(10)
def prostarsolar_watchdog(self):
"""Write a random integer to the PLC and then 1 seconds later check that it has been decremented by 1."""
randval = randint(0, 32767)
write_tag(str(PLC_IP_ADDRESS), 'watchdog_INT', randval)
time.sleep(1)
watchdog_val = read_tag(str(PLC_IP_ADDRESS), 'watchdog_INT')
try:
return (randval - 1) == watchdog_val[0]
except (KeyError, TypeError):
return False
def prostarsolar_sync(self, name, value):
"""Sync all data from the driver."""
self.forceSend = True
# self.sendtodb("log", "synced", 0)
return True
def prostarsolar_writeplctag(self, name, value):
"""Write a value to the PLC."""
new_val = json.loads(str(value).replace("'", '"'))
tag_n = str(new_val['tag']) # "cmd_Start"
val_n = new_val['val']
w = write_tag(str(PLC_IP_ADDRESS), tag_n, val_n)
print("Result of prostarsolar_writeplctag(self, {}, {}) = {}".format(name, value, w))
if w is None:
w = "Error writing to PLC..."
return w

View File

@@ -0,0 +1,40 @@
"""Utility functions for the driver."""
import socket
def get_public_ip_address():
"""Find the public IP Address of the host device."""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
def int_to_float16(int_to_convert):
"""Convert integer into float16 representation."""
bin_rep = ('0' * 16 + '{0:b}'.format(int_to_convert))[-16:]
sign = 1
if int(bin_rep[0]) == 1:
sign = -1
exponent = int(bin_rep[1:6], 2)
fraction = int(bin_rep[7:17], 2)
return sign * 2 ** (exponent - 15) * float("1.{}".format(fraction))
def degf_to_degc(temp_f):
"""Convert deg F to deg C."""
return (temp_f - 32.0) * (5.0/9.0)
def degc_to_degf(temp_c):
"""Convert deg C to deg F."""
return temp_c * 1.8 + 32.0
def reverse_map(value, map_):
"""Perform the opposite of mapping to an object."""
for x in map_:
if map_[x] == value:
return x
return None