forked from pymodbus-dev/pymodbus
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Changing @staticmethod to @classmethod to fix inheritance
- Loading branch information
Showing
3 changed files
with
247 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,235 @@ | ||
from pymodbus.constants import Endian | ||
from pymodbus.client.sync import ModbusTcpClient | ||
from pymodbus.payload import BinaryPayloadDecoder | ||
from twisted.internet.defer import Deferred | ||
|
||
|
||
#---------------------------------------------------------------------------# | ||
# Sunspec Constants | ||
#---------------------------------------------------------------------------# | ||
class SunspecDefaultValue(object): | ||
''' A collection of constants to indicate if | ||
a value is not implemented. | ||
''' | ||
Signed16 = 0x8000 | ||
Unsigned16 = 0xffff | ||
Accumulator16 = 0x0000 | ||
Scale = 0x8000 | ||
Signed32 = 0x80000000 | ||
Float32 = 0x7fc00000 | ||
Unsigned32 = 0xffffffff | ||
Accumulator32 = 0x00000000 | ||
Signed64 = 0x8000000000000000 | ||
Unsigned64 = 0xffffffffffffffff | ||
Accumulator64 = 0x0000000000000000 | ||
String = 0x0000 | ||
|
||
|
||
class SunspecStatus(object): | ||
''' Indicators of the current status of a | ||
sunspec device | ||
''' | ||
Normal = 0x00000000 | ||
Error = 0xfffffffe | ||
Unknown = 0xffffffff | ||
|
||
|
||
class SunspecIdentifier(object): | ||
''' Assigned identifiers that are pre-assigned | ||
by the sunspec protocol. | ||
''' | ||
Sunspec = 0x53756e53 | ||
|
||
|
||
class SunspecModel(object): | ||
''' Assigned device indentifiers that are pre-assigned | ||
by the sunspec protocol. | ||
''' | ||
# 0xx Common Models | ||
CommonBlock = 1 | ||
AggregatorBlock = 2 | ||
|
||
# 1xx Inverter Models | ||
SinglePhaseIntegerInverter = 101 | ||
SplitPhaseIntegerInverter = 102 | ||
ThreePhaseIntegerInverter = 103 | ||
SinglePhaseFloatsInverter = 103 | ||
SplitPhaseFloatsInverter = 102 | ||
ThreePhaseFloatsInverter = 103 | ||
|
||
# 2xx Meter Models | ||
SinglePhaseMeter = 201 | ||
SplitPhaseMeter = 201 | ||
WyeConnectMeter = 201 | ||
DeltaConnectMeter = 201 | ||
|
||
# 3xx Environmental Models | ||
BaseMeteorological = 301 | ||
Irradiance = 302 | ||
BackOfModuleTemperature = 303 | ||
Inclinometer = 304 | ||
Location = 305 | ||
ReferencePoint = 306 | ||
BaseMeteorological = 307 | ||
MiniMeteorological = 308 | ||
|
||
# 4xx String Combiner Models | ||
BasicStringCombiner = 401 | ||
AdvancedStringCombiner = 402 | ||
|
||
# 5xx Panel Models | ||
PanelFloat = 501 | ||
PanelInteger = 502 | ||
|
||
# 64xxx Vender Extension Block | ||
EndOfSunSpecMap = 65535 | ||
|
||
@classmethod | ||
def lookup(klass, code): | ||
''' Given a device identifier, return the | ||
device model name for that identifier | ||
:param code: The device code to lookup | ||
:returns: The device model name, or None if none available | ||
''' | ||
values = dict((v, k) for k, v in klass.__dict__.items() | ||
if not callable(v)) | ||
return values.get(code, None) | ||
|
||
|
||
class SunspecOffsets(object): | ||
''' Well known offsets that are used throughout | ||
the sunspec protocol | ||
''' | ||
CommonBlock = 40000 | ||
AlternateCommonBlock = 50000 | ||
|
||
|
||
class SunspecDecoder(BinaryPayloadDecoder): | ||
''' A decoder that deals correctly with the sunspec | ||
binary format. | ||
''' | ||
|
||
def __init__(self, payload, endian): | ||
''' Initialize a new instance of the SunspecDecoder | ||
.. note:: This is always set to big endian byte order | ||
as specified in the protocol. | ||
''' | ||
endian = Endian.Big | ||
BinaryPayloadDecoder.__init__(self, payload, endian) | ||
|
||
def decode_string(self, size=1): | ||
''' Decodes a string from the buffer | ||
:param size: The size of the string to decode | ||
''' | ||
self._pointer = size | ||
string = self._payload[self._pointer - size:self._pointer] | ||
return string.split('\x00')[0] | ||
|
||
|
||
#---------------------------------------------------------------------------# | ||
# Common Functions | ||
#---------------------------------------------------------------------------# | ||
def defer_or_apply(func): | ||
''' Decorator to apply an adapter method | ||
to a result regardless if it is a deferred | ||
or a concrete response. | ||
:param func: The function to decorate | ||
''' | ||
def closure(future, adapt): | ||
if isinstance(defer, Deferred): | ||
d = Deferred() | ||
future.addCallback(lambda r: d.callback(adapt(r))) | ||
return d | ||
return adapt(future) | ||
return closure | ||
|
||
|
||
def create_sunspec_client(host): | ||
''' A quick helper method to create a sunspec | ||
client. | ||
:param host: The host to connect to | ||
:returns: an initialized SunspecClient | ||
''' | ||
modbus = ModbusTcpClient(host) | ||
modbus.connect() | ||
client = SunspecClient(modbus) | ||
client.initialize() | ||
return client | ||
|
||
|
||
#---------------------------------------------------------------------------# | ||
# Sunspec Client | ||
#---------------------------------------------------------------------------# | ||
class SunspecClient(object): | ||
|
||
def __init__(self, client): | ||
''' Initialize a new instance of the client | ||
:param client: The modbus client to use | ||
''' | ||
self.client = client | ||
self.offset = SunspecOffsets.CommonBlock | ||
|
||
def initialize(self): | ||
''' Initialize the underlying client values | ||
:returns: True if successful, false otherwise | ||
''' | ||
decoder = self.get_device_block(self.offset, 2) | ||
if decoder.decode_32bit_uint() == SunspecIdentifier.Sunspec: | ||
return True | ||
self.offset = SunspecOffsets.AlternateCommonBlock | ||
decoder = self.get_device_block(self.offset, 2) | ||
return decoder.decode_32bit_uint() == SunspecIdentifier.Sunspec | ||
|
||
def get_common_block(self): | ||
''' Read and return the sunspec common information | ||
block. | ||
:returns: A dictionary of the common block information | ||
''' | ||
decoder = self.get_device_block(self.offset, 69) | ||
return { | ||
'SunSpec_ID': decoder.decode_32bit_uint(), | ||
'SunSpec_DID': decoder.decode_16bit_uint(), | ||
'SunSpec_Length': decoder.decode_16bit_uint(), | ||
'Manufacturer': decoder.decode_string(size=32), | ||
'Model': decoder.decode_string(size=32), | ||
'Options': decoder.decode_string(size=16), | ||
'Version': decoder.decode_string(size=16), | ||
'SerialNumber': decoder.decode_string(size=32), | ||
'DeviceAddress': decoder.decode_16bit_uint(), | ||
'Next_DID': decoder.decode_16bit_uint(), | ||
'Next_DID_Length': decoder.decode_16bit_uint(), | ||
} | ||
|
||
def get_device_block(self, offset, size): | ||
''' A helper method to retrieve the next device block | ||
.. note:: We will read 2 more registers so that we have | ||
the information for the next block. | ||
:param offset: The offset to start reading at | ||
:param size: The size of the offset to read | ||
:returns: An initialized decoder for that result | ||
''' | ||
response = self.client.read_holding_registers(offset, size 2) | ||
return SunspecDecoder.fromRegisters(response.registers) | ||
|
||
#------------------------------------------------------------ | ||
# A quick test runner | ||
#------------------------------------------------------------ | ||
if __name__ == "__main__": | ||
client = create_sunspec_client("YOUR.HOST.GOES.HERE") | ||
common = client.get_common_block() | ||
for key, value in common.items(): | ||
if key == "SunSpec_DID": | ||
value = SunspecModel.lookup(value) | ||
print "{:<20}: {}".format(key, value) | ||
client.client.close() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters