Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
boomer41 committed Jun 6, 2023
0 parents commit 5798b3b
Show file tree
Hide file tree
Showing 45 changed files with 14,446 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
tmp/
bin/
.idea/
*.iml
619 changes: 619 additions & 0 deletions LICENSE.txt

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
all: bin native linux-arm64 linux-amd64

.PHONY: clean native linux-arm64 linux-amd64

clean:
-rm -Rf ./bin

bin:
mkdir -p bin

native:
CGO_ENABLED=0 go build -o bin/smlToHttp-native

linux-arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/smlToHttp-linux-arm64

linux-amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/smlToHttp-linux-amd64
132 changes: 132 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# SML to HTTP proxy

This application is designed to read a binary SML stream via TCP/IP and export all values via a simple HTTP REST API using JSON.
It is intended to be used with home automation systems to read the values of smart meters.
In the most basic configuration, a serial to TCP/IP converter in conjunction with a D0 read head can be used.

## Building

To build this application first install Go 1.20 or later.
Then run the provided Makefile:

```shell
make
```

This builds multiple binaries:

| Path | Usage |
|----------------------------|-----------------------------------------------------------------------------|
| bin/smlToHttp-linux-native | Built for the computer building the application |
| bin/smlToHttp-linux-amd64 | Built for a normal desktop computer |
| bin/smlToHttp-linux-arm64 | Built for a computer running the ARM64 architecture, e. g. a Raspberry Pi 4 |

Feel free to extend the Makefile if your use case is not dealt with.

## Usage

First create a configuration file named `config.yml`.
Replace the IP address serial to TCP/IP converter in the meters section.
Note: You can add multiple meters, not just one.

To disable the HTTP request log, set `web/disable_request_log` to `true`.
Similarly, you can disable the SML reception log by setting `meters/disable_reception_log` to `true` on a per meter basis.

```yaml
web:
address: 127.0.0.1:11123
disable_request_log: false

meters:
- id: my_smartmeter
address: 192.168.0.1:8234
reconnect_delay: 10
read_timeout: 5
connect_timeout: 10
debug: false
disable_reception_log: false
```
Then start the application:
```shell
./bin/smlToHttp-native -config config.yml
```

The log should now yield that the connection is successful and that SML frames are being decoded.
You can then try to access the API:

```shell
curl http://127.0.0.1:11123/processImage
```

The response should look something along the lines of:

```json
{
"meters": {
"my_smartmeter": {
"connected": true,
"lastUpdate": "2023-06-06T13:12:10.064515753Z",
"values": {
"1-0:1.8.0*255": {
"value": 123456.7,
"unit": 30
},
"1-0:2.8.0*255": {
"value": 234567.8,
"unit": 30
},
"1-0:16.7.0*255": {
"value": -3210,
"unit": 27
},
"... continued ...": {}
}
}
}
}
```

The response above has been truncated a bit, but you should get the gist out of it.
In the example above, the OBIS key `1-0:1.8.0*255` yields a value of `123456.7`, which represents 123456.7 kWh of retrieved energy from the energy provider.
Also, we have sold 234567.8 kWh of energy to the service provider.
And our current power draw is -3210 W, so we are currently selling 3210 Watts to the service provider.
A positive value here would mean that we currently buy energy from the provider.
Please refer to your smart meter user manual for exported OBIS items.

## Integration with OpenHAB

The proxy is currently in production use in combination with OpenHAB, but may of course serve other systems.
For this example, the HTTP binding and the JSONPath transformation addons are required.
An example configuration might be:

```text
Thing http:url:smlToHttp "SML to HTTP" [
baseURL="http://127.0.0.1:11123/processImage",
refresh=2
] {
Channels:
Type number : my_smartmeter_1_8_0 "My SmartMeter 1.8.0" [ stateTransformation="JSONPATH:$.meters.my_smartmeter.values.['1-0:1.8.0*255'].value", unit="Wh", mode="READONLY" ]
Type number : my_smartmeter_2_8_0 "My SmartMeter 2.8.0" [ stateTransformation="JSONPATH:$.meters.my_smartmeter.values.['1-0:2.8.0*255'].value", unit="Wh", mode="READONLY" ]
Type number : my_smartmeter_16_7_0 "My SmartMeter 16.7.0" [ stateTransformation="JSONPATH:$.meters.my_smartmeter.values.['1-0:16.7.0*255'].value", unit="W", mode="READONLY" ]
}
```

