Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ examples/mock/mock
examples/logging_demo
otto/otto
ottoctl/ottoctl
cmd/ottoctl/ottoctl/ottoctl
mock/mock
gpio/gpio
station/station
Expand Down
12 changes: 12 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ func (c *Client) GetLogConfig() (result utils.LogConfig, err error) {
return result, nil
}

// GetTimers retrieves timer information from the Otto server.
// Returns a slice of TickerInfo containing details about active timers.
//
// This calls the /api/timers endpoint on the server.
func (c *Client) GetTimers() ([]utils.TickerInfo, error) {
var timers []utils.TickerInfo
if err := c.get("/api/timers", &timers); err != nil {
return nil, err
}
return timers, nil
}

// Ping checks if the Otto server is reachable and responding.
// Returns nil if the server is healthy, error otherwise.
func (c *Client) Ping() error {
Expand Down
1 change: 1 addition & 0 deletions cmd/ottoctl/cmd_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func init() {
rootCmd.AddCommand(shutdownCmd)
rootCmd.AddCommand(stationsCmd)
rootCmd.AddCommand(statsCmd)
rootCmd.AddCommand(timersCmd)
rootCmd.AddCommand(versionCmd)
}

Expand Down
43 changes: 43 additions & 0 deletions cmd/ottoctl/cmd_timers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ottoctl

import (
"encoding/json"
"errors"
"fmt"
"log/slog"

"github.com/spf13/cobra"
)

var timersCmd = &cobra.Command{
Use: "timers",
Short: "Display timer information",
Long: `Display timer information from local or remote Otto instance`,
RunE: timersRun,
}

func timersRun(cmd *cobra.Command, args []string) error {
// Check if we should connect to a remote server
client := getClient()
if client == nil {
return errors.New("timersRun failed to get a client")
}

// Remote mode: fetch timers from server
slog.Debug("Fetching timers from remote server", "url", client.BaseURL)
timers, err := client.GetTimers()
if err != nil {
fmt.Fprintf(errOutput, "Error fetching remote timers: %v\n", err)
return err
}

// Pretty print the JSON response
jsonBytes, err := json.MarshalIndent(timers, "", " ")
if err != nil {
fmt.Fprintf(errOutput, "%s", err)

Copilot AI Jan 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message formatting is inconsistent with the pattern on line 30. The error should include a newline character for consistency. Consider changing fmt.Fprintf(errOutput, "%s", err) to fmt.Fprintf(errOutput, "%v\n", err) or fmt.Fprintf(errOutput, "Error marshaling JSON: %v\n", err) to match the format used on line 30.

Suggested change
fmt.Fprintf(errOutput, "%s", err)
fmt.Fprintf(errOutput, "%v\n", err)

Copilot uses AI. Check for mistakes.
return err
} else {
fmt.Fprintf(cmdOutput, "%s\n", string(jsonBytes))
}
return nil
}
Comment thread
rustyeddy marked this conversation as resolved.
Outdated
39 changes: 39 additions & 0 deletions cmd/ottoctl/cmd_timers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package ottoctl

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestTimers(t *testing.T) {
tstfile := "testdata/timers.json"
response, err := os.ReadFile(tstfile)
assert.NoError(t, err, "failed to open")

tst := newTstInput(timersRun, string(response))
err = httpQuery(t, tst)
assert.NoError(t, err)

output := tst.buffer.String()
assert.Contains(t, output, "heartbeat")
assert.Contains(t, output, "sensor_poll")
}

func TestTimersEmpty(t *testing.T) {
response := ""
tst := newTstInput(timersRun, response)
err := httpQuery(t, tst)
assert.Error(t, err)
}

func TestTimersError(t *testing.T) {
tst := newTstInput(timersRun, "")
tst.statusCode = 500
err := httpQuery(t, tst)
assert.Error(t, err)

output := tst.errbuf.String()
assert.Contains(t, output, "500 - Internal Server Error")
}
2 changes: 1 addition & 1 deletion cmd/ottoctl/ottoctl/Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ottoctl: main.go
go build -v . -o ottoctl
go build -v -o ottoctl .
20 changes: 20 additions & 0 deletions cmd/ottoctl/testdata/timers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"name": "heartbeat",
"last_tick": "2026-01-04T16:40:00Z",
"ticks": 120,
"active": true
},
{
"name": "sensor_poll",
"last_tick": "2026-01-04T16:39:55Z",
"ticks": 240,
"active": true
},
{
"name": "cleanup",
"last_tick": "2026-01-04T16:35:00Z",
"ticks": 6,
"active": false
}
]