diff --git a/api.go b/api.go
index 8fb9080..99543b4 100644
--- a/api.go
+++ b/api.go
@@ -1,12 +1,64 @@
package main
-import "encoding/xml"
+import (
+ "encoding/xml"
+ "fmt"
+ "regexp"
+ "strconv"
+ "time"
+)
+
+// List of XML RPC getter function codes.
+const (
+ FnLogin = "15"
+ FnLogout = "16"
+)
+
+// List of XML RPC setter function codes.
+const (
+ FnCMSystemInfo = "2"
+ FnCMState = "136"
+)
// List of string constants from the XML API responses.
const (
- OperStateOK = "OPERATIONAL"
+ OperStateOK = "OPERATIONAL"
+ NetworkAccessAllowed = "NetworkAccess"
)
+// CMSystemInfo shows cable modem system info.
+type CMSystemInfo struct {
+ DocsisMode string `xml:"cm_docsis_mode"`
+ HardwareVersion string `xml:"cm_hardware_version"`
+ MacAddr string `xml:"cm_mac_addr"`
+ SerialNumber string `xml:"cm_serial_number"`
+ SystemUptime int `xml:"cm_system_uptime"`
+ NetworkAccess string `xml:"cm_network_access"`
+}
+
+// UnmarshalXML is a standard unmarshaller + string to seconds convertor.
+func (c *CMSystemInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ type Alias CMSystemInfo
+ aux := &struct {
+ *Alias
+ SystemUptime string `xml:"cm_system_uptime"`
+ }{
+ Alias: (*Alias)(c),
+ }
+
+ if err := d.DecodeElement(&aux, &start); err != nil {
+ return err //nolint:wrapcheck
+ }
+
+ dur, err := parseDuration(aux.SystemUptime)
+ if err != nil {
+ return err //nolint:wrapcheck
+ }
+ c.SystemUptime = int(dur.Seconds())
+
+ return nil
+}
+
// CMState shows cable modem state.
type CMState struct {
TunnerTemperature int `xml:"TunnerTemperature"`
@@ -34,6 +86,28 @@ func (c *CMState) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil
}
+var durationRegexp = regexp.MustCompile(`(?:(\d+)day\(s\))?(\d+)h:(\d+)m:(\d+)s`)
+
+// Input format: "1day(s)2h:34m:56s".
+func parseDuration(s string) (time.Duration, error) {
+ matches := durationRegexp.FindStringSubmatch(s)
+ if len(matches) != 5 {
+ return 0, fmt.Errorf("invalid duration string")
+ }
+
+ days, _ := strconv.Atoi(matches[1])
+ hours, _ := strconv.Atoi(matches[2])
+ minutes, _ := strconv.Atoi(matches[3])
+ seconds, _ := strconv.Atoi(matches[4])
+
+ dur := time.Duration(days)*24*time.Hour +
+ time.Duration(hours)*time.Hour +
+ time.Duration(minutes)*time.Minute +
+ time.Duration(seconds)*time.Second
+
+ return dur, nil
+}
+
func fahrenheitToCelsius(f int) int {
return (f - 32) * 5.0 / 9
}
diff --git a/api_test.go b/api_test.go
index 7b247a1..507b5bc 100644
--- a/api_test.go
+++ b/api_test.go
@@ -7,6 +7,59 @@ import (
"github.com/stretchr/testify/require"
)
+// Allowed
+func TestCMSystemInfo_UnmarshalXML(t *testing.T) {
+ t.Run("valid xml", func(t *testing.T) {
+ data := `` +
+ `` +
+ `DOCSIS 3.0` +
+ `5.01` +
+ `00:00:00:00:00:00` +
+ `AAAAAAAAAAAA` +
+ `10day(s)20h:15m:30s` +
+ `Allowed` +
+ ``
+
+ var cminfo CMSystemInfo
+ err := xml.Unmarshal([]byte(data), &cminfo)
+ require.NoError(t, err)
+
+ expected := CMSystemInfo{
+ DocsisMode: "DOCSIS 3.0",
+ HardwareVersion: "5.01",
+ MacAddr: "00:00:00:00:00:00",
+ SerialNumber: "AAAAAAAAAAAA",
+ SystemUptime: 936930,
+ NetworkAccess: "Allowed",
+ }
+ require.Equal(t, expected, cminfo)
+ })
+
+ t.Run("invalid duration", func(t *testing.T) {
+ data := `` +
+ `` +
+ `DOCSIS 3.0` +
+ `5.01` +
+ `00:00:00:00:00:00` +
+ `AAAAAAAAAAAA` +
+ `hello, world` +
+ `Allowed` +
+ ``
+
+ var cminfo CMSystemInfo
+ err := xml.Unmarshal([]byte(data), &cminfo)
+ require.ErrorContains(t, err, "invalid duration string")
+ })
+
+ t.Run("invalid xml", func(t *testing.T) {
+ data := ``
+
+ var cminfo CMSystemInfo
+ err := xml.Unmarshal([]byte(data), &cminfo)
+ require.ErrorContains(t, err, "XML syntax error")
+ })
+}
+
func TestCMState_UnmarshalXML(t *testing.T) {
t.Run("valid xml", func(t *testing.T) {
data := `` +
diff --git a/collector.go b/collector.go
index a9fcddc..1f0f963 100644
--- a/collector.go
+++ b/collector.go
@@ -37,15 +37,67 @@ func (c *Collector) ServeHTTP(w http.ResponseWriter, r *http.Request) {
reg := prometheus.NewRegistry()
c.collectCMState(r.Context(), reg, client)
-
- if err := client.Logout(r.Context()); err != nil {
- log.Fatalf("Failed to logout: %v", err)
- }
+ c.collectCMSSystemInfo(r.Context(), reg, client)
h := promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
h.ServeHTTP(w, r)
}
+func (c *Collector) collectCMSSystemInfo(
+ ctx context.Context,
+ reg *prometheus.Registry,
+ client *ConnectBox,
+) {
+ cmDocsisModeGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "connect_box_cm_docsis_mode",
+ Help: "cm_docsis_mode.",
+ }, []string{"mode"})
+ cmHardwareVersionGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "connect_box_cm_hardware_version",
+ Help: "cm_hardware_version.",
+ }, []string{"version"})
+ cmMacAddrGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "connect_box_cm_mac_addr",
+ Help: "cm_mac_addr.",
+ }, []string{"addr"})
+ cmSerialNumberGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "connect_box_cm_serial_number",
+ Help: "cm_serial_number.",
+ }, []string{"sn"})
+ cmSystemUptimeGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "connect_box_cm_system_uptime",
+ Help: "cm_system_uptime.",
+ }, []string{})
+ cmNetworkAccessGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "connect_box_cm_network_access",
+ Help: "cm_network_access.",
+ }, []string{})
+
+ reg.MustRegister(cmDocsisModeGauge)
+ reg.MustRegister(cmHardwareVersionGauge)
+ reg.MustRegister(cmMacAddrGauge)
+ reg.MustRegister(cmSerialNumberGauge)
+ reg.MustRegister(cmSystemUptimeGauge)
+ reg.MustRegister(cmNetworkAccessGauge)
+
+ var data CMSystemInfo
+ err := client.GetMetrics(ctx, FnCMSystemInfo, &data)
+ if err == nil {
+ cmDocsisModeGauge.WithLabelValues(data.DocsisMode).Set(1)
+ cmHardwareVersionGauge.WithLabelValues(data.HardwareVersion).Set(1)
+ cmMacAddrGauge.WithLabelValues(data.MacAddr).Set(1)
+ cmSerialNumberGauge.WithLabelValues(data.SerialNumber).Set(1)
+ cmSystemUptimeGauge.WithLabelValues().Set(float64(data.SystemUptime))
+ var val float64
+ if data.NetworkAccess == NetworkAccessAllowed {
+ val = 1
+ }
+ cmNetworkAccessGauge.WithLabelValues().Set(val)
+ } else {
+ log.Printf("Failed to get CMState: %v", err)
+ }
+}
+
func (c *Collector) collectCMState(
ctx context.Context,
reg *prometheus.Registry,