Rewriting from scratch...
This commit is contained in:
589
plugin.py
589
plugin.py
@ -1,18 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# DS238-2 D/ZN ModbusTCP
|
||||
#
|
||||
# Source: https://github.com/xbeaudouin/domoticz-ds238-modbus-tcp
|
||||
# Author: Xavier Beaudouin (https://oav.net/)
|
||||
# License: MIT
|
||||
#
|
||||
# Based on the work of :
|
||||
# Source: https://github.com/addiejanssen/domoticz-solaredge-modbustcp-plugin
|
||||
# Author: Addie Janssen (https://addiejanssen.com)
|
||||
# License: MIT
|
||||
#
|
||||
|
||||
"""
|
||||
Hiking DDS238-2 ZN/S Energy Meter
|
||||
Author: Xavier Beaudouin
|
||||
Requirements:
|
||||
1. modbus over TCP adapter like PW21
|
||||
2. pymodbus AND pymodbusTCP
|
||||
"""
|
||||
"""
|
||||
<plugin key="DS238_ModbusTCP" name="DS238-2 D/ZN ModbusTCP" author="Xavier Beaudouin" version="0.0.1" externallink="https://github.com/xbeaudouin/domoticz-ds238-modbus-tcp">
|
||||
<params>
|
||||
@ -24,22 +17,7 @@
|
||||
<option label="No" value="No" />
|
||||
</options>
|
||||
</param>
|
||||
<param field="Mode2" label="Interval" width="100px" required="true" default="5" >
|
||||
<options>
|
||||
<option label="5 seconds" value="5" />
|
||||
<option label="10 seconds" value="10" default="true" />
|
||||
<option label="20 seconds" value="20" />
|
||||
<option label="30 seconds" value="30" />
|
||||
<option label="60 seconds" value="60" />
|
||||
</options>
|
||||
</param>
|
||||
<param field="Mode3" label="Modbus address" width="100px" required="true" default="1" />
|
||||
<param field="Mode5" label="Log level" width="100px">
|
||||
<options>
|
||||
<option label="Normal" value="Normal" default="true" />
|
||||
<option label="Extra" value="Extra"/>
|
||||
</options>
|
||||
</param>
|
||||
<param field="Mode6" label="Debug" width="100px">
|
||||
<options>
|
||||
<option label="True" value="Debug"/>
|
||||
@ -51,485 +29,89 @@
|
||||
"""
|
||||
|
||||
import Domoticz
|
||||
import solaredge_modbus
|
||||
import json
|
||||
import sys
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from enum import IntEnum, unique, auto
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
sys.path.append('/usr/local/lib/python3.4/dist-packages')
|
||||
sys.path.append('/usr/local/lib/python3.5/dist-packages')
|
||||
sys.path.append('/usr/local/lib/python3.6/dist-packages')
|
||||
sys.path.append('/usr/local/lib/python3.7/dist-packages')
|
||||
sys.path.append('/usr/local/lib/python3.8/dist-packages')
|
||||
|
||||
#
|
||||
# Domoticz shows graphs with intervals of 5 minutes.
|
||||
# When collecting information from the inverter more frequently than that, then it makes no sense to only show the last value.
|
||||
#
|
||||
# The Average class can be used to calculate the average value based on a sliding window of samples.
|
||||
# The number of samples stored depends on the interval used to collect the value from the inverter itself.
|
||||
#
|
||||
import pymodbus
|
||||
|
||||
class Average:
|
||||
|
||||
def __init__(self):
|
||||
self.samples = []
|
||||
self.max_samples = 30
|
||||
|
||||
def set_max_samples(self, max):
|
||||
self.max_samples = max
|
||||
if self.max_samples < 1:
|
||||
self.max_samples = 1
|
||||
|
||||
def update(self, new_value, scale = 0):
|
||||
self.samples.append(new_value * (10 ** scale))
|
||||
while (len(self.samples) > self.max_samples):
|
||||
del self.samples[0]
|
||||
|
||||
Domoticz.Debug("Average: {} - {} values".format(self.get(), len(self.samples)))
|
||||
|
||||
def get(self):
|
||||
return sum(self.samples) / len(self.samples)
|
||||
|
||||
#
|
||||
# Domoticz shows graphs with intervals of 5 minutes.
|
||||
# When collecting information from the inverter more frequently than that, then it makes no sense to only show the last value.
|
||||
#
|
||||
# The Maximum class can be used to calculate the highest value based on a sliding window of samples.
|
||||
# The number of samples stored depends on the interval used to collect the value from the inverter itself.
|
||||
#
|
||||
|
||||
class Maximum:
|
||||
|
||||
def __init__(self):
|
||||
self.samples = []
|
||||
self.max_samples = 30
|
||||
|
||||
def set_max_samples(self, max):
|
||||
self.max_samples = max
|
||||
if self.max_samples < 1:
|
||||
self.max_samples = 1
|
||||
|
||||
def update(self, new_value, scale = 0):
|
||||
self.samples.append(new_value * (10 ** scale))
|
||||
while (len(self.samples) > self.max_samples):
|
||||
del self.samples[0]
|
||||
|
||||
Domoticz.Debug("Maximum: {} - {} values".format(self.get(), len(self.samples)))
|
||||
|
||||
def get(self):
|
||||
return max(self.samples)
|
||||
|
||||
#
|
||||
# The Unit class lists all possible pieces of information that can be retrieved from the inverter.
|
||||
#
|
||||
# Not all inverters will support all these options.
|
||||
# The class is used to generate a unique id for each device in Domoticz.
|
||||
#
|
||||
|
||||
@unique
|
||||
class Unit(IntEnum):
|
||||
|
||||
STATUS = 1
|
||||
VENDOR_STATUS = 2
|
||||
CURRENT = 3
|
||||
P1_CURRENT = 4
|
||||
P2_CURRENT = 5
|
||||
P3_CURRENT = 6
|
||||
P1_VOLTAGE = 7
|
||||
P2_VOLTAGE = 8
|
||||
P3_VOLTAGE = 9
|
||||
P1N_VOLTAGE = 10
|
||||
P2N_VOLTAGE = 11
|
||||
P3N_VOLTAGE = 12
|
||||
POWER_AC = 13
|
||||
FREQUENCY = 14
|
||||
POWER_APPARENT = 15
|
||||
POWER_REACTIVE = 16
|
||||
POWER_FACTOR = 17
|
||||
ENERGY_TOTAL = 18
|
||||
CURRENT_DC = 19
|
||||
VOLTAGE_DC = 20
|
||||
POWER_DC = 21
|
||||
TEMPERATURE = 22
|
||||
|
||||
#
|
||||
# The plugin is using a few tables to setup Domoticz and to process the feedback from the inverter.
|
||||
# The Column class is used to easily identify the columns in those tables.
|
||||
#
|
||||
|
||||
@unique
|
||||
class Column(IntEnum):
|
||||
|
||||
ID = 0
|
||||
NAME = 1
|
||||
TYPE = 2
|
||||
SUBTYPE = 3
|
||||
SWITCHTYPE = 4
|
||||
OPTIONS = 5
|
||||
MODBUSNAME = 6
|
||||
MODBUSSCALE = 7
|
||||
FORMAT = 8
|
||||
PREPEND = 9
|
||||
LOOKUP = 10
|
||||
MATH = 11
|
||||
|
||||
#
|
||||
# This table represents a single phase inverter.
|
||||
#
|
||||
|
||||
SINGLE_PHASE_INVERTER = [
|
||||
# ID, NAME, TYPE, SUBTYPE, SWITCHTYPE, OPTIONS, MODBUSNAME, MODBUSSCALE, FORMAT, PREPEND, LOOKUP, MATH
|
||||
[Unit.STATUS, "Status", 0xF3, 0x13, 0x00, {}, "status", None, "{}", None, solaredge_modbus.INVERTER_STATUS_MAP, None ],
|
||||
[Unit.VENDOR_STATUS, "Vendor Status", 0xF3, 0x13, 0x00, {}, "vendor_status", None, "{}", None, None, None ],
|
||||
[Unit.CURRENT, "Current", 0xF3, 0x17, 0x00, {}, "current", "current_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P1_CURRENT, "P1 Current", 0xF3, 0x17, 0x00, {}, "p1_current", "current_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P1_VOLTAGE, "P1 Voltage", 0xF3, 0x08, 0x00, {}, "p1_voltage", "voltage_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P1N_VOLTAGE, "P1-N Voltage", 0xF3, 0x08, 0x00, {}, "p1n_voltage", "voltage_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_AC, "Power", 0xF8, 0x01, 0x00, {}, "power_ac", "power_ac_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.FREQUENCY, "Frequency", 0xF3, 0x1F, 0x00, { "Custom": "1;Hz" }, "frequency", "frequency_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_APPARENT, "Power (Apparent)", 0xF3, 0x1F, 0x00, { "Custom": "1;VA" }, "power_apparent", "power_apparent_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_REACTIVE, "Power (Reactive)", 0xF3, 0x1F, 0x00, { "Custom": "1;VAr" }, "power_reactive", "power_reactive_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_FACTOR, "Power Factor", 0xF3, 0x06, 0x00, {}, "power_factor", "power_factor_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.ENERGY_TOTAL, "Total Energy", 0xF3, 0x1D, 0x04, {}, "energy_total", "energy_total_scale", "{};{}", Unit.POWER_AC, None, None ],
|
||||
[Unit.CURRENT_DC, "DC Current", 0xF3, 0x17, 0x00, {}, "current_dc", "current_dc_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.VOLTAGE_DC, "DC Voltage", 0xF3, 0x08, 0x00, {}, "voltage_dc", "voltage_dc_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_DC, "DC Power", 0xF8, 0x01, 0x00, {}, "power_dc", "power_dc_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.TEMPERATURE, "Temperature", 0xF3, 0x05, 0x00, {}, "temperature", "temperature_scale", "{:.2f}", None, None, Maximum() ]
|
||||
]
|
||||
|
||||
#
|
||||
# This table represents a three phase inverter.
|
||||
#
|
||||
|
||||
THREE_PHASE_INVERTER = [
|
||||
# ID, NAME, TYPE, SUBTYPE, SWITCHTYPE, OPTIONS, MODBUSNAME, MODBUSSCALE, FORMAT, PREPEND, LOOKUP, MATH
|
||||
[Unit.STATUS, "Status", 0xF3, 0x13, 0x00, {}, "status", None, "{}", None, solaredge_modbus.INVERTER_STATUS_MAP, None ],
|
||||
[Unit.VENDOR_STATUS, "Vendor Status", 0xF3, 0x13, 0x00, {}, "vendor_status", None, "{}", None, None, None ],
|
||||
[Unit.CURRENT, "Current", 0xF3, 0x17, 0x00, {}, "current", "current_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P1_CURRENT, "P1 Current", 0xF3, 0x17, 0x00, {}, "p1_current", "current_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P2_CURRENT, "P2 Current", 0xF3, 0x17, 0x00, {}, "p2_current", "current_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P3_CURRENT, "P3 Current", 0xF3, 0x17, 0x00, {}, "p3_current", "current_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P1_VOLTAGE, "P1 Voltage", 0xF3, 0x08, 0x00, {}, "p1_voltage", "voltage_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P2_VOLTAGE, "P2 Voltage", 0xF3, 0x08, 0x00, {}, "p2_voltage", "voltage_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P3_VOLTAGE, "P3 Voltage", 0xF3, 0x08, 0x00, {}, "p3_voltage", "voltage_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P1N_VOLTAGE, "P1-N Voltage", 0xF3, 0x08, 0x00, {}, "p1n_voltage", "voltage_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P2N_VOLTAGE, "P2-N Voltage", 0xF3, 0x08, 0x00, {}, "p2n_voltage", "voltage_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.P3N_VOLTAGE, "P3-N Voltage", 0xF3, 0x08, 0x00, {}, "p3n_voltage", "voltage_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_AC, "Power", 0xF8, 0x01, 0x00, {}, "power_ac", "power_ac_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.FREQUENCY, "Frequency", 0xF3, 0x1F, 0x00, { "Custom": "1;Hz" }, "frequency", "frequency_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_APPARENT, "Power (Apparent)", 0xF3, 0x1F, 0x00, { "Custom": "1;VA" }, "power_apparent", "power_apparent_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_REACTIVE, "Power (Reactive)", 0xF3, 0x1F, 0x00, { "Custom": "1;VAr" }, "power_reactive", "power_reactive_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_FACTOR, "Power Factor", 0xF3, 0x06, 0x00, {}, "power_factor", "power_factor_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.ENERGY_TOTAL, "Total Energy", 0xF3, 0x1D, 0x04, {}, "energy_total", "energy_total_scale", "{};{}", Unit.POWER_AC, None, None ],
|
||||
[Unit.CURRENT_DC, "DC Current", 0xF3, 0x17, 0x00, {}, "current_dc", "current_dc_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.VOLTAGE_DC, "DC Voltage", 0xF3, 0x08, 0x00, {}, "voltage_dc", "voltage_dc_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.POWER_DC, "DC Power", 0xF8, 0x01, 0x00, {}, "power_dc", "power_dc_scale", "{:.2f}", None, None, Average() ],
|
||||
[Unit.TEMPERATURE, "Temperature", 0xF3, 0x05, 0x00, {}, "temperature", "temperature_scale", "{:.2f}", None, None, Maximum() ]
|
||||
]
|
||||
|
||||
#
|
||||
# The BasePlugin is the actual Domoticz plugin.
|
||||
# This is where the fun starts :-)
|
||||
#
|
||||
from pyModbusTCP.client import ModbusClient
|
||||
from pymodbus.constants import Endian
|
||||
from pymodbus.payload import BinaryPayloadDecoder
|
||||
|
||||
class BasePlugin:
|
||||
|
||||
#enabled = False
|
||||
def __init__(self):
|
||||
|
||||
# The _LOOKUP_TABLE will point to one of the tables above, depending on the type of inverter.
|
||||
|
||||
self._LOOKUP_TABLE = None
|
||||
|
||||
# This is the solaredge_modbus Inverter object that will be used to communicate with the inverter.
|
||||
|
||||
self.inverter = None
|
||||
|
||||
# Default heartbeat is 10 seconds; therefore 30 samples in 5 minutes.
|
||||
|
||||
self.max_samples = 30
|
||||
|
||||
# Whether the plugin should add missing devices.
|
||||
# If set to True, a deleted device will be added on the next restart of Domoticz.
|
||||
|
||||
self.add_devices = False
|
||||
|
||||
# When there is an issue contacting the inverter, the plugin will retry after a certain retry delay.
|
||||
# The actual time after which the plugin will try again is stored in the retry after variable.
|
||||
# According to the documenation, the inverter may need up to 2 minutes to "reset".
|
||||
|
||||
self.retrydelay = timedelta(minutes = 2)
|
||||
self.retryafter = datetime.now() - timedelta(seconds = 1)
|
||||
|
||||
#
|
||||
# onStart is called by Domoticz to start the processing of the plugin.
|
||||
#
|
||||
return
|
||||
|
||||
def onStart(self):
|
||||
try:
|
||||
Domoticz.Log("DS238-2 ZN/S Energy Meter TCP loaded!, using python v" + sys.version[:6] + " and pymodbus v" + pymodbus.__version__)
|
||||
except:
|
||||
Domoticz.Log("DS238-2 ZN/S Energy Meter TCP loaded!")
|
||||
|
||||
self.add_devices = bool(Parameters["Mode1"])
|
||||
# Check dependancies
|
||||
try:
|
||||
if (float(Parameters["DomoticzVersion"][:6]) < float("2020.2")): Domoticz.Error("WARNING: Domoticz version is outdated or not supported. Please update!")
|
||||
if (float(sys.version[:1]) < 3): Domoticz.Error("WARNING: Python3 should be used !")
|
||||
if (float(pymodbus.__version__[:3]) < float("2.3")): Domoticz.Error("WARNING: pymodbus version is outdated, please update!")
|
||||
except:
|
||||
Domoticz.Error("Warning ! Dependancies could not be checked !")
|
||||
|
||||
# Domoticz will generate graphs showing an interval of 5 minutes.
|
||||
# Calculate the number of samples to store over a period of 5 minutes.
|
||||
|
||||
self.max_samples = 300 / int(Parameters["Mode2"])
|
||||
|
||||
# Now set the interval at which the information is collected accordingly.
|
||||
|
||||
Domoticz.Heartbeat(int(Parameters["Mode2"]))
|
||||
Domoticz.Log("Started !")
|
||||
# Parse parameters
|
||||
|
||||
# Debug
|
||||
if Parameters["Mode6"] == "Debug":
|
||||
Domoticz.Debugging(1)
|
||||
else:
|
||||
Domoticz.Debugging(0)
|
||||
|
||||
Domoticz.Debug(
|
||||
"onStart Address: {} Port: {}".format(
|
||||
Parameters["Address"],
|
||||
Parameters["Port"]
|
||||
)
|
||||
)
|
||||
self.IPAddress = Parameters["Address"]
|
||||
self.IPPort = Parameters["Port"]
|
||||
if Parameters["Mode1"] == "1":
|
||||
self.AddMissing = 1
|
||||
else:
|
||||
self.AddMissing = 0
|
||||
|
||||
self.inverter = solaredge_modbus.Inverter(
|
||||
host=Parameters["Address"],
|
||||
port=Parameters["Port"],
|
||||
timeout=3,
|
||||
unit=1
|
||||
)
|
||||
self.MBAddr = int(Parameters["Mode3"])
|
||||
|
||||
# Lets get in touch with the inverter.
|
||||
Domoticz.Debug("Query IP " + self.IPAddress + ":" + str(self.IPPort) +" on device : "+str(self.MBAddr))
|
||||
|
||||
self.contactInverter()
|
||||
# Create the Domoticz.Devices() here.
|
||||
|
||||
|
||||
#
|
||||
# OnHeartbeat is called by Domoticz at a specific interval as set in onStart()
|
||||
#
|
||||
def onStop(self):
|
||||
Domoticz.Log("onStop called")
|
||||
|
||||
def onConnect(self, Connection, Status, Description):
|
||||
Domoticz.Log("onConnect called")
|
||||
|
||||
def onMessage(self, Connection, Data):
|
||||
Domoticz.Log("onMessage called")
|
||||
|
||||
def onCommand(self, Unit, Command, Level, Hue):
|
||||
Domoticz.Log("onCommand called for Unit " + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level))
|
||||
|
||||
def onNotification(self, Name, Subject, Text, Status, Priority, Sound, ImageFile):
|
||||
Domoticz.Log("Notification: " + Name + "," + Subject + "," + Text + "," + Status + "," + str(Priority) + "," + Sound + "," + ImageFile)
|
||||
|
||||
def onDisconnect(self, Connection):
|
||||
Domoticz.Log("onDisconnect called")
|
||||
|
||||
def onHeartbeat(self):
|
||||
Domoticz.Debug("onHeartbeat")
|
||||
|
||||
# We need to make sure that we have a table to work with.
|
||||
# This will be set by contactInverter and will be None till it is clear
|
||||
# that the inverter responds and that a matching table is available.
|
||||
|
||||
if self._LOOKUP_TABLE:
|
||||
|
||||
inverter_values = None
|
||||
Domoticz.Log("onHeartbeat called")
|
||||
Domoticz.Debug(" Interface : IP="+self.IPAddress +", Port="+str(self.IPPort)+" ID="+str(self.MBAddr))
|
||||
try:
|
||||
inverter_values = self.inverter.read_all()
|
||||
except ConnectionException:
|
||||
inverter_values = None
|
||||
Domoticz.Debug("ConnectionException")
|
||||
else:
|
||||
|
||||
if inverter_values:
|
||||
|
||||
if "Mode5" in Parameters and Parameters["Mode5"] == "Extra":
|
||||
to_log = inverter_values
|
||||
if "c_serialnumber" in to_log:
|
||||
to_log.pop("c_serialnumber")
|
||||
Domoticz.Log("inverter values: {}".format(json.dumps(to_log, indent=4, sort_keys=False)))
|
||||
|
||||
# Just for cosmetics in the log
|
||||
|
||||
updated = 0
|
||||
device_count = 0
|
||||
|
||||
# Now process each unit in the table.
|
||||
|
||||
for unit in self._LOOKUP_TABLE:
|
||||
Domoticz.Debug(str(unit))
|
||||
|
||||
# Skip a unit when the matching device got deleted.
|
||||
|
||||
if unit[Column.ID] in Devices:
|
||||
Domoticz.Debug("-> found in Devices")
|
||||
|
||||
# For certain units the table has a lookup table to replace the value with something else.
|
||||
|
||||
if unit[Column.LOOKUP]:
|
||||
Domoticz.Debug("-> looking up...")
|
||||
|
||||
lookup_table = unit[Column.LOOKUP]
|
||||
to_lookup = int(inverter_values[unit[Column.MODBUSNAME]])
|
||||
|
||||
if to_lookup >= 0 and to_lookup < len(lookup_table):
|
||||
value = lookup_table[to_lookup]
|
||||
else:
|
||||
value = "Key not found in lookup table: {}".format(to_lookup)
|
||||
|
||||
# When a math object is setup for the unit, update the samples in it and get the calculated value.
|
||||
|
||||
elif unit[Column.MATH]:
|
||||
Domoticz.Debug("-> calculating...")
|
||||
m = unit[Column.MATH]
|
||||
if unit[Column.MODBUSSCALE]:
|
||||
m.update(inverter_values[unit[Column.MODBUSNAME]], inverter_values[unit[Column.MODBUSSCALE]])
|
||||
else:
|
||||
m.update(inverter_values[unit[Column.MODBUSNAME]])
|
||||
|
||||
value = m.get()
|
||||
|
||||
# When there is no math object then just store the latest value.
|
||||
# Some values from the inverter need to be scaled before they can be stored.
|
||||
|
||||
elif unit[Column.MODBUSSCALE]:
|
||||
Domoticz.Debug("-> calculating...")
|
||||
# we need to do some calculation here
|
||||
value = inverter_values[unit[Column.MODBUSNAME]] * (10 ** inverter_values[unit[Column.MODBUSSCALE]])
|
||||
|
||||
# Some values require no action but storing in Domoticz.
|
||||
|
||||
else:
|
||||
Domoticz.Debug("-> copying...")
|
||||
value = inverter_values[unit[Column.MODBUSNAME]]
|
||||
|
||||
Domoticz.Debug("value = {}".format(value))
|
||||
|
||||
# Time to store the value in Domoticz.
|
||||
# Some devices require multiple values, in which case the plugin will combine those values.
|
||||
# Currently, there is only a need to prepend one value with another.
|
||||
|
||||
if unit[Column.PREPEND]:
|
||||
Domoticz.Debug("-> has prepend")
|
||||
prepend = Devices[unit[Column.PREPEND]].sValue
|
||||
Domoticz.Debug("prepend = {}".format(prepend))
|
||||
sValue = unit[Column.FORMAT].format(prepend, value)
|
||||
else:
|
||||
Domoticz.Debug("-> no prepend")
|
||||
sValue = unit[Column.FORMAT].format(value)
|
||||
|
||||
Domoticz.Debug("sValue = {}".format(sValue))
|
||||
|
||||
# Only store the value in Domoticz when it has changed.
|
||||
# TODO:
|
||||
# We should not store certain values when the inverter is sleeping.
|
||||
# That results in a strange graph; it would be better just to skip it then.
|
||||
|
||||
if sValue != Devices[unit[Column.ID]].sValue:
|
||||
Devices[unit[Column.ID]].Update(nValue=0, sValue=str(sValue), TimedOut=0)
|
||||
updated += 1
|
||||
|
||||
device_count += 1
|
||||
|
||||
else:
|
||||
Domoticz.Debug("-> NOT found in Devices")
|
||||
|
||||
Domoticz.Log("Updated {} values out of {}".format(updated, device_count))
|
||||
else:
|
||||
Domoticz.Log("Inverter returned no information")
|
||||
|
||||
# Try to contact the inverter when the lookup table is not yet initialized.
|
||||
|
||||
else:
|
||||
self.contactInverter()
|
||||
|
||||
|
||||
#
|
||||
# Contact the inverter and find out what type it is.
|
||||
# Initialize the lookup table when the type is supported.
|
||||
#
|
||||
|
||||
def contactInverter(self):
|
||||
|
||||
# Do not stress the inverter when it did not respond in the previous attempt to contact it.
|
||||
|
||||
if self.retryafter <= datetime.now():
|
||||
|
||||
# Here we go...
|
||||
inverter_values = None
|
||||
try:
|
||||
inverter_values = self.inverter.read_all()
|
||||
except ConnectionException:
|
||||
|
||||
# There are multiple reasons why this may fail.
|
||||
# - Perhaps the ip address or port are incorrect.
|
||||
# - The inverter may not be connected to the networ,
|
||||
# - The inverter may be turned off.
|
||||
# - The inverter has a bad hairday....
|
||||
# Try again in the future.
|
||||
|
||||
self.retryafter = datetime.now() + self.retrydelay
|
||||
inverter_values = None
|
||||
|
||||
Domoticz.Log("Connection Exception when trying to contact: {}:{}".format(Parameters["Address"], Parameters["Port"]))
|
||||
Domoticz.Log("Retrying to communicate with inverter after: {}".format(self.retryafter))
|
||||
|
||||
else:
|
||||
|
||||
if inverter_values:
|
||||
Domoticz.Log("Connection established with: {}:{}".format(Parameters["Address"], Parameters["Port"]))
|
||||
|
||||
inverter_type = solaredge_modbus.sunspecDID(inverter_values["c_sunspec_did"])
|
||||
Domoticz.Log("Inverter type: {}".format(inverter_type))
|
||||
|
||||
# The plugin currently has 2 supported types.
|
||||
# This may be updated in the future based on user feedback.
|
||||
|
||||
if inverter_type == solaredge_modbus.sunspecDID.SINGLE_PHASE_INVERTER:
|
||||
self._LOOKUP_TABLE = SINGLE_PHASE_INVERTER
|
||||
elif inverter_type == solaredge_modbus.sunspecDID.THREE_PHASE_INVERTER:
|
||||
self._LOOKUP_TABLE = THREE_PHASE_INVERTER
|
||||
else:
|
||||
Domoticz.Log("Unsupported inverter type: {}".format(inverter_type))
|
||||
|
||||
if self._LOOKUP_TABLE:
|
||||
|
||||
# Set the number of samples on all the math objects.
|
||||
|
||||
for unit in self._LOOKUP_TABLE:
|
||||
if unit[Column.MATH]:
|
||||
unit[Column.MATH].set_max_samples(self.max_samples)
|
||||
|
||||
|
||||
# We updated some device types over time.
|
||||
# Let's make sure that we have the correct type setup.
|
||||
|
||||
for unit in self._LOOKUP_TABLE:
|
||||
if unit[Column.ID] in Devices:
|
||||
device = Devices[unit[Column.ID]]
|
||||
|
||||
if (device.Type != unit[Column.TYPE] or
|
||||
device.SubType != unit[Column.SUBTYPE] or
|
||||
device.SwitchType != unit[Column.SWITCHTYPE] or
|
||||
device.Options != unit[Column.OPTIONS]):
|
||||
|
||||
Domoticz.Log("Updating device \"{}\"".format(device.Name))
|
||||
|
||||
nValue = device.nValue
|
||||
sValue = device.sValue
|
||||
|
||||
device.Update(
|
||||
Type=unit[Column.TYPE],
|
||||
Subtype=unit[Column.SUBTYPE],
|
||||
Switchtype=unit[Column.SWITCHTYPE],
|
||||
Options=unit[Column.OPTIONS],
|
||||
nValue=nValue,
|
||||
sValue=sValue
|
||||
)
|
||||
|
||||
# Add missing devices if needed.
|
||||
|
||||
if self.add_devices:
|
||||
for unit in self._LOOKUP_TABLE:
|
||||
if unit[Column.ID] not in Devices:
|
||||
Domoticz.Device(
|
||||
Unit=unit[Column.ID],
|
||||
Name=unit[Column.NAME],
|
||||
Type=unit[Column.TYPE],
|
||||
Subtype=unit[Column.SUBTYPE],
|
||||
Switchtype=unit[Column.SWITCHTYPE],
|
||||
Options=unit[Column.OPTIONS],
|
||||
Used=1,
|
||||
).Create()
|
||||
else:
|
||||
Domoticz.Log("Connection established with: {}:{}. BUT... inverter returned no information".format(Parameters["Address"], Parameters["Port"]))
|
||||
Domoticz.Log("Retrying to communicate with inverter after: {}".format(self.retryafter))
|
||||
else:
|
||||
Domoticz.Log("Retrying to communicate with inverter after: {}".format(self.retryafter))
|
||||
|
||||
|
||||
#
|
||||
# Instantiate the plugin and register the supported callbacks.
|
||||
# Currently that is only onStart() and onHeartbeat()
|
||||
#
|
||||
client = ModbusClient(host=self.IPAddress, port=self.IPPort, unit_id=self.MBAddr, auto_open=True, auto_close=True, timeout=2)
|
||||
except:
|
||||
Domoticz.Error("Error connecting to TCP/Interface on address : "+self.IPaddress+":"+str(self.IPPort))
|
||||
# TODO: add for each devices
|
||||
#Devices[1].Update(1, "0") # Set value to 0 (error)
|
||||
|
||||
global _plugin
|
||||
_plugin = BasePlugin()
|
||||
@ -538,6 +120,45 @@ def onStart():
|
||||
global _plugin
|
||||
_plugin.onStart()
|
||||
|
||||
def onStop():
|
||||
global _plugin
|
||||
_plugin.onStop()
|
||||
|
||||
def onConnect(Connection, Status, Description):
|
||||
global _plugin
|
||||
_plugin.onConnect(Connection, Status, Description)
|
||||
|
||||
def onMessage(Connection, Data):
|
||||
global _plugin
|
||||
_plugin.onMessage(Connection, Data)
|
||||
|
||||
def onCommand(Unit, Command, Level, Hue):
|
||||
global _plugin
|
||||
_plugin.onCommand(Unit, Command, Level, Hue)
|
||||
|
||||
def onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile):
|
||||
global _plugin
|
||||
_plugin.onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile)
|
||||
|
||||
def onDisconnect(Connection):
|
||||
global _plugin
|
||||
_plugin.onDisconnect(Connection)
|
||||
|
||||
def onHeartbeat():
|
||||
global _plugin
|
||||
_plugin.onHeartbeat()
|
||||
|
||||
# Generic helper functions
|
||||
def DumpConfigToLog():
|
||||
for x in Parameters:
|
||||
if Parameters[x] != "":
|
||||
Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'")
|
||||
Domoticz.Debug("Device count: " + str(len(Devices)))
|
||||
for x in Devices:
|
||||
Domoticz.Debug("Device: " + str(x) + " - " + str(Devices[x]))
|
||||
Domoticz.Debug("Device ID: '" + str(Devices[x].ID) + "'")
|
||||
Domoticz.Debug("Device Name: '" + Devices[x].Name + "'")
|
||||
Domoticz.Debug("Device nValue: " + str(Devices[x].nValue))
|
||||
Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'")
|
||||
Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user