1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
|
from pymodbus.constants import Endian
from pymodbus.client.sync import ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder
from twisted.internet.defer import Deferred
# --------------------------------------------------------------------------- #
# Logging
# --------------------------------------------------------------------------- #
import logging
_logger = logging.getLogger(__name__)
_logger.setLevel(logging.DEBUG)
logging.basicConfig()
# --------------------------------------------------------------------------- #
# Sunspec Common 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 = '\x00'
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
#---------------------------------------------
# 641xx Outback Blocks
#---------------------------------------------
OutbackDeviceIdentifier = 64110
OutbackChargeController = 64111
OutbackFMSeriesChargeController = 64112
OutbackFXInverterRealTime = 64113
OutbackFXInverterConfiguration = 64114
OutbackSplitPhaseRadianInverter = 64115
OutbackRadianInverterConfiguration = 64116
OutbackSinglePhaseRadianInverterRealTime = 64117
OutbackFLEXNetDCRealTime = 64118
OutbackFLEXNetDCConfiguration = 64119
OutbackSystemControl = 64120
#---------------------------------------------
# 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__.iteritems()
if not callable(v))
return values.get(code, None)
class SunspecOffsets(object):
""" Well known offsets that are used throughout
the sunspec protocol
"""
CommonBlock = 40000
CommonBlockLength = 69
AlternateCommonBlock = 50000
# --------------------------------------------------------------------------- #
# 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(future, Deferred):
d = Deferred()
future.addCallback(lambda r: d.callback(adapt(r)))
return d
return adapt(future)
return closure
def create_sunspec_sync_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 SunspecDecoder(BinaryPayloadDecoder):
""" A decoder that deals correctly with the sunspec
binary format.
"""
def __init__(self, payload, byteorder):
""" Initialize a new instance of the SunspecDecoder
.. note:: This is always set to big endian byte order
as specified in the protocol.
"""
byteorder = Endian.Big
BinaryPayloadDecoder.__init__(self, payload, byteorder)
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(SunspecDefaultValue.String)[0]
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
"""
length = SunspecOffsets.CommonBlockLength
decoder = self.get_device_block(self.offset, length)
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
"""
_logger.debug("reading device block[{}..{}]".format(offset, offset + size))
response = self.client.read_holding_registers(offset, size + 2)
return SunspecDecoder.fromRegisters(response.registers)
def get_all_device_blocks(self):
""" Retrieve all the available blocks in the supplied
sunspec device.
.. note:: Since we do not know how to decode the available
blocks, this returns a list of dictionaries of the form:
decoder: the-binary-decoder,
model: the-model-identifier (name)
:returns: A list of the available blocks
"""
blocks = []
offset = self.offset + 2
model = SunspecModel.CommonBlock
while model != SunspecModel.EndOfSunSpecMap:
decoder = self.get_device_block(offset, 2)
model = decoder.decode_16bit_uint()
length = decoder.decode_16bit_uint()
blocks.append({
'model' : model,
'name' : SunspecModel.lookup(model),
'length': length,
'offset': offset + length + 2
})
offset += length + 2
return blocks
#------------------------------------------------------------
# A quick test runner
#------------------------------------------------------------
if __name__ == "__main__":
client = create_sunspec_sync_client("YOUR.HOST.GOES.HERE")
# print out all the device common block
common = client.get_common_block()
for key, value in common.iteritems():
if key == "SunSpec_DID":
value = SunspecModel.lookup(value)
print("{:<20}: {}".format(key, value))
# print out all the available device blocks
blocks = client.get_all_device_blocks()
for block in blocks:
print(block)
client.client.close()
|