Skip to content

Commit c594ed4

Browse files
committed
feat: add device monitor command
1 parent 168a15f commit c594ed4

4 files changed

Lines changed: 190 additions & 0 deletions

File tree

internal/app/enaptercli/cmd_device.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func buildCmdDevice() *cli.Command {
3030
buildCmdDeviceDelete(),
3131
buildCmdDeviceCommand(),
3232
buildCmdDeviceTelemetry(),
33+
buildCmdDeviceStream(),
3334
buildCmdDeviceCommunicationConfig(),
3435
buildCmdDeviceRunTerminal(),
3536
},
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package enaptercli
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"slices"
11+
"time"
12+
13+
"github.com/urfave/cli/v2"
14+
)
15+
16+
type cmdDeviceMonitor struct {
17+
cmdDevice
18+
deviceID string
19+
includeRuntime bool
20+
}
21+
22+
func buildCmdDeviceStream() *cli.Command {
23+
cmd := &cmdDeviceMonitor{}
24+
return &cli.Command{
25+
Name: "monitor",
26+
Usage: "Monitor device traffic",
27+
CustomHelpTemplate: cmd.CommandHelpTemplate(),
28+
Flags: cmd.Flags(),
29+
Before: cmd.Before,
30+
Action: func(cliCtx *cli.Context) error {
31+
return cmd.do(cliCtx.Context)
32+
},
33+
}
34+
}
35+
36+
func (c *cmdDeviceMonitor) Flags() []cli.Flag {
37+
flags := c.cmdDevice.Flags()
38+
return append(flags, &cli.StringFlag{
39+
Name: "device-id",
40+
Aliases: []string{"d"},
41+
Usage: "Device ID",
42+
Destination: &c.deviceID,
43+
Required: true,
44+
}, &cli.BoolFlag{
45+
Name: "include-runtime",
46+
Usage: "Monitor device's runtime traffic too",
47+
Destination: &c.includeRuntime,
48+
})
49+
}
50+
51+
func (c *cmdDeviceMonitor) do(ctx context.Context) error {
52+
deviceID, runtimeID, err := c.resolveDeviceIDs(ctx)
53+
if err != nil {
54+
return err
55+
}
56+
57+
query := make(url.Values)
58+
query.Add("id.in", deviceID)
59+
if runtimeID != "" {
60+
query.Add("id.in", runtimeID)
61+
}
62+
63+
return c.runWebSocket(ctx, runWebSocketParams{
64+
Path: "/",
65+
Query: query,
66+
RespProcessor: func(r io.Reader) error {
67+
return c.process(r, deviceID, runtimeID)
68+
},
69+
})
70+
}
71+
72+
func (c *cmdDeviceMonitor) resolveDeviceIDs(ctx context.Context) (string, string, error) {
73+
var resp struct {
74+
Device struct {
75+
ID string `json:"id"`
76+
Type string `json:"type"`
77+
Communication struct {
78+
Type string `json:"type"`
79+
UpstreamID string `json:"upstream_id"`
80+
} `json:"communication"`
81+
} `json:"device"`
82+
}
83+
84+
var query url.Values
85+
if c.includeRuntime {
86+
query = url.Values{"expand": {"communication"}}
87+
}
88+
89+
if err := c.doHTTPRequest(ctx, doHTTPRequestParams{
90+
Method: http.MethodGet,
91+
Path: c.deviceID,
92+
Query: query,
93+
RespProcessor: func(r *http.Response) error {
94+
if r.StatusCode != http.StatusOK {
95+
return cli.Exit(parseRespErrorMessage(r), 1)
96+
}
97+
return json.NewDecoder(r.Body).Decode(&resp)
98+
},
99+
}); err != nil {
100+
return "", "", err
101+
}
102+
103+
if !c.includeRuntime {
104+
return resp.Device.ID, "", nil
105+
}
106+
107+
runtimeCommTypes := []string{"UCM_LUA"}
108+
if !slices.Contains(runtimeCommTypes, resp.Device.Communication.Type) {
109+
fmt.Fprintln(c.errWriter,
110+
"WARNING: device does not run on a runtime, --include-runtime is ignored")
111+
return resp.Device.ID, "", nil
112+
}
113+
114+
return resp.Device.ID, resp.Device.Communication.UpstreamID, nil
115+
}
116+
117+
type streamMessage struct {
118+
DeviceID string `json:"device_id"`
119+
ReceivedAt time.Time `json:"received_at"`
120+
Timestamp int64 `json:"timestamp"`
121+
Telemetry json.RawMessage `json:"telemetry,omitempty"`
122+
Properties json.RawMessage `json:"properties,omitempty"`
123+
Log *struct {
124+
Severity string `json:"severity"`
125+
Message string `json:"message"`
126+
} `json:"log,omitempty"`
127+
}
128+
129+
func (c *cmdDeviceMonitor) process(r io.Reader, deviceID, runtimeID string) error {
130+
var m streamMessage
131+
if err := json.NewDecoder(r).Decode(&m); err != nil {
132+
return fmt.Errorf("parse payload: %w", err)
133+
}
134+
135+
fmt.Fprint(c.writer, c.messageTimestamp(m))
136+
fmt.Fprint(c.writer, " ")
137+
138+
switch m.DeviceID {
139+
case runtimeID:
140+
fmt.Fprint(c.writer, "runtime ")
141+
case deviceID:
142+
fmt.Fprint(c.writer, "device ")
143+
}
144+
145+
switch {
146+
case m.Telemetry != nil:
147+
fmt.Fprint(c.writer, "telemetry: ")
148+
fmt.Fprint(c.writer, string(m.Telemetry))
149+
case m.Properties != nil:
150+
fmt.Fprint(c.writer, "properties: ")
151+
fmt.Fprint(c.writer, string(m.Properties))
152+
case m.Log != nil:
153+
fmt.Fprint(c.writer, "logs: ")
154+
fmt.Fprintf(c.writer, "[%s] %s", m.Log.Severity, m.Log.Message)
155+
}
156+
157+
fmt.Fprintln(c.writer)
158+
159+
return nil
160+
}
161+
162+
func (c *cmdDeviceMonitor) messageTimestamp(m streamMessage) string {
163+
ts := m.ReceivedAt
164+
if ts.IsZero() {
165+
ts = time.Unix(m.Timestamp, 0)
166+
}
167+
return ts.UTC().Format(time.RFC3339)
168+
}

internal/app/enaptercli/testdata/helps/enapter device

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ COMMANDS:
1414
delete Delete a device
1515
command Manage device commands
1616
telemetry Show device telemetry
17+
monitor Monitor device traffic
1718
communication-config Manage device communication config
1819
run-terminal Run new remote terminal session
1920
help, h Shows a list of commands or help for one command
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
NAME:
2+
enaptercli.test device monitor - Monitor device traffic
3+
4+
USAGE:
5+
enaptercli.test device monitor [command options]
6+
7+
OPTIONS:
8+
--connection value, -c value Name of the connection to use
9+
--api-allow-insecure Allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE]
10+
--verbose Log extra details about the operation (default: false)
11+
--site-id value Site ID
12+
--device-id value, -d value Device ID
13+
--include-runtime Monitor device's runtime traffic too (default: false)
14+
--help, -h show help
15+
16+
ENVIRONMENT VARIABLES:
17+
ENAPTER3_API_TOKEN Enapter API access token
18+
ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com)
19+
ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false)
20+

0 commit comments

Comments
 (0)