parse messages
This commit is contained in:
parent
65ad286add
commit
0897210754
5 changed files with 193 additions and 2 deletions
26
event_proxy/README.md
Normal file
26
event_proxy/README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Home Assistant Event Proxy
|
||||||
|
|
||||||
|
I found getting MQTT message sending on the pico MCP to be too trickey. I built this event proxy go app to simplify things.
|
||||||
|
With it in place the MCP only needs to send a very simple TCP message like "M01,25.5,60,1013". This this proxy app with
|
||||||
|
convert it into a Home Assistant MQTT event.
|
||||||
|
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
|
||||||
|
Run dev server with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Run unit tests with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ go test -v
|
||||||
|
```
|
||||||
|
|
||||||
|
To send a test message from the command line:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ echo "M01,1.0,1.1,1.2" | nc 192.168.1.153 8080
|
||||||
|
```
|
||||||
3
event_proxy/go.mod
Normal file
3
event_proxy/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module event_proxy
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
@ -1,18 +1,48 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
ID int
|
||||||
|
Data []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMsg(msg string) (*Message, error) {
|
||||||
|
parts := strings.Split(msg, ",")
|
||||||
|
msgIdStr := parts[0]
|
||||||
|
if !strings.HasPrefix(msgIdStr, "M") {
|
||||||
|
return nil, fmt.Errorf("message must start with 'M' prefix")
|
||||||
|
}
|
||||||
|
msgIdStr = strings.TrimPrefix(msgIdStr, "M")
|
||||||
|
msgId, err := strconv.Atoi(msgIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
msgData := make([]string, len(parts)-1)
|
||||||
|
for i, data := range parts[1:] {
|
||||||
|
msgData[i] = strings.TrimSpace(data)
|
||||||
|
}
|
||||||
|
message := &Message{
|
||||||
|
ID: msgId,
|
||||||
|
Data: msgData,
|
||||||
|
}
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Initialize JSON logger
|
// Initialize JSON logger
|
||||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
addr := "192.168.1.153:8080"
|
addr := "0.0.0.0:8080"
|
||||||
slog.Info("Starting TCP server", "address", addr)
|
slog.Info("Starting TCP server", "address", addr)
|
||||||
|
|
||||||
//Resolve address
|
//Resolve address
|
||||||
|
|
@ -60,7 +90,14 @@ func handleConnection(conn *net.TCPConn) {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slog.Info("Received message", "message", string(buffer[:n]), "bytes", n)
|
msgStr := string(buffer[:n])
|
||||||
|
slog.Info("Received message", "message", msgStr, "bytes", n)
|
||||||
|
msg, err := ParseMsg(msgStr)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error parsing message", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Info("Parsed message", "message", msg)
|
||||||
|
|
||||||
//Echo message back
|
//Echo message back
|
||||||
// _, err = conn.Write(buffer[:n])
|
// _, err = conn.Write(buffer[:n])
|
||||||
|
|
|
||||||
116
event_proxy/main_test.go
Normal file
116
event_proxy/main_test.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseMsg(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantID int
|
||||||
|
wantData []string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid message with single data element",
|
||||||
|
input: "M01,25.5",
|
||||||
|
wantID: 1,
|
||||||
|
wantData: []string{"25.5"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid message with multiple data elements",
|
||||||
|
input: "M02,25.5,60,1013",
|
||||||
|
wantID: 2,
|
||||||
|
wantData: []string{"25.5", "60", "1013"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid message with ID only",
|
||||||
|
input: "M0",
|
||||||
|
wantID: 0,
|
||||||
|
wantData: []string{},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid message with non-numeric ID",
|
||||||
|
input: "Mabc,data",
|
||||||
|
wantID: 0,
|
||||||
|
wantData: nil,
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid message with missing M prefix",
|
||||||
|
input: "1,data",
|
||||||
|
wantID: 0,
|
||||||
|
wantData: nil,
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid message with empty string",
|
||||||
|
input: "",
|
||||||
|
wantID: 0,
|
||||||
|
wantData: nil,
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid message with empty data elements",
|
||||||
|
input: "M5,,data,,",
|
||||||
|
wantID: 5,
|
||||||
|
wantData: []string{"", "data", "", ""},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid message with special characters in data",
|
||||||
|
input: "M10,key=value!@#$",
|
||||||
|
wantID: 10,
|
||||||
|
wantData: []string{"key=value!@#$"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newlines and whitespace are stripped from data",
|
||||||
|
input: "M1,1.0,1.1,1.2\n",
|
||||||
|
wantID: 1,
|
||||||
|
wantData: []string{"1.0", "1.1", "1.2"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leading and trailing whitespace stripped",
|
||||||
|
input: "M5, data1 , data2 \t, data3\r\n",
|
||||||
|
wantID: 5,
|
||||||
|
wantData: []string{"data1", "data2", "data3"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := ParseMsg(tt.input)
|
||||||
|
|
||||||
|
if (err != nil) != tt.wantError {
|
||||||
|
t.Errorf("ParseMsg() error = %v, wantError %v", err, tt.wantError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.ID != tt.wantID {
|
||||||
|
t.Errorf("ParseMsg() ID = %d, want %d", got.ID, tt.wantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got.Data) != len(tt.wantData) {
|
||||||
|
t.Errorf("ParseMsg() data length = %d, want %d", len(got.Data), len(tt.wantData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, val := range got.Data {
|
||||||
|
if val != tt.wantData[i] {
|
||||||
|
t.Errorf("ParseMsg() data[%d] = %q, want %q", i, val, tt.wantData[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
9
tasks.txt
Normal file
9
tasks.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
task: create text based architecture diagram of event proxy
|
||||||
|
task: split event proxy into multiple files following the architecture pattern
|
||||||
|
task: add deps files to event proxy needed for MQTT
|
||||||
|
task: finish node1 by creating a M002 for the air quility sensor readings
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
------------------------------
|
||||||
Loading…
Add table
Add a link
Reference in a new issue