Skip to content
This repository has been archived by the owner on Jun 25, 2019. It is now read-only.

Implement Saia Burgess ALE3 #54

Merged
merged 4 commits into from
Sep 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ manuals for yourself, I could be wrong):
| SDM630 v2 | 3 | + | + | + | + | + | + | + | + |
| Janitza B23-312 | 3 | + | + | + | + | + | + | - | - |
| DZG DVH4013 | 3 | + | + | - | - | + | + | - | - |
| SBC ALE3 | 3 | + | + | + | + | + | + | - | - |

Please note that voltage, current, power and power factor are always
reported for each connected phase.
Expand All @@ -50,6 +51,9 @@ reported for each connected phase.
serial number (top right of the device), e.g. 23, and add one (24).
Assume this is a hexadecimal number and convert it to decimal (36). Use
this as the meter ID.
* SBC ALE3: This compact Saia Burgess Controls meter is comparable to the SDM630:
two tariffs, both import and export depending on meter version and compact (4TE).
It's often used with Viessmann heat pumps.

Some of my test devices have been provided by [B+G
E-Tech](http://bg-etech.de/) - please consider to buy your meter from
Expand Down
5 changes: 3 additions & 2 deletions cmd/sdm630/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ func main() {
Usage: `MODBUS device type and ID to query, separated by comma.
Valid types are:
"SDM" for Eastron SDM meters
"JANITZA" for Janitza B-Series DIN-Rail meters
"DZG" for the DZG Metering GmbH DVH4013 DIN-Rail meter
"JANITZA" for Janitza B-Series meters
"DZG" for the DZG Metering GmbH DVH4013 meters
"SBC" for the Saia Burgess Controls ALE3 meters
Example: -d JANITZA:1,SDM:22,DZG:23`,
},
cli.StringFlag{
Expand Down
5 changes: 3 additions & 2 deletions cmd/sdm630_httpd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ func main() {
Usage: `MODBUS device type and ID to query, separated by comma.
Valid types are:
"SDM" for Eastron SDM meters
"JANITZA" for Janitza B-Series DIN-Rail meters
"DZG" for the DZG Metering GmbH DVH4013 DIN-Rail meter
"JANITZA" for Janitza B-Series meters
"DZG" for the DZG Metering GmbH DVH4013 meters
"SBC" for the Saia Burgess Controls ALE3 meters
Example: -d JANITZA:1,SDM:22,DZG:23`,
},
cli.StringFlag{
Expand Down
3 changes: 1 addition & 2 deletions datagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,6 @@ type QuerySnip struct {
ReadLen uint16 `json:"-"`
Value float64
IEC61850 string
Description string
ReadTimestamp time.Time
Transform RTUTransform `json:"-"`
}
Expand All @@ -280,7 +279,7 @@ func (q *QuerySnip) MarshalJSON() ([]byte, error) {
DeviceId: q.DeviceId,
Value: q.Value,
IEC61850: q.IEC61850,
Description: q.Description,
Description: GetIecDescription(q.IEC61850),
Timestamp: q.ReadTimestamp.UnixNano() / 1e6,
})
}
Expand Down
19 changes: 11 additions & 8 deletions dzg.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,24 @@ func NewDZGProducer() *DZGProducer {
return &DZGProducer{}
}

func (p *DZGProducer) GetMeterType() string {
return METERTYPE_DZG
}

func (p *DZGProducer) snip(devid uint8, opcode uint16, iec string, scaler ...float64) QuerySnip {
transform := RTU32ToFloat64 // default conversion
if len(scaler) > 0 {
transform = MakeRTU32ScaledIntToFloat64(scaler[0])
}

snip := QuerySnip{
DeviceId: devid,
FuncCode: ReadHoldingReg,
OpCode: opcode,
ReadLen: 2,
Value: math.NaN(),
IEC61850: iec,
Description: GetIecDescription(iec),
Transform: transform,
DeviceId: devid,
FuncCode: ReadHoldingReg,
OpCode: opcode,
ReadLen: 2,
Value: math.NaN(),
IEC61850: iec,
Transform: transform,
}
return snip
}
Expand Down
19 changes: 11 additions & 8 deletions janitza.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,19 @@ func NewJanitzaProducer() *JanitzaProducer {
return &JanitzaProducer{}
}

func (p *JanitzaProducer) GetMeterType() string {
return METERTYPE_JANITZA
}

func (p *JanitzaProducer) snip(devid uint8, opcode uint16, iec string) QuerySnip {
snip := QuerySnip{
DeviceId: devid,
FuncCode: ReadHoldingReg,
OpCode: opcode,
ReadLen: 2,
Value: math.NaN(),
IEC61850: iec,
Description: GetIecDescription(iec),
Transform: RTU32ToFloat64,
DeviceId: devid,
FuncCode: ReadHoldingReg,
OpCode: opcode,
ReadLen: 2,
Value: math.NaN(),
IEC61850: iec,
Transform: RTU32ToFloat64,
}
return snip
}
Expand Down
16 changes: 7 additions & 9 deletions meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"time"
)

