Skip to content

Commit

Permalink
Merge pull request #164 from calvinmclean/feature/ui-improve
Browse files Browse the repository at this point in the history
Improve UI
  • Loading branch information
calvinmclean authored Jun 15, 2024
2 parents d1b4196 + a30fd74 commit 3858e8d
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 245 deletions.
97 changes: 4 additions & 93 deletions garden-app/pkg/garden.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,16 @@ package pkg

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"time"

"github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb"
"github.com/calvinmclean/babyapi"
)

const (
// LightTimeFormat is used to control format of time fields
LightTimeFormat = "15:04:05-07:00"
)

const (
// LightStateOff is the value used to turn off a light
LightStateOff LightState = iota
// LightStateOn is the value used to turn on a light
LightStateOn
// LightStateToggle is the empty value that results in toggling
LightStateToggle
)

var (
stateToString = []string{"OFF", "ON", ""}
stringToState = map[string]LightState{
`"OFF"`: LightStateOff,
`OFF`: LightStateOff,
`"ON"`: LightStateOn,
`ON`: LightStateOn,
`""`: LightStateToggle,
``: LightStateToggle,
}
)

// LightState is an enum representing the state of a Light (ON or OFF)
type LightState int

// Return the string representation of this LightState
func (l LightState) String() string {
return stateToString[l]
}

// MarshalJSON will convert LightState into it's JSON string representation
func (l LightState) MarshalJSON() ([]byte, error) {
if int(l) >= len(stateToString) {
return []byte{}, fmt.Errorf("cannot convert %d to %T", int(l), l)
}
return json.Marshal(stateToString[l])
}

// UnmarshalJSON with convert LightState's JSON string representation, ignoring case, into a LightState
func (l *LightState) UnmarshalJSON(data []byte) error {
return l.unmarshal(data)
}

func (l *LightState) UnmarshalText(data []byte) error {
return l.unmarshal(data)
}

func (l *LightState) unmarshal(data []byte) error {
upper := strings.ToUpper(string(data))
var ok bool
*l, ok = stringToState[upper]
if !ok {
return fmt.Errorf("cannot unmarshal %s into Go value of type %T", string(data), l)
}
return nil
}

// Garden is the representation of a single garden-controller device
type Garden struct {
Name string `json:"name" yaml:"name,omitempty"`
Expand Down Expand Up @@ -103,32 +40,6 @@ type GardenHealth struct {
LastContact *time.Time `json:"last_contact,omitempty"`
}

// LightSchedule allows the user to control when the Garden light is turned on and off
// "Time" should be in the format of LightTimeFormat constant ("15:04:05-07:00")
type LightSchedule struct {
Duration *Duration `json:"duration" yaml:"duration"`
StartTime string `json:"start_time" yaml:"start_time"`
AdhocOnTime *time.Time `json:"adhoc_on_time,omitempty" yaml:"adhoc_on_time,omitempty"`
}

// String...
func (ls *LightSchedule) String() string {
return fmt.Sprintf("%+v", *ls)
}

// Patch allows modifying the struct in-place with values from a different instance
func (ls *LightSchedule) Patch(new *LightSchedule) {
if new.Duration != nil {
ls.Duration = new.Duration
}
if new.StartTime != "" {
ls.StartTime = new.StartTime
}
if new.AdhocOnTime == nil {
ls.AdhocOnTime = nil
}
}

// Health returns a GardenHealth struct after querying InfluxDB for the Garden controller's last contact time
func (g *Garden) Health(ctx context.Context, influxdbClient influxdb.Client) *GardenHealth {
lastContact, err := influxdbClient.GetLastContact(ctx, g.TopicPrefix)
Expand Down Expand Up @@ -265,9 +176,9 @@ func (g *Garden) Bind(r *http.Request) error {
return errors.New("missing required light_schedule.start_time field")
}
// Check that LightSchedule.StartTime is valid
_, err := time.Parse(LightTimeFormat, g.LightSchedule.StartTime)
_, err := g.LightSchedule.ParseStartTime()
if err != nil {
return fmt.Errorf("invalid time format for light_schedule.start_time: %s", g.LightSchedule.StartTime)
return err
}
}
case http.MethodPatch:
Expand All @@ -291,9 +202,9 @@ func (g *Garden) Bind(r *http.Request) error {
}
// Check that LightSchedule.StartTime is valid
if g.LightSchedule.StartTime != "" {
_, err := time.Parse(LightTimeFormat, g.LightSchedule.StartTime)
_, err := g.LightSchedule.ParseStartTime()
if err != nil {
return fmt.Errorf("invalid time format for light_schedule.start_time: %s", g.LightSchedule.StartTime)
return err
}
}
}
Expand Down
135 changes: 0 additions & 135 deletions garden-app/pkg/garden_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package pkg

import (
"context"
"encoding/json"
"errors"
"testing"
"time"
Expand Down Expand Up @@ -199,140 +198,6 @@ func TestGardenPatch(t *testing.T) {
})
}

func TestLightStateString(t *testing.T) {
tests := []struct {
name string
input LightState
expected string
}{
{
"ON",
LightStateOn,
"ON",
},
{
"OFF",
LightStateOff,
"OFF",
},
{
"Toggle",
LightStateToggle,
"",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.input.String() != tt.expected {
t.Errorf("Expected %v, but got: %v", tt.expected, tt.input)
}
})
}
}