## License

SML to HTTP proxy
Copyright (C) 2023 Stephan Brunner

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
21 changes: 21 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

type config struct {
Web webConfig `yaml:"web"`
Meters []meterConfig `yaml:"meters"`
}

type webConfig struct {
Address string `yaml:"address"`
DisableRequestLog bool `yaml:"disable_request_log"`
}

type meterConfig struct {
Id string `yaml:"id"`
Address string `yaml:"address"`
ReconnectDelay int `yaml:"reconnect_delay"`
ReadTimeout int `yaml:"read_timeout"`
ConnectTimeout int `yaml:"connect_timeout"`
DisableReceptionLog bool `yaml:"disable_reception_log"`
Debug bool `yaml:"debug"`
}
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module smlToHttp

go 1.20

require (
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
gopkg.in/yaml.v3 v3.0.1
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
54 changes: 54 additions & 0 deletions logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"fmt"
"log"
"sync"
)

type logger interface {
newSubLogger(prefix string) logger

Printf(fmt string, v ...any)
}

type rootLogger struct {
lock *sync.Mutex
}

func newLogger() logger {
return &rootLogger{
lock: &sync.Mutex{},
}
}

func (l *rootLogger) newSubLogger(prefix string) logger {
return &subLogger{
parent: l,
prefix: prefix,
}
}

func (l *rootLogger) Printf(fmt string, v ...any) {
l.lock.Lock()
defer l.lock.Unlock()

log.Printf(fmt, v...)
}

type subLogger struct {
parent logger
prefix string
}

func (s *subLogger) newSubLogger(prefix string) logger {
return &subLogger{
parent: s,
prefix: prefix,
}
}

func (s *subLogger) Printf(format string, v ...any) {
str := fmt.Sprintf(format, v...)
s.parent.Printf("%s: %s", s.prefix, str)
}
93 changes: 93 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package main

import (
"flag"
"io"
"log"
"net/http"
"os"

"gopkg.in/yaml.v3"
)

func main() {
configFileFlag := flag.String("config", "", "The config file")

flag.Parse()

if len(*configFileFlag) == 0 {
print(
"SML to HTTP proxy\n" +
" Copyright (C) 2023 Stephan Brunner\n" +
" This program comes with ABSOLUTELY NO WARRANTY.\n" +
" This is free software, and you are welcome to redistribute it\n" +
" under the terms of the GNU GPL v3; see LICENSE.txt and README.md for details.\n\n" +
" The source code is available at https://github.com/boomer41/SML-to-HTTP-proxy\n\n",
)
flag.Usage()
return
}

cfg, err := loadConfig(*configFileFlag)

if err != nil {
log.Fatalf("failed to load configuration: %v", err)
}

l := newLogger()

image := newProcessImageManager(cfg)
exporter := newWebExporter(image, l.newSubLogger("web"))
meters := newMeterManager(cfg.Meters, image, l.newSubLogger("meterManager"))

errorChannel := make(chan error)

go func() {
err := exporter.serve(&cfg.Web)

if err == http.ErrServerClosed {
return
}

errorChannel <- err
}()

go func() {
err := meters.run()
errorChannel <- err
}()

for {
err := <-errorChannel

log.Fatalf("subsystem returned error: %v", err)
}
}

func loadConfig(path string) (*config, error) {
var configContent []byte
{
f, err := os.OpenFile(path, os.O_RDONLY, 0)

if err != nil {
return nil, err
}

defer f.Close()

configContent, err = io.ReadAll(f)

if err != nil {
return nil, err
}
}

var c config
err := yaml.Unmarshal(configContent, &c)

if err != nil {
return nil, err
}

return &c, nil
}
Loading

0 comments on commit 5798b3b

Please sign in to comment.