2021-02-03 22:26:03 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-02-03 23:50:39 +00:00
|
|
|
"bytes"
|
2021-02-03 22:26:03 +00:00
|
|
|
"flag"
|
2022-12-11 21:56:14 +00:00
|
|
|
"fmt"
|
2021-02-03 23:50:39 +00:00
|
|
|
"io"
|
2021-02-03 22:26:03 +00:00
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"os/signal"
|
|
|
|
"syscall"
|
|
|
|
"time"
|
2021-02-03 23:50:39 +00:00
|
|
|
"unicode"
|
2021-02-03 22:26:03 +00:00
|
|
|
|
|
|
|
"github.com/tarm/serial"
|
|
|
|
)
|
|
|
|
|
2021-02-03 23:50:39 +00:00
|
|
|
func readSerial(s *serial.Port, c chan []byte) {
|
|
|
|
var unread bytes.Buffer
|
|
|
|
buf := make([]byte, 128)
|
2021-02-03 22:26:03 +00:00
|
|
|
|
2021-02-03 23:50:39 +00:00
|
|
|
for {
|
|
|
|
n, err := s.Read(buf)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
unread.Write(buf[:n])
|
|
|
|
|
|
|
|
for {
|
2022-08-20 10:02:14 +00:00
|
|
|
frame, err := unread.ReadBytes(2)
|
2021-02-03 23:50:39 +00:00
|
|
|
if err == io.EOF {
|
2022-08-20 10:02:14 +00:00
|
|
|
if _, err = unread.Write(frame); err != nil {
|
2021-02-03 23:50:39 +00:00
|
|
|
log.Println(err)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
} else if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2022-08-20 10:02:14 +00:00
|
|
|
c <- bytes.TrimRightFunc(frame, unicode.IsControl)
|
2021-02-03 23:58:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-20 10:02:14 +00:00
|
|
|
type Point struct {
|
|
|
|
Data []byte
|
|
|
|
Horodate *time.Time
|
2021-02-04 02:31:27 +00:00
|
|
|
}
|
|
|
|
|
2022-08-20 18:20:46 +00:00
|
|
|
func treatFrames(frames chan []byte, writer TICWriter, legacyMode bool) {
|
2021-02-03 23:58:03 +00:00
|
|
|
first := true
|
2021-02-10 01:21:15 +00:00
|
|
|
fframe:
|
2021-02-03 23:58:03 +00:00
|
|
|
for {
|
|
|
|
frame := <-frames
|
|
|
|
|
|
|
|
// Skip the first frame because it's not complete
|
|
|
|
if first {
|
|
|
|
first = false
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-08-20 10:02:14 +00:00
|
|
|
points := map[string]Point{}
|
2021-02-04 02:31:27 +00:00
|
|
|
|
|
|
|
var defaultHorodate time.Time
|
|
|
|
|
2021-02-03 23:58:03 +00:00
|
|
|
for _, line := range bytes.Split(frame, []byte("\r\n")) {
|
2022-08-20 18:20:46 +00:00
|
|
|
key, horodate, data, err := treatLine(line, legacyMode)
|
2021-02-03 23:58:03 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-08-20 18:20:46 +00:00
|
|
|
// Replace legacy keys
|
|
|
|
if legacyMode {
|
|
|
|
if nkey, ok := Legacy2Std[key]; ok {
|
|
|
|
key = nkey
|
|
|
|
}
|
|
|
|
}
|
2022-08-20 18:21:14 +00:00
|
|
|
|
2021-02-04 02:31:27 +00:00
|
|
|
// Skip ADCO, this is the Linky address, confidential and unrelevant
|
2022-08-20 18:21:14 +00:00
|
|
|
if key == "ADCO" || key == "ADSC" || key == "PRM" {
|
2021-02-04 02:31:27 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if key == "DATE" {
|
2021-02-10 01:21:15 +00:00
|
|
|
if horodate == nil {
|
|
|
|
continue fframe
|
|
|
|
}
|
2021-02-04 02:31:27 +00:00
|
|
|
defaultHorodate = *horodate
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-08-20 10:02:14 +00:00
|
|
|
points[key] = Point{
|
|
|
|
Data: data,
|
|
|
|
Horodate: horodate,
|
2021-02-04 02:31:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-20 10:02:14 +00:00
|
|
|
err := writer.AddPoints(points, defaultHorodate)
|
2021-02-04 02:31:27 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Println("Unable to write points:", err)
|
2021-02-03 23:50:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func computeChecksum(area []byte) (checksum byte) {
|
|
|
|
for _, c := range area {
|
|
|
|
checksum += c
|
|
|
|
}
|
|
|
|
|
|
|
|
checksum = (checksum & 0x3F) + 0x20
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-03 23:58:03 +00:00
|
|
|
func getHorodate(fields *[][]byte) (*time.Time, error) {
|
|
|
|
if (len(*fields) == 4 || string((*fields)[0]) == "DATE") && len((*fields)[1]) == 13 {
|
|
|
|
horodate, err := time.Parse("060102150405", string((*fields)[1][1:]))
|
2021-02-03 23:51:04 +00:00
|
|
|
if err != nil {
|
2021-02-03 23:58:03 +00:00
|
|
|
return nil, err
|
2021-02-03 23:51:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Handle "saison"
|
|
|
|
if (*fields)[1][0] == 'E' || (*fields)[1][0] == 'e' {
|
2021-02-04 02:31:27 +00:00
|
|
|
horodate = horodate.Add(-2 * time.Hour)
|
2021-02-03 23:51:04 +00:00
|
|
|
} else {
|
2021-02-04 02:31:27 +00:00
|
|
|
horodate = horodate.Add(-1 * time.Hour)
|
2021-02-03 23:51:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Mark field as treated
|
|
|
|
*fields = append((*fields)[:1], (*fields)[2:]...)
|
2021-02-03 23:58:03 +00:00
|
|
|
|
|
|
|
return &horodate, nil
|
2021-02-03 23:51:04 +00:00
|
|
|
}
|
|
|
|
|
2021-02-03 23:58:03 +00:00
|
|
|
return nil, nil
|
2021-02-03 23:51:04 +00:00
|
|
|
}
|
|
|
|
|
2022-08-20 18:20:46 +00:00
|
|
|
func treatLine(line []byte, legacyMode bool) (key string, horodate *time.Time, data []byte, err error) {
|
2021-02-03 23:58:03 +00:00
|
|
|
line = bytes.TrimSpace(line)
|
2021-02-03 23:50:39 +00:00
|
|
|
|
2021-02-03 23:58:03 +00:00
|
|
|
if len(line) <= 1 {
|
|
|
|
return
|
|
|
|
}
|
2021-02-03 23:50:39 +00:00
|
|
|
|
2021-02-03 23:58:03 +00:00
|
|
|
if computeChecksum(line[:len(line)-1]) != line[len(line)-1] {
|
2022-08-20 18:20:46 +00:00
|
|
|
// Try checksum mode 1
|
|
|
|
if !legacyMode || computeChecksum(line[:len(line)-2]) != line[len(line)-1] {
|
|
|
|
log.Printf("BAD checksum on %s: calculated: %c\n", line, computeChecksum(line[:len(line)-1]))
|
|
|
|
return
|
|
|
|
}
|
2021-02-03 23:58:03 +00:00
|
|
|
}
|
2021-02-03 23:50:39 +00:00
|
|
|
|
2021-02-03 23:58:03 +00:00
|
|
|
fields := bytes.Fields(line)
|
2021-02-03 23:50:39 +00:00
|
|
|
|
2022-12-11 21:56:14 +00:00
|
|
|
if len(fields) < 2 {
|
|
|
|
err = fmt.Errorf("Invalid number of fields in line")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-03 23:58:03 +00:00
|
|
|
key = string(fields[0])
|
2021-02-03 23:51:04 +00:00
|
|
|
|
2021-02-03 23:58:03 +00:00
|
|
|
horodate, err = getHorodate(&fields)
|
|
|
|
if err != nil {
|
|
|
|
return
|
2021-02-03 23:50:39 +00:00
|
|
|
}
|
2021-02-03 23:58:03 +00:00
|
|
|
|
2021-02-04 02:31:27 +00:00
|
|
|
data = bytes.Join(fields[1:len(fields)-1], []byte{' '})
|
2021-02-03 23:58:03 +00:00
|
|
|
|
|
|
|
return
|
2021-02-03 22:26:03 +00:00
|
|
|
}
|
|
|
|
|
2022-08-20 10:02:14 +00:00
|
|
|
type TICWriter interface {
|
|
|
|
Init() error
|
|
|
|
Close()
|
|
|
|
AddPoints(m map[string]Point, defaultHorodate time.Time) (err error)
|
|
|
|
}
|
|
|
|
|
2021-02-03 22:26:03 +00:00
|
|
|
func main() {
|
2022-08-19 19:44:10 +00:00
|
|
|
var legacyMode = flag.Bool("legacy-mode", false, "Assume teleinformation in legacy mode")
|
2022-12-04 09:54:23 +00:00
|
|
|
var pushFrequency = flag.Bool("push-frequency", false, "Also fetch data about the grid frequency")
|
2021-02-03 22:26:03 +00:00
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
if len(flag.Args()) < 1 {
|
|
|
|
log.Println("missing required argument: serial device (eg. /dev/ttyUSB0)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-19 19:44:10 +00:00
|
|
|
serialSpeed := 9600
|
2022-08-20 10:02:14 +00:00
|
|
|
parity := serial.ParityNone
|
2022-08-19 19:44:10 +00:00
|
|
|
if *legacyMode {
|
|
|
|
serialSpeed = 1200
|
2022-08-20 10:02:14 +00:00
|
|
|
parity = serial.ParityOdd
|
2022-08-19 19:44:10 +00:00
|
|
|
}
|
|
|
|
|
2021-02-03 22:26:03 +00:00
|
|
|
config := &serial.Config{
|
|
|
|
Name: flag.Args()[0],
|
2022-08-19 19:44:10 +00:00
|
|
|
Baud: serialSpeed,
|
2021-02-03 22:26:03 +00:00
|
|
|
Size: 7,
|
2022-08-20 10:02:14 +00:00
|
|
|
Parity: parity,
|
2021-02-03 22:26:03 +00:00
|
|
|
StopBits: 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
s, err := serial.OpenPort(config)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2022-08-20 10:02:14 +00:00
|
|
|
var writer TICWriter
|
|
|
|
writer = &InfluxWriter{}
|
|
|
|
err = writer.Init()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("Unable to initialize the writer:", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("linky2influxdb ready, listen to %s at %d baud", config.Name, config.Baud)
|
2021-02-04 02:31:27 +00:00
|
|
|
|
2021-02-03 23:58:03 +00:00
|
|
|
frames := make(chan []byte)
|
|
|
|
go readSerial(s, frames)
|
2022-08-20 18:20:46 +00:00
|
|
|
go treatFrames(frames, writer, *legacyMode)
|
2021-02-03 22:26:03 +00:00
|
|
|
|
|
|
|
interrupt := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
|
2022-12-04 09:54:23 +00:00
|
|
|
if *pushFrequency {
|
|
|
|
ticker := time.NewTicker(25 * time.Second)
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
frequencyloop:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ticker.C:
|
|
|
|
freq, err := FetchFrequency()
|
|
|
|
if err != nil {
|
|
|
|
log.Println("An error occurs during FetchFrequency: ", err.Error())
|
|
|
|
continue frequencyloop
|
|
|
|
}
|
|
|
|
|
|
|
|
err = WriteFrequency(writer, freq)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("An error occurs during WriteFrequency: ", err.Error())
|
|
|
|
}
|
|
|
|
case <-interrupt:
|
|
|
|
break frequencyloop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
<-interrupt
|
|
|
|
}
|
2021-02-04 02:31:27 +00:00
|
|
|
|
2022-08-20 10:02:14 +00:00
|
|
|
writer.Close()
|
2021-02-03 22:26:03 +00:00
|
|
|
}
|