From 1a953af65ca5b3a83e7a50407a0c480ab22cc924 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sat, 21 Mar 2026 16:06:11 +0100 Subject: tools: websocket: Add dummy and build integration Adding support for Shelly devices requires usage of the Websocket API. To make development easier a debug tool is created. Since it is not relevant for users it is not an artefact included into the deploy file archive. This also avoids later removing it which is by definition of the public API (see README.md) a breaking change. --- tools/meson.build | 15 +++++++++++++++ tools/websocket.go | 9 +++++++++ 2 files changed, 24 insertions(+) create mode 100644 tools/meson.build create mode 100644 tools/websocket.go (limited to 'tools') diff --git a/tools/meson.build b/tools/meson.build new file mode 100644 index 0000000..1322ce0 --- /dev/null +++ b/tools/meson.build @@ -0,0 +1,15 @@ +websocket_linux_amd64 = custom_target( + input : [ + meson.current_source_dir() / 'websocket.go', + ], + output : 'websocket-linux-amd64', + env : {'GOOS': 'linux', 'GOARCH': 'amd64'}, + command : [ + go, + 'build', + '-o', + '@OUTPUT@', + '@INPUT@', + ], + build_by_default : true, +) diff --git a/tools/websocket.go b/tools/websocket.go new file mode 100644 index 0000000..e429a6d --- /dev/null +++ b/tools/websocket.go @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Websockets are cool.") +} -- cgit v1.3 From c47c0736bceff60de31c0afc7005c51c5bf1daa6 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sat, 21 Mar 2026 16:38:56 +0100 Subject: tools: websocket: Add connection set up / tear down This was validated by sniffing the communication with Wireshark. The following is executed: - TCP initial handshake - GET /rpc from tool - HTTP 101 Switching Protocols (to Websocket) from Shelly - ACK by tool - TCP connection close initiated by tool This shows that the tool is able to make Websocket connections. --- tools/websocket.go | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) (limited to 'tools') diff --git a/tools/websocket.go b/tools/websocket.go index e429a6d..575bcd5 100644 --- a/tools/websocket.go +++ b/tools/websocket.go @@ -1,9 +1,47 @@ +// Websocket debug tool +// +// Usage: ./websocket-linux-amd64 ws:///rpc +// +// This tools is intended to support development of the Websocket-based +// application programming interface (API) of the Shelly Internet of Things +// (IoT) devices. + package main import ( - "fmt" + "log" + "net/url" + "os" + "os/signal" + + "github.com/gorilla/websocket" ) func main() { - fmt.Println("Websockets are cool.") + log.SetFlags(0) + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + var u url.URL = getURL() + log.Printf("connecting to %s", u.String()) + + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if (err != nil) { + log.Fatal(err) + } + defer c.Close() +} + +func getURL() url.URL { + if (len(os.Args) != 2) { + log.Fatalf("Exactly one argument expected but got %d.", len(os.Args) - 1) + } + + maybeURL, err := url.Parse(os.Args[1]) + if (err != nil) { + log.Fatalf("Cannot parse given URL: %s", os.Args[1]) + } + + return *maybeURL } -- cgit v1.3 From c68b6988d6a6e18efc73d2a979ab427c20ef2108 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sat, 21 Mar 2026 17:02:00 +0100 Subject: tools: websocket: Add wait for CTRL-C Now the connection is immediately established but just closed on SIGTERM and SIGINT. This allows to keep the connection for some time and lets the user decide when to stop. --- tools/websocket.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'tools') diff --git a/tools/websocket.go b/tools/websocket.go index 575bcd5..fc76d56 100644 --- a/tools/websocket.go +++ b/tools/websocket.go @@ -13,6 +13,7 @@ import ( "net/url" "os" "os/signal" + "syscall" "github.com/gorilla/websocket" ) @@ -31,6 +32,8 @@ func main() { log.Fatal(err) } defer c.Close() + + Await(syscall.SIGTERM, syscall.SIGINT) } func getURL() url.URL { @@ -45,3 +48,12 @@ func getURL() url.URL { return *maybeURL } + +func Await(signals ...os.Signal) { + listener := make(chan os.Signal, 1) + signal.Notify(listener, signals...) + defer signal.Stop(listener) + + sig := <-listener + log.Printf("Received OS signal '%v'\n", sig) +} -- cgit v1.3 From 8ee2df7b5f85b2914c074bf07da5557d5dbe4ebf Mon Sep 17 00:00:00 2001 From: xengineering Date: Sat, 21 Mar 2026 17:09:37 +0100 Subject: tools: websocket: Remove needless braces Programming in too many languages ... of course not needed in Go. --- tools/websocket.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'tools') diff --git a/tools/websocket.go b/tools/websocket.go index fc76d56..ea2ea86 100644 --- a/tools/websocket.go +++ b/tools/websocket.go @@ -28,7 +28,7 @@ func main() { log.Printf("connecting to %s", u.String()) c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) - if (err != nil) { + if err != nil { log.Fatal(err) } defer c.Close() @@ -37,12 +37,12 @@ func main() { } func getURL() url.URL { - if (len(os.Args) != 2) { + if len(os.Args) != 2 { log.Fatalf("Exactly one argument expected but got %d.", len(os.Args) - 1) } maybeURL, err := url.Parse(os.Args[1]) - if (err != nil) { + if err != nil { log.Fatalf("Cannot parse given URL: %s", os.Args[1]) } -- cgit v1.3 From 9ef837ba3816c57dccc35c39318824851896ff11 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 22 Mar 2026 09:54:32 +0100 Subject: tools: websocket: Implement Sys.GenConfig This is the first working command for the Shelly 2PM Gen3. --- tools/websocket.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'tools') diff --git a/tools/websocket.go b/tools/websocket.go index ea2ea86..c882ce2 100644 --- a/tools/websocket.go +++ b/tools/websocket.go @@ -33,6 +33,19 @@ func main() { } defer c.Close() + go func() { + for { + _, message, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + return + } + log.Printf("recv: %s", message) + } + }() + + getConfig(c) + Await(syscall.SIGTERM, syscall.SIGINT) } @@ -57,3 +70,21 @@ func Await(signals ...os.Signal) { sig := <-listener log.Printf("Received OS signal '%v'\n", sig) } + +func getConfig(c *websocket.Conn) { + request := ` +{ + "jsonrpc":"2.0", + "id": 1, + "src":"user_1", + "method":"Sys.GetConfig", + "params": { + "id":2 + } +} +` + err := c.WriteMessage(websocket.TextMessage, []byte(request)) + if err != nil { + log.Fatal(err) + } +} -- cgit v1.3 From f4cdb605863c883f77ff3a6dab6a5ead8930b0fc Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 22 Mar 2026 10:12:18 +0100 Subject: tools: websocket: Log pretty-printed TX JSON This allows to more easily see what is sent to the Shelly device. --- tools/websocket.go | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) (limited to 'tools') diff --git a/tools/websocket.go b/tools/websocket.go index c882ce2..b956ee8 100644 --- a/tools/websocket.go +++ b/tools/websocket.go @@ -9,10 +9,12 @@ package main import ( + "encoding/json" "log" "net/url" "os" "os/signal" + "strings" "syscall" "github.com/gorilla/websocket" @@ -83,8 +85,38 @@ func getConfig(c *websocket.Conn) { } } ` - err := c.WriteMessage(websocket.TextMessage, []byte(request)) + + tx(c, request) +} + +func tx(c *websocket.Conn, d string) { + input := []byte(d) + + var parsed any + + err := json.Unmarshal(input, &parsed) + if err != nil { + log.Fatal(err) + } + + pretty, err := json.MarshalIndent(parsed, "", " ") if err != nil { log.Fatal(err) } + + log.Println(quote(string(pretty), "> ")) + err = c.WriteMessage(websocket.TextMessage, pretty) + if err != nil { + log.Fatal(err) + } +} + +func quote(input string, quotation string) string { + lines := strings.Split(input, "\n") + + for i, line := range lines { + lines[i] = quotation + line + } + + return strings.Join(lines, "\n") } -- cgit v1.3 From 5bae36f4ccee5e699969770948a02f70e8fed9b0 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 22 Mar 2026 10:21:36 +0100 Subject: tools: websocket: Quote and prettify RX JSON This makes the responses from the Shelly device readable. --- tools/websocket.go | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) (limited to 'tools') diff --git a/tools/websocket.go b/tools/websocket.go index b956ee8..31c39fb 100644 --- a/tools/websocket.go +++ b/tools/websocket.go @@ -35,16 +35,7 @@ func main() { } defer c.Close() - go func() { - for { - _, message, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - return - } - log.Printf("recv: %s", message) - } - }() + go rx(c) getConfig(c) @@ -89,26 +80,40 @@ func getConfig(c *websocket.Conn) { tx(c, request) } -func tx(c *websocket.Conn, d string) { - input := []byte(d) +func rx(c *websocket.Conn) { + for { + _, message, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + return + } + log.Println(quote(prettify(string(message)), "< ")) + } +} - var parsed any +func tx(c *websocket.Conn, d string) { + log.Println(quote(prettify(d), "> ")) - err := json.Unmarshal(input, &parsed) + err := c.WriteMessage(websocket.TextMessage, []byte(d)) if err != nil { log.Fatal(err) } +} - pretty, err := json.MarshalIndent(parsed, "", " ") +func prettify(input string) string { + var parsed any + + err := json.Unmarshal([]byte(input), &parsed) if err != nil { log.Fatal(err) } - log.Println(quote(string(pretty), "> ")) - err = c.WriteMessage(websocket.TextMessage, pretty) + pretty, err := json.MarshalIndent(parsed, "", " ") if err != nil { log.Fatal(err) } + + return string(pretty) } func quote(input string, quotation string) string { -- cgit v1.3 From 10ef120da246e0e0eb2245e160792ed29e623a30 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 22 Mar 2026 10:26:42 +0100 Subject: tools: websocket: Add empty line before RX log This makes it easier to distinguish the JSON messages visually. --- tools/websocket.go | 1 + 1 file changed, 1 insertion(+) (limited to 'tools') diff --git a/tools/websocket.go b/tools/websocket.go index 31c39fb..594cb1b 100644 --- a/tools/websocket.go +++ b/tools/websocket.go @@ -87,6 +87,7 @@ func rx(c *websocket.Conn) { log.Println("read:", err) return } + log.Println("") log.Println(quote(prettify(string(message)), "< ")) } } -- cgit v1.3 From 589425d307cd7d06d3f7d052f6ea1a5245688d0d Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 22 Mar 2026 10:44:27 +0100 Subject: tools: websocket: Implement cover close / open This implements the minimal functionality to control covers with the Sia server in the development tool. Based on that the actual Sia server software can be adapted. --- tools/websocket.go | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) (limited to 'tools') diff --git a/tools/websocket.go b/tools/websocket.go index 594cb1b..af23959 100644 --- a/tools/websocket.go +++ b/tools/websocket.go @@ -16,6 +16,7 @@ import ( "os/signal" "strings" "syscall" + "time" "github.com/gorilla/websocket" ) @@ -38,6 +39,9 @@ func main() { go rx(c) getConfig(c) + coverClose(c) + time.Sleep(1 * time.Second) + coverOpen(c) Await(syscall.SIGTERM, syscall.SIGINT) } @@ -65,7 +69,7 @@ func Await(signals ...os.Signal) { } func getConfig(c *websocket.Conn) { - request := ` + tx(c, ` { "jsonrpc":"2.0", "id": 1, @@ -75,9 +79,35 @@ func getConfig(c *websocket.Conn) { "id":2 } } -` +`) +} - tx(c, request) +func coverClose(c *websocket.Conn) { + tx(c, ` +{ + "jsonrpc":"2.0", + "id": 1, + "src":"user_1", + "method":"Cover.Close", + "params": { + "id":0 + } +} +`) +} + +func coverOpen(c *websocket.Conn) { + tx(c, ` +{ + "jsonrpc":"2.0", + "id": 1, + "src":"user_1", + "method":"Cover.Open", + "params": { + "id":0 + } +} +`) } func rx(c *websocket.Conn) { -- cgit v1.3