Commit Diff


commit - 1636c523802a3144d79ae3175abb6a3ea5d64778
commit + 6ab6ce12c6d5cd60f205c9f5d9d33391b52c5f45
blob - 147b0817f915ff2871fb23ca627da9371eb95400
blob + b2879e418897334a3a65bcc858fe147d66c8f3d0
--- cmd/dishy/dishy.go
+++ cmd/dishy/dishy.go
@@ -2,7 +2,9 @@ package main
 
 import (
 	"flag"
+	"fmt"
 	"log"
+	"os"
 
 	"olowe.co/dishy"
 )
@@ -11,6 +13,23 @@ const usage = "usage: dishy [-a address] command"
 
 var aFlag = flag.String("a", dishy.DefaultDishyAddr, "dishy device IP address")
 
+func printStatus(client *dishy.Client) error {
+	stat, err := client.Status()
+	if err != nil {
+		return fmt.Errorf("read status: %w", err)
+	}
+	fmt.Fprintln(os.Stderr, stat)
+	fmt.Println("alerts:", stat.Alerts)
+	fmt.Println("id:", stat.DeviceInfo.Id)
+	fmt.Println("ready:", stat.ReadyStates)
+	fmt.Println("outage:", stat.Outage)
+	fmt.Println("gps:", stat.GpsStats)
+	fmt.Println("stowed:", stat.StowRequested)
+	fmt.Println("updates:", stat.SoftwareUpdateState)
+	fmt.Println("lowsnr:", stat.IsSnrPersistentlyLow)
+	return nil
+}
+
 func main() {
 	log.SetFlags(0)
 	log.SetPrefix("dishy:")
@@ -36,6 +55,14 @@ func main() {
 		err = client.Stow()
 	case "unstow":
 		err = client.Unstow()
+	case "stat":
+		err = printStatus(client)
+	case "metrics":
+		stat, err := client.Status()
+		if err != nil {
+			log.Fatal("read status: %v", err)
+		}
+		err = dishy.WriteOpenMetrics(os.Stdout, stat)
 	}
 	if err != nil {
 		log.Fatalf("%s: %v", cmd, err)
blob - 9534fb0d1ac62309fd51d135b4637ea51f9eaaac
blob + 3d3730d6779ecd999f4a4d97942a0918dc669622
--- cmd/dishy/doc.go
+++ cmd/dishy/doc.go
@@ -11,6 +11,10 @@ The following commands are understood:
 		Reposition dish in a vertical orientation for easier transport.
 	unstow
 		Reposition the dish in the orientation prior to unstowing.
+	stat
+		Print short device status and diagnostics.
+	metrics
+		Print device statistics in OpenMetrics/Prometheus format.
 
 The flag -a specifies the address to connect to dishy.
 By default this is the default IPv4 address and port that dishy listens on 192.168.100.1:9200.
blob - 6893fbe340039216aea4a53d12a7662d90cc73a3
blob + 5b2c4d7e3d7799570fd20e7ce6d8c22f2c3dea28
--- dishy.go
+++ dishy.go
@@ -2,6 +2,7 @@ package dishy
 
 import (
 	"context"
+	"fmt"
 	"time"
 
 	"google.golang.org/grpc"
@@ -69,6 +70,71 @@ func (c *Client) Reboot() error {
 	return err
 }
 
+func (c *Client) Status() (*device.DishGetStatusResponse, error) {
+	req := &device.Request{
+		Request: &device.Request_GetStatus{
+			GetStatus: &device.GetStatusRequest{},
+		},
+	}
+	resp, err := c.do(req)
+	return resp.GetDishGetStatus(), err
+}
+
+func (c *Client) Interfaces() ([]device.NetworkInterface, error) {
+	req := &device.Request{
+		Request: &device.Request_GetNetworkInterfaces{
+			GetNetworkInterfaces: &device.GetNetworkInterfacesRequest{},
+		},
+	}
+	resp, err := c.do(req)
+	if err != nil {
+		return nil, err
+	}
+	if resp.GetGetNetworkInterfaces() == nil {
+		return nil, fmt.Errorf("no interfaces in response")
+	}
+	var ifaces []device.NetworkInterface
+	for _, iface := range resp.GetGetNetworkInterfaces().NetworkInterfaces {
+		if iface == nil {
+			continue
+		}
+		ifaces = append(ifaces, *iface)
+	}
+	return ifaces, nil
+}
+
+func (c *Client) TransceiverTelemetry() (*device.TransceiverGetTelemetryResponse, error) {
+	req := &device.Request{
+		Request: &device.Request_TransceiverGetTelemetry{
+			TransceiverGetTelemetry: &device.TransceiverGetTelemetryRequest{},
+		},
+	}
+	resp, err := c.do(req)
+	if err != nil {
+		return nil, fmt.Errorf("do request: %w", err)
+	}
+	if resp.GetTransceiverGetTelemetry() == nil {
+		return nil, fmt.Errorf("no telemetry in response")
+	}
+	return resp.GetTransceiverGetTelemetry(), nil
+}
+
+func (c *Client) TransceiverStat() (*device.TransceiverGetStatusResponse, error) {
+	req := &device.Request{
+		Request: &device.Request_TransceiverGetStatus{
+			TransceiverGetStatus: &device.TransceiverGetStatusRequest{},
+		},
+	}
+	resp, err := c.do(req)
+	if err != nil {
+		return nil, fmt.Errorf("do request: %w", err)
+	}
+	if resp.GetTransceiverGetStatus() == nil {
+		return nil, fmt.Errorf("no telemetry in response")
+	}
+	return resp.GetTransceiverGetStatus(), nil
+}
+
 func (c *Client) do(req *device.Request) (*device.Response, error) {
 	ctx := context.Background()
 	if c.Timeout > 0 {
blob - /dev/null
blob + 48e0fd5734d22c73f4a5744292fce191962c3fef (mode 644)
--- /dev/null
+++ metrics.go
@@ -0,0 +1,44 @@
+package dishy
+
+import (
+	"fmt"
+	"io"
+	"text/template"
+
+	"olowe.co/dishy/device"
+)
+
+const metricsPage string = `
+# HELP dishy_uptime_seconds Seconds since last boot.
+# TYPE dishy_uptime_seconds counter
+dishy_uptime_seconds {{ .DeviceState.UptimeS }}
+# HELP dishy_pop_ping_drop_rate
+# TYPE dishy_pop_ping_drop_rate gauge
+dishy_pop_ping_drop_rate {{ .PopPingDropRate }}
+# HELP dishy_pop_ping_latency_milliseconds
+# TYPE dishy_pop_ping_latency gauge
+dishy_pop_ping_latency_milliseconds {{ .PopPingLatencyMs }}
+# HELP dishy_downlink_throughput Received bytes per second.
+# TYPE dishy_downlink_throughput gauge
+dishy_downlink_throughput {{ .DownlinkThroughputBps }}
+# HELP dishy_uplink_throughput Transmitted bytes per second.
+# TYPE dishy_uplink_throughput gauge
+dishy_uplink_throughput {{ .UplinkThroughputBps }}
+# HELP dishy_obstruction_percentage
+# TYPE dishy_obstruction_percentage gauge
+dishy_obstruction_percentage {{ .ObstructionStats.FractionObstructed }}
+`
+
+var metricsTmpl = template.Must(template.New("metrics").Parse(metricsPage))
+
+// WriteOpenMetrics writes any metrics found in status in [OpenMetrics]
+// format to w for use in systems such as Prometheus and VictoriaMetrics.
+//
+// [OpenMetrics]: https://openmetrics.io/
+func WriteOpenMetrics(w io.Writer, status *device.DishGetStatusResponse) error {
+	err := metricsTmpl.Execute(w, status)
+	if err != nil {
+		return fmt.Errorf("execute template: %w", err)
+	}
+	return nil
+}