Skip to content
This repository has been archived by the owner on Apr 14, 2021. It is now read-only.

Commit

Permalink
Issue #116: display fbox info via new command "fritzctl boxinfo" (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
bpicode authored Dec 16, 2017
1 parent 525ebe8 commit ca48642
Show file tree
Hide file tree
Showing 13 changed files with 275 additions and 25 deletions.
36 changes: 36 additions & 0 deletions cmd/boxinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cmd

import (
"fmt"

"github.com/bpicode/fritzctl/console"
"github.com/bpicode/fritzctl/fritz"
"github.com/spf13/cobra"
)

var boxInfoCmd = &cobra.Command{
Use: "boxinfo",
Short: "Display information about the FRITZ!Box",
Long: "Show information about the FRITZ!Box like firmware version, uptime, etc.",
Example: "fritzctl boxinfo",
RunE: boxInfo,
}

func init() {
RootCmd.AddCommand(boxInfoCmd)
}

func boxInfo(_ *cobra.Command, _ []string) error {
c := clientLogin()
f := fritz.NewInternal(c)
info, err := f.BoxInfo()
assertNoErr(err, "cannot obtain FRITZ!Box data")
printBoxInfo(info)
return nil
}

func printBoxInfo(boxData *fritz.BoxData) {
fmt.Printf("%s %s\n", console.Cyan("Model: "), boxData.Model)
fmt.Printf("%s %s\n", console.Cyan("Firmware: "), boxData.FirmwareVersion)
fmt.Printf("%s %s\n", console.Cyan("Running: "), boxData.Runtime)
}
2 changes: 2 additions & 0 deletions cmd/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ func TestCommands(t *testing.T) {
{cmd: listCallsCmd, args: []string{}, srv: mock.New().UnstartedServer()},
{cmd: listSwitchesCmd, srv: mock.New().UnstartedServer()},
{cmd: listThermostatsCmd, srv: mock.New().UnstartedServer()},
{cmd: listThermostatsCmd, srv: mock.New().UnstartedServer()},
{cmd: docManCmd, srv: mock.New().UnstartedServer()},
{cmd: boxInfoCmd, srv: mock.New().UnstartedServer()},
}
for _, testCase := range testCases {
t.Run(fmt.Sprintf("Test run command %s", testCase.cmd.Name()), func(t *testing.T) {
Expand Down
14 changes: 14 additions & 0 deletions console/colors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package console

import "github.com/fatih/color"

var (
// Red can be used as Sprintf, where the output it wrapped in escape characters which will render the text red in terminals.
Red = color.New(color.Bold, color.FgRed).SprintfFunc()
// Green can be used as Sprintf, where the output it wrapped in escape characters which will render the text green in terminals.
Green = color.New(color.Bold, color.FgGreen).SprintfFunc()
// Yellow can be used as Sprintf, where the output it wrapped in escape characters which will render the text yellow in terminals.
Yellow = color.New(color.Bold, color.FgYellow).SprintfFunc()
// Cyan can be used as Sprintf, where the output it wrapped in escape characters which will render the text cyan in terminals.
Cyan = color.New(color.FgCyan).SprintfFunc()
)
3 changes: 1 addition & 2 deletions console/survey.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,8 @@ func (s *Survey) writeTo(m map[string]interface{}, v interface{}) error {

func (q *Question) prompt(w io.Writer) {
hint := q.defaultHint()
cyan := color.New(color.FgCyan).SprintfFunc()
bold := color.New(color.Bold).SprintfFunc()
fmt.Fprintf(w, "%s %s%s: ", cyan("?"), bold("%s", q.Text), hint)
fmt.Fprintf(w, "%s %s%s: ", Cyan("?"), bold("%s", q.Text), hint)
}

func (q *Question) defaultHint() string {
Expand Down
11 changes: 0 additions & 11 deletions console/symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package console

import (
"strings"

"github.com/fatih/color"
)

const (
Expand All @@ -12,15 +10,6 @@ const (
checkQ = "?"
)

var (
// Red can be used as Sprintf, where the output it wrapped in escape characters which will render the text red in terminals.
Red = color.New(color.Bold, color.FgRed).SprintfFunc()
// Green can be used as Sprintf, where the output it wrapped in escape characters which will render the text green in terminals.
Green = color.New(color.Bold, color.FgGreen).SprintfFunc()
// Yellow can be used as Sprintf, where the output it wrapped in escape characters which will render the text yellow in terminals.
Yellow = color.New(color.Bold, color.FgYellow).SprintfFunc()
)

// Checkmark type is a string with some functions attached.
type Checkmark string

Expand Down
19 changes: 19 additions & 0 deletions fritz/api_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package fritz

import (
"github.com/bpicode/fritzctl/httpread"
"github.com/pkg/errors"
)

// Internal exposes Fritz!Box internal and undocumented API.
type Internal interface {
ListLanDevices() (*LanDevices, error)
ListLogs() (*MessageLog, error)
InternetStats() (*TrafficMonitoringData, error)
BoxInfo() (*BoxData, error)
}

// NewInternal creates a Fritz/internal API from a given client.
Expand Down Expand Up @@ -54,10 +56,27 @@ func (i *internal) InternetStats() (*TrafficMonitoringData, error) {
return &data[0], err
}

// BoxInfo queries metadata from the FRITZ!Box. Data is drawn from: https://fritz.box/cgi-bin/system_status.
func (i *internal) BoxInfo() (*BoxData, error) {
url := i.systemStatus().build()
h := struct {
Body string `xml:"body"`
}{}
err := httpread.XML(i.client.getf(url), &h)
if err != nil {
return nil, errors.Wrap(err, "could not obtain raw system status data")
}
return boxDataParser{}.parse(h.Body), nil
}

func (i *internal) query() fritzURLBuilder {
return i.client.query().path(queryURI)
}

func (i *internal) inetStat() fritzURLBuilder {
return i.client.query().path(inetStatURI)
}

func (i *internal) systemStatus() fritzURLBuilder {
return i.client.query().path(systemStatusURI)
}
44 changes: 32 additions & 12 deletions fritz/api_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,32 @@ func TestInternalFritzAPI(t *testing.T) {
{testListLanDevices},
{testListLogs},
{testInetStats},
{testBoxInfo},
}
for _, testCase := range testCases {
t.Run(fmt.Sprintf("Test aha api %s", runtime.FuncForPC(reflect.ValueOf(testCase.tc).Pointer()).Name()), func(t *testing.T) {
srv := mock.New().Start()
defer srv.Close()

client, err := NewClient("../mock/client_config_template.json")
assert.NoError(t, err)
u, err := url.Parse(srv.Server.URL)
assert.NoError(t, err)
client.Config.Net.Protocol = u.Scheme
client.Config.Net.Host = u.Host

err = client.Login()
assert.NoError(t, err)

internal := NewInternal(client)
internal := setUpClient(t, srv)
assert.NotNil(t, internal)
testCase.tc(t, internal)
})
}
}

func setUpClient(t *testing.T, srv *mock.Fritz) Internal {
client, err := NewClient("../mock/client_config_template.json")
assert.NoError(t, err)
u, err := url.Parse(srv.Server.URL)
assert.NoError(t, err)
client.Config.Net.Protocol = u.Scheme
client.Config.Net.Host = u.Host
err = client.Login()
assert.NoError(t, err)
internal := NewInternal(client)
return internal
}

func testInetStats(t *testing.T, i Internal) {
_, err := i.InternetStats()
assert.NoError(t, err)
Expand All @@ -64,3 +67,20 @@ func testListLogs(t *testing.T, i Internal) {
assert.Len(t, m, 3)
}
}

func testBoxInfo(t *testing.T, i Internal) {
data, err := i.BoxInfo()
assert.NoError(t, err)
assert.NotZero(t, data)
}

// TestInternal_BoxInfo_WithError test the error path of BoxInfo.
func TestInternal_BoxInfo_WithError(t *testing.T) {
m := mock.New()
m.SystemStatus = "/path/to/nonexistent/file/jsafbjsabfjasb.html"
srv := m.Start()
defer srv.Close()
internal := setUpClient(t, srv)
_, err := internal.BoxInfo()
assert.Error(t, err)
}
113 changes: 113 additions & 0 deletions fritz/box_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package fritz

import (
"fmt"
"strconv"
"strings"

"github.com/bpicode/fritzctl/stringutils"
)

// BoxData contains runtime information of the FRITZ!Box.
// codebeat:disable[TOO_MANY_IVARS]
type BoxData struct {
Model Model
FirmwareVersion FirmwareVersion
Runtime Runtime
Hash string
Status string
}

// Model contains information about the type of the box.
type Model struct {
Name string
Annex string
Branding string
}

// FirmwareVersion represent the FRITZ!OS version.
type FirmwareVersion struct {
Image string
OsVersionMajor string
OsVersionMinor string
OsVersionRevision string
}

// Runtime contains data on how long the FRITZ!Box has been running.
type Runtime struct {
Hours uint64
Days uint64
Months uint64
Years uint64
Reboots uint64
}

// codebeat:enable[TOO_MANY_IVARS]

// String returns a textual representation of Model.
func (m Model) String() string {
return fmt.Sprintf("%s, ADSL standard '%s', branded as '%s'", m.Name, m.Annex,
stringutils.DefaultIfEmpty(m.Branding, "unknown"))
}

// String returns a textual representation of FirmwareVersion.
func (v FirmwareVersion) String() string {
return fmt.Sprintf("FRITZ!OS %s.%s (%s.%s.%s revision %s)", v.OsVersionMajor, v.OsVersionMinor,
v.Image, v.OsVersionMajor, v.OsVersionMinor, stringutils.DefaultIfEmpty(v.OsVersionRevision, "unknown"))
}

// String returns a textual representation of Runtime.
func (r Runtime) String() string {
return fmt.Sprintf("%d years, %d months, %d days, %d hours, %d reboots", r.Years, r.Months, r.Days, r.Hours, r.Reboots)
}

type boxDataParser struct {
}

func (p boxDataParser) parse(raw string) *BoxData {
trimmed := strings.TrimSpace(raw)
tokens := strings.Split(trimmed, "-")
var tenTokens [10]string
copy(tenTokens[:], tokens)
if len(tokens) > 9 {
tenTokens[9] = strings.Join(tokens[9:], "-")
}
return p.parseTokens(tenTokens)
}

func (p boxDataParser) parseTokens(tokens [10]string) *BoxData {
var data BoxData
data.Model = p.model(tokens[0], tokens[1], tokens[9])
data.Runtime = p.runningFor(tokens[2], tokens[3])
data.Hash = fmt.Sprintf("%s-%s", tokens[4], tokens[5])
data.Status = tokens[6]
data.FirmwareVersion = p.firmwareVersion(tokens[7], tokens[8])
return &data
}

func (p boxDataParser) runningFor(firstToken, secondToken string) Runtime {
var r Runtime
r.Hours, _ = strconv.ParseUint(firstToken[:2], 10, 64)
r.Days, _ = strconv.ParseUint(firstToken[2:4], 10, 64)
r.Months, _ = strconv.ParseUint(firstToken[4:], 10, 64)
r.Years, _ = strconv.ParseUint(secondToken[:2], 10, 64)
r.Reboots, _ = strconv.ParseUint(secondToken[2:], 10, 64)
return r

}

func (p boxDataParser) firmwareVersion(version, revision string) FirmwareVersion {
return FirmwareVersion{
Image: version[0:3],
OsVersionMajor: version[3:5],
OsVersionMinor: version[5:],
OsVersionRevision: revision,
}
}
func (p boxDataParser) model(name, annex, branding string) Model {
return Model{
Name: name,
Annex: annex,
Branding: branding,
}
}
39 changes: 39 additions & 0 deletions fritz/box_data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package fritz

import (
"fmt"
"testing"

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

func Test_boxDataParser_parse(t *testing.T) {
tests := []struct {
name string
arg string
}{
{name: "Fon ata 1020", arg: "FRITZ!Box Fon ata 1020-B-060102-040410-262336-724176-787902-110401-2956-avme-en"},
{name: "Fon WLAN UI", arg: "FRITZ!Box Fon WLAN (UI)-B-070608-050527-151063-152677-787902-080434-7804-1und1"},
{name: "Fon WLAN 7170", arg: "FRITZ!Box Fon WLAN 7170-B-171008-000028-457563-147110-787902-290487-19985-avm"},
{name: "Fon WLAN 7170", arg: "FRITZ!Box Fon WLAN 7170 (UI)-B-171607-041025-521747-370532-147902-290486-19138-1und1"},
{name: "Fon WLAN 7170", arg: "FRITZ!Box Fon WLAN 7170 Annex A-A-042602-030325-402401-042265-787902-580482"},
{name: "Fon WLAN 7270", arg: "FRITZ!Box Fon WLAN 7270-B-111700-030716-306463-160202-787902-540480-15918-avm"},
{name: "Fon WLAN 7270", arg: "FRITZ!Box Fon WLAN 7270 v2-B-071710-020026-055200-026256-147902-540504-20260-avme-en"},
{name: "Fon WLAN 7340", arg: "FRITZ!Box Fon WLAN 7340-B-072706-000217-533416-737311-147902-990505-20608-avme-en"},
{name: "Fon WLAN 7390", arg: "FRITZ!Box Fon WLAN 7390-B-161001-000109-670300-000324-787902-840507-21400-avm"},
{name: "Fon WLAN 7390", arg: "FRITZ!Box Fon WLAN 7390 (UI)-B-171408-010206-436146-332654-147902-840505-20359-1und1"},
{name: "Fon WLAN 7390", arg: "FRITZ!Box Fon WLAN 7390-A-091301-000006-002567-355146-787902-840505-20608-avme-de"},
{name: "Repeater NG", arg: "FRITZ!WLAN Repeater N/G-B-152108-020222-445034-614506-787902-680486-20648-avm"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := boxDataParser{}
data := p.parse(tt.arg)
assert.NotZero(t, data)
fmt.Println("Model", data.Model)
fmt.Println("Version", data.FirmwareVersion)
fmt.Println("Runtime", data.Runtime)
fmt.Println("----")
})
}
}
1 change: 1 addition & 0 deletions fritz/fritzapi_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
queryURI = "/query.lua"
inetStatURI = "/internet/inetstat_monitor.lua"
phoneListURI = "/fon_num/foncalls_list.lua"
systemStatusURI = "/cgi-bin/system_status"
)

type fritzURLBuilder interface {
Expand Down
Loading

0 comments on commit ca48642

Please sign in to comment.