diff --git a/event_proxy/README.md b/event_proxy/README.md new file mode 100644 index 0000000..184abf9 --- /dev/null +++ b/event_proxy/README.md @@ -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 +``` diff --git a/event_proxy/go.mod b/event_proxy/go.mod new file mode 100644 index 0000000..6ca9105 --- /dev/null +++ b/event_proxy/go.mod @@ -0,0 +1,3 @@ +module event_proxy + +go 1.25.0 diff --git a/event_proxy/main.go b/event_proxy/main.go index 53e1c33..e20def8 100644 --- a/event_proxy/main.go +++ b/event_proxy/main.go @@ -1,18 +1,48 @@ package main import ( + "fmt" "io" "log/slog" "net" "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() { // Initialize JSON logger logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) slog.SetDefault(logger) - addr := "192.168.1.153:8080" + addr := "0.0.0.0:8080" slog.Info("Starting TCP server", "address", addr) //Resolve address @@ -60,7 +90,14 @@ func handleConnection(conn *net.TCPConn) { if n == 0 { 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 // _, err = conn.Write(buffer[:n]) diff --git a/event_proxy/main_test.go b/event_proxy/main_test.go new file mode 100644 index 0000000..9b1593b --- /dev/null +++ b/event_proxy/main_test.go @@ -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]) + } + } + }) + } +} diff --git a/tasks.txt b/tasks.txt new file mode 100644 index 0000000..c550892 --- /dev/null +++ b/tasks.txt @@ -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 + + + +------------------------------