Skip to content

Commit

Permalink
Adding sunspec client example
Browse files Browse the repository at this point in the history
* Changing @staticmethod to @classmethod to fix inheritance
  • Loading branch information
bashwork committed Feb 3, 2013
1 parent 075aaf3 commit 5a5d5ae
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 12 deletions.
235 changes: 235 additions & 0 deletions examples/contrib/sunspec-client.py
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()

12 changes: 6 additions & 6 deletions pymodbus/datastore/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 144,14 @@ def __init__(self, address, values):
else: self.values = [values]
self.default_value = self.values[0].__class__()

@staticmethod
def create():
@classmethod
def create(klass):
''' Factory method to create a datastore with the
full address space initialized to 0x00
:returns: An initialized datastore
'''
return ModbusSequentialDataBlock(0x00, [0x00] * 65536)
return klass(0x00, [0x00] * 65536)

def validate(self, address, count=1):
''' Checks to see if the request is in range
Expand Down Expand Up @@ -206,14 206,14 @@ def __init__(self, values):
self.default_value = self.values.values()[0].__class__()
self.address = self.values.iterkeys().next()

@staticmethod
def create():
@classmethod
def create(klass):
''' Factory method to create a datastore with the
full address space initialized to 0x00
:returns: An initialized datastore
'''
return ModbusSparseDataBlock([0x00]*65536)
return klass([0x00]*65536)

def validate(self, address, count=1):
''' Checks to see if the request is in range
Expand Down
12 changes: 6 additions & 6 deletions pymodbus/payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 185,8 @@ def __init__(self, payload, endian=Endian.Little):
self._pointer = 0x00
self._endian = endian

@staticmethod
def fromRegisters(registers, endian=Endian.Little):
@classmethod
def fromRegisters(klass, registers, endian=Endian.Little):
''' Initialize a payload decoder with the result of
reading a collection of registers from a modbus device.
Expand All @@ -200,11 200,11 @@ def fromRegisters(registers, endian=Endian.Little):
'''
if isinstance(registers, list): # repack into flat binary
payload = ''.join(pack('>H', x) for x in registers)
return BinaryPayloadDecoder(payload, endian)
return klass(payload, endian)
raise ParameterException('Invalid collection of registers supplied')

@staticmethod
def fromCoils(coils, endian=Endian.Little):
@classmethod
def fromCoils(klass, coils, endian=Endian.Little):
''' Initialize a payload decoder with the result of
reading a collection of coils from a modbus device.
Expand All @@ -216,7 216,7 @@ def fromCoils(coils, endian=Endian.Little):
'''
if isinstance(coils, list):
payload = pack_bitstring(coils)
return BinaryPayloadDecoder(payload, endian)
return klass(payload, endian)
raise ParameterException('Invalid collection of coils supplied')

def reset(self):
Expand Down

0 comments on commit 5a5d5ae

Please sign in to comment.