type MeterType string
type MeterState uint8

const (
Expand All @@ -17,7 +16,6 @@ const (
)

type Meter struct {
Type MeterType
DeviceId uint8
Producer Producer
MeterReadings *MeterReadings
Expand All @@ -28,6 +26,7 @@ type Meter struct {
// Producer is the interface that produces query snips which represent
// modbus operations
type Producer interface {
GetMeterType() string
Produce(devid uint8) []QuerySnip
Probe(devid uint8) QuerySnip
}
Expand All @@ -50,22 +49,25 @@ func NewMeterByType(
measurements as the other meters. Only limited functionality is
implemented.`)
p = NewDZGProducer()
case METERTYPE_SBC:
log.Println(`WARNING: The SBC ALE3 does not report the same
measurements as the other meters. Only limited functionality is
implemented.`)
p = NewSBCProducer()
default:
return nil, fmt.Errorf("Unknown meter type %s", typeid)
}

return NewMeter(MeterType(typeid), devid, p, timeToCacheReadings), nil
return NewMeter(devid, p, timeToCacheReadings), nil
}

func NewMeter(
typeid MeterType,
devid uint8,
producer Producer,
timeToCacheReadings time.Duration,
) *Meter {
r := NewMeterReadings(devid, timeToCacheReadings)
return &Meter{
Type: typeid,
Producer: producer,
DeviceId: devid,
MeterReadings: r,
Expand Down Expand Up @@ -101,10 +103,6 @@ func (m *Meter) GetReadableState() string {
return retval
}

func (m *Meter) GetMeterType() MeterType {
return m.Type
}

func (m *Meter) AddSnip(snip QuerySnip) {
m.MeterReadings.AddSnip(snip)
}
Expand Down
113 changes: 58 additions & 55 deletions modbus.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,14 @@ func NewModbusEngine(

func (q *ModbusEngine) query(snip QuerySnip) (retval []byte, err error) {
q.status.IncreaseModbusRequestCounter()

// update the slave id in the handler
q.handler.SlaveId = snip.DeviceId

if snip.ReadLen <= 0 {
log.Fatalf("Invalid meter operation %v.", snip)
}

switch snip.FuncCode {
case ReadInputReg:
retval, err = q.client.ReadInputRegisters(snip.OpCode, snip.ReadLen)
Expand All @@ -112,9 +118,11 @@ func (q *ModbusEngine) query(snip QuerySnip) (retval []byte, err error) {
log.Fatalf("Unknown function code %d - cannot query device.",
snip.FuncCode)
}

if err != nil && q.verbose {
log.Printf("Failed to retrieve opcode 0x%x, error was: %s\r\n", snip.OpCode, err.Error())
}

return retval, err
}

Expand All @@ -125,6 +133,7 @@ func (q *ModbusEngine) Transform(
) {
var previousDeviceId uint8
for {
PROCESS_READINGS:
snip := <-inputStream
// The SDM devices need to have a little pause between querying
// different devices.
Expand All @@ -133,99 +142,93 @@ func (q *ModbusEngine) Transform(
}
previousDeviceId = snip.DeviceId

var err error
var reading []byte
for retryCount := 0; retryCount < MaxRetryCount; retryCount++ {
reading, err := q.query(snip)
if err == nil {
// convert bytes to value
snip.Value = snip.Transform(reading)
snip.ReadTimestamp = time.Now()
outputStream <- snip

// signal ok
successSnip := ControlSnip{
Type: CONTROLSNIP_OK,
Message: "OK",
DeviceId: snip.DeviceId,
}
controlStream <- successSnip

tryCnt := 0
for tryCnt = 0; tryCnt < MaxRetryCount; tryCnt++ {
reading, err = q.query(snip)
if err != nil {
goto PROCESS_READINGS
} else {
q.status.IncreaseModbusReconnectCounter()
log.Printf("Device %d failed to respond - retry attempt %d of %d",
snip.DeviceId, tryCnt+1, MaxRetryCount)
snip.DeviceId, retryCount+1, MaxRetryCount)
time.Sleep(time.Duration(100) * time.Millisecond)
} else {
break
}
}

if tryCnt == MaxRetryCount {
// signal error
errorSnip := ControlSnip{
Type: CONTROLSNIP_ERROR,
Message: fmt.Sprintf("Device %d did not respond.", snip.DeviceId),
DeviceId: snip.DeviceId,
}
controlStream <- errorSnip
} else {
// convert bytes to value
snip.Value = snip.Transform(reading)
snip.ReadTimestamp = time.Now()
outputStream <- snip

successSnip := ControlSnip{
Type: CONTROLSNIP_OK,
Message: "OK",
DeviceId: snip.DeviceId,
}
controlStream <- successSnip
}
}
}

func (q *ModbusEngine) Scan() {
type Device struct {
type DeviceInfo struct {
DeviceId uint8
DeviceType MeterType
MeterType string
}

devicelist := make([]Device, 0)
var deviceId uint8
deviceList := make([]DeviceInfo, 0)
oldtimeout := q.handler.Timeout
q.handler.Timeout = 50 * time.Millisecond
log.Printf("Starting bus scan")

probe := func(meterType MeterType, snip QuerySnip) bool {
producers := []Producer{
NewSDMProducer(),
NewJanitzaProducer(),
NewDZGProducer(),
}

SCAN:
// loop over all valid slave adresses
for deviceId = 1; deviceId <= 247; deviceId++ {
// give the bus some time to recover before querying the next device
time.Sleep(time.Duration(40) * time.Millisecond)

for _, producer := range producers {
snip := producer.Probe(deviceId)

value, err := q.query(snip)
if err == nil {
log.Printf("Device %d: %s type device found, %s: %.2f\r\n",
snip.DeviceId,
meterType,
deviceId,
producer.GetMeterType(),
GetIecDescription(snip.IEC61850),
snip.Transform(value))
dev := Device{
DeviceId: snip.DeviceId,
DeviceType: meterType,
dev := DeviceInfo{
DeviceId: deviceId,
MeterType: producer.GetMeterType(),
}
devicelist = append(devicelist, dev)
return true
deviceList = append(deviceList, dev)
continue SCAN
}
return false
}

// loop over all valid slave adresses
var devid uint8
for devid = 1; devid <= 247; devid++ {
if probe(METERTYPE_SDM, NewSDMProducer().Probe(devid)) {
continue
}
if probe(METERTYPE_JANITZA, NewJanitzaProducer().Probe(devid)) {
continue
}
if probe(METERTYPE_DZG, NewDZGProducer().Probe(devid)) {
continue
}

log.Printf("Device %d: n/a\r\n", devid)

// give the bus some time to recover before querying the next device
time.Sleep(time.Duration(40) * time.Millisecond)
log.Printf("Device %d: n/a\r\n", deviceId)
}

// restore timeout to old value
q.handler.Timeout = oldtimeout
log.Printf("Found %d active devices:\r\n", len(devicelist))
for _, device := range devicelist {
log.Printf("Found %d active devices:\r\n", len(deviceList))
for _, device := range deviceList {
log.Printf("* slave address %d: type %s\r\n", device.DeviceId,
device.DeviceType)
device.MeterType)
}
log.Println("WARNING: This lists only the devices that responded to " +
"a known probe request. Devices with different " +
Expand Down
6 changes: 5 additions & 1 deletion mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ func (mqttClient *MqttClient) Run() {
for {
snip := <-mqttClient.in
if mqttClient.verbose {
log.Printf("MQTT: got meter data (device %d: data: %s, value: %.3f W, desc: %s)", snip.DeviceId, snip.IEC61850, snip.Value, snip.Description)
log.Printf("MQTT: got meter data (device %d: data: %s, value: %.3f W, desc: %s)",
snip.DeviceId,
snip.IEC61850,
snip.Value,
GetIecDescription(snip.IEC61850))
}

uniqueID := fmt.Sprintf(UniqueIdFormat, snip.DeviceId)
Expand Down
Loading