-
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.
- Loading branch information
0 parents
commit 5798b3b
Showing
45 changed files
with
14,446 additions
and
0 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,4 @@ | ||
tmp/ | ||
bin/ | ||
.idea/ | ||
*.iml |
Large diffs are not rendered by default.
Oops, something went wrong.
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,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 |
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,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/>. |
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,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"` | ||
} |
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,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 | ||
) |
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,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= |
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,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) | ||
} |
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,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 | ||
} |
Oops, something went wrong.