func TestLightStateUnmarshalJSON(t *testing.T) {
tests := []struct {
name string
input string
expected LightState
}{
{
"ON",
`"ON"`,
LightStateOn,
},
{
"on",
`"on"`,
LightStateOn,
},
{
"OFF",
`"OFF"`,
LightStateOff,
},
{
"off",
`"off"`,
LightStateOff,
},
{
"Toggle",
`""`,
LightStateToggle,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var l LightState
err := json.Unmarshal([]byte(tt.input), &l)
if err != nil {
t.Errorf("Unexpected error when unmarshaling JSON: %v", err)
}
if l != tt.expected {
t.Errorf("Expected %v, but got: %v", tt.expected, l.String())
}
})
}

t.Run("InvalidInput", func(t *testing.T) {
var l LightState
err := json.Unmarshal([]byte(`"invalid"`), &l)
if err == nil {
t.Error("Expected error but got nil")
}
if err.Error() != `cannot unmarshal "invalid" into Go value of type *pkg.LightState` {
t.Errorf("Unexpected error string: %v", err)
}
})
}

func TestLightStateMarshal(t *testing.T) {
tests := []struct {
name string
input LightState
expected string
}{
{
"ON",
LightStateOn,
`"ON"`,
},
{
"OFF",
LightStateOff,
`"OFF"`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := json.Marshal(tt.input)
if err != nil {
t.Errorf("Unexpected error when marshaling JSON: %v", err)
}
if string(result) != tt.expected {
t.Errorf("Expected %v, but got: %s", tt.expected, string(result))
}
})
}

t.Run("InvalidLightState", func(t *testing.T) {
result, err := json.Marshal(LightState(3))
if err == nil {
t.Error("Expected error but got nil")
}
if err.Error() != `json: error calling MarshalJSON for type pkg.LightState: cannot convert 3 to pkg.LightState` {
t.Errorf("Unexpected error string: %v", err)
}
if string(result) != "" {
t.Errorf("Expected empty string but got: %v", string(result))
}
})
}

func TestHasTemperatureHumiditySensor(t *testing.T) {
trueBool := true
falseBool := false
Expand Down
104 changes: 104 additions & 0 deletions garden-app/pkg/light_schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package pkg

import (
"encoding/json"
"fmt"
"strings"
"time"
)

const (
// LightTimeFormat is used to control format of time fields
LightTimeFormat = "15:04:05-07:00"
)

const (
// LightStateOff is the value used to turn off a light
LightStateOff LightState = iota
// LightStateOn is the value used to turn on a light
LightStateOn
// LightStateToggle is the empty value that results in toggling
LightStateToggle
)

var (
stateToString = []string{"OFF", "ON", ""}
stringToState = map[string]LightState{
`"OFF"`: LightStateOff,
`OFF`: LightStateOff,
`"ON"`: LightStateOn,
`ON`: LightStateOn,
`""`: LightStateToggle,
``: LightStateToggle,
}
)

// LightState is an enum representing the state of a Light (ON or OFF)
type LightState int

// Return the string representation of this LightState
func (l LightState) String() string {
return stateToString[l]
}

// MarshalJSON will convert LightState into it's JSON string representation
func (l LightState) MarshalJSON() ([]byte, error) {
if int(l) >= len(stateToString) {
return []byte{}, fmt.Errorf("cannot convert %d to %T", int(l), l)
}
return json.Marshal(stateToString[l])
}

// UnmarshalJSON with convert LightState's JSON string representation, ignoring case, into a LightState
func (l *LightState) UnmarshalJSON(data []byte) error {
return l.unmarshal(data)
}

func (l *LightState) UnmarshalText(data []byte) error {
return l.unmarshal(data)
}

func (l *LightState) unmarshal(data []byte) error {
upper := strings.ToUpper(string(data))
var ok bool
*l, ok = stringToState[upper]
if !ok {
return fmt.Errorf("cannot unmarshal %s into Go value of type %T", string(data), l)
}
return nil
}

// LightSchedule allows the user to control when the Garden light is turned on and off
// "Time" should be in the format of LightTimeFormat constant ("15:04:05-07:00")
type LightSchedule struct {
Duration *Duration `json:"duration" yaml:"duration"`
StartTime string `json:"start_time" yaml:"start_time"`
AdhocOnTime *time.Time `json:"adhoc_on_time,omitempty" yaml:"adhoc_on_time,omitempty"`
}

// String...
func (ls *LightSchedule) String() string {
return fmt.Sprintf("%+v", *ls)
}

// Patch allows modifying the struct in-place with values from a different instance
func (ls *LightSchedule) Patch(new *LightSchedule) {
if new.Duration != nil {
ls.Duration = new.Duration
}
if new.StartTime != "" {
ls.StartTime = new.StartTime
}
if new.AdhocOnTime == nil {
ls.AdhocOnTime = nil
}
}

func (ls *LightSchedule) ParseStartTime() (time.Time, error) {
startTime, err := time.Parse(LightTimeFormat, ls.StartTime)
if err != nil {
return time.Time{}, fmt.Errorf("invalid time format for light_schedule.start_time: %s", ls.StartTime)
}

return startTime, nil
}
Loading

0 comments on commit 3858e8d

Please sign in to comment.