diff --git a/app.go b/app.go index 5b936b1..2fc5eda 100644 --- a/app.go +++ b/app.go @@ -1,43 +1,50 @@ -package main +package gomeassistant -import "time" +import ( + "context" + "time" +) -type App struct { - Name string - Schedules []Schedule - Listeners []Listener +type app struct { + url string + ctx context.Context + schedules []Schedule + listeners []Listener } -type Schedule struct { - /* - RunEvery is a time.Duration representing how often you want to run your function. +func App(url string) (app, error) { + // TODO: connect to websocket, return error if fails + return app{url: url}, nil +} - Some examples: - time.Second * 5 // runs every 5 seconds at 00:00:00, 00:00:05, etc. - time.Hour * 12 // runs at BaseTime, +12 hours, +24 hours, etc. - gomeassistant.Daily // runs at BaseTime, +24 hours, +48 hours, etc. Daily is a const helper for time.Hour * 24 - // Helpers include Daily, Hourly, Minutely - */ - RunEvery time.Duration - /* Callback is the function you want to run. Takes zero arguments. */ - Callback func() - /* - BaseTime is 4 character string representing hours and minutes - in a 24-hr format. - It is the base that your RunEvery will be added to. - Defaults to "0000" (which is probably fine for most cases). +func (a app) RegisterSchedule(s Schedule) { + if s.err != nil { + panic(s.err) // something wasn't configured properly when the schedule was built + } - Example: Run in the 3rd minute of every hour. - Schedule{ - RunEvery: gomeassistant.Hourly // helper const for time.Hour - BaseTime: "0003" - } - */ - BaseTime string + if s.frequency == 0 { + panic("A schedule must call either Daily() or Every() when built.") + } + + now := time.Now() + startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) // start at midnight today + + // apply offset if set + if s.offset.Hour != 0 || s.offset.Minute != 0 { + startTime.Add(time.Hour * time.Duration(s.offset.Hour)) + startTime.Add(time.Minute * time.Duration(s.offset.Minute)) + } + + // advance first scheduled time by frequency until it is in the future + for startTime.Before(now) { + startTime = startTime.Add(s.frequency) + } + + // TODO: save realStartTime or _startTime to s, add to list of Schedules } const ( - RunEveryMissing time.Duration = 0 + FrequencyMissing time.Duration = 0 Daily time.Duration = time.Hour * 24 Hourly time.Duration = time.Hour diff --git a/cmd/main/testing.go b/cmd/main/testing.go new file mode 100644 index 0000000..26e5e0c --- /dev/null +++ b/cmd/main/testing.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "time" + + ga "github.com/saml-dev/gome-assistant" +) + +func main() { + app, err := ga.App("192.168.86.67:8123") + if err != nil { + panic(err) + } + s := ga.ScheduleBuilder().Call(lightsOut).Daily().At(ga.HourMinute(22, 00)).Build() + s2 := ga.ScheduleBuilder().Call(lightsOut).Every(time.Hour * 4).Offset(ga.HourMinute(1, 0)).Build() + app.RegisterSchedule(s2) + // err = app.Start() + + simpleListener := ga.EntityListenerBuilder(). + EntityId("light.lights"). + Call(cool). + OnlyBetween(ga.HourMinute(22, 00), ga.HourMinute(07, 00)) + fmt.Println(simpleListener) + + // p := ga.NewPersonBuilder().Lives().At("lskdjflskf").WithPostalCode("kdjf").Works().As("SWE") + + fmt.Println(s, s2) +} + +func lightsOut(service ga.Service) { + // ga.TurnOff("light.all_lights") +} + +func cool(service ga.Service, data ga.Data) { + service.Light.TurnOn("light.entryway_lamp") +} diff --git a/entitylistener.go b/entitylistener.go new file mode 100644 index 0000000..bddf492 --- /dev/null +++ b/entitylistener.go @@ -0,0 +1,56 @@ +package gomeassistant + +type entityListener struct { + entityId string + callback entityListenerCallback + fromState string + toState string + betweenStart hourMinute + betweenEnd hourMinute +} + +type entityListenerCallback func(Service, Data) + +type Data struct{} + +func EntityListenerBuilder() elBuilder1 { + return elBuilder1{entityListener{}} +} + +type elBuilder1 struct { + entityListener +} + +func (b elBuilder1) EntityId(eid string) elBuilder2 { + b.entityListener.entityId = eid + return elBuilder2(b) +} + +type elBuilder2 struct { + entityListener +} + +func (b elBuilder2) Call(callback entityListenerCallback) elBuilder3 { + b.entityListener.callback = callback + return elBuilder3(b) +} + +type elBuilder3 struct { + entityListener +} + +func (b elBuilder3) OnlyBetween(start hourMinute, end hourMinute) elBuilder3 { + b.entityListener.betweenStart = start + b.entityListener.betweenEnd = end + return b +} + +func (b elBuilder3) FromState(s string) elBuilder3 { + b.entityListener.fromState = s + return b +} + +func (b elBuilder3) ToState(s string) elBuilder3 { + b.entityListener.toState = s + return b +} diff --git a/gomeassistant.go b/gomeassistant.go index 5d49852..d34999e 100644 --- a/gomeassistant.go +++ b/gomeassistant.go @@ -1,4 +1,4 @@ -package main +package gomeassistant import ( "context" @@ -9,49 +9,14 @@ import ( "nhooyr.io/websocket" ) -var c = context.Background() -var b, x = context.WithTimeout(c, time.Second) - -const a = time.April - var ctx, ctxCancel = context.WithTimeout(context.Background(), time.Second*5) + var conn, _, err = websocket.Dial(ctx, "ws://192.168.86.67:8123/api/websocket", nil) -type Light struct { - EntityId string -} - -type LightOnRequest struct { - Id int `json:"id"` - Type string `json:"type"` - Domain string `json:"domain"` - Service string `json:"service"` - Target struct { - EntityId string `json:"entity_id"` - } `json:"target"` -} - -func NewLightOnRequest(entity_id string) LightOnRequest { - req := LightOnRequest{ - Id: 5, - Type: "call_service", - Domain: "light", - Service: "turn_on", - } - req.Target.EntityId = entity_id - return req -} - -func (l *Light) TurnOn() error { - // req := json.Marshal() - return nil -} - func main() { - - sched := Schedule{ - RunEvery: Daily, - } + // sched := Schedule{ + // RunEvery: Daily, + // } defer ctxCancel() if err != nil { panic(err) diff --git a/internal/network/light/light.go b/internal/network/light/light.go deleted file mode 100644 index 4d46ccd..0000000 --- a/internal/network/light/light.go +++ /dev/null @@ -1,4 +0,0 @@ -package light - -type Light struct { -} diff --git a/internal/network/service.go b/internal/network/service.go deleted file mode 100644 index 1ae2e9d..0000000 --- a/internal/network/service.go +++ /dev/null @@ -1 +0,0 @@ -package network diff --git a/internal/network/setup.go b/internal/network/setup.go index 740e00b..eba6f8b 100644 --- a/internal/network/setup.go +++ b/internal/network/setup.go @@ -5,20 +5,16 @@ import ( "encoding/json" "fmt" "os" - "time" "nhooyr.io/websocket" ) -var ctx, ctxCancel = context.WithTimeout(context.Background(), time.Second*5) -var conn, _, err = websocket.Dial(ctx, "ws://192.168.86.67:8123/api/websocket", nil) - type AuthMessage struct { MsgType string `json:"type"` AccessToken string `json:"access_token"` } -func SendAuthMessage() error { +func SendAuthMessage(conn websocket.Conn, ctx context.Context) error { token := os.Getenv("AUTH_TOKEN") msgJson, err := json.Marshal(AuthMessage{MsgType: "auth", AccessToken: token}) if err != nil { @@ -31,7 +27,7 @@ func SendAuthMessage() error { return nil } -func WriteMessage[T any](msg T) error { +func WriteMessage[T any](msg T, conn websocket.Conn, ctx context.Context) error { msgJson, err := json.Marshal(msg) fmt.Println(string(msgJson)) if err != nil { @@ -46,7 +42,7 @@ func WriteMessage[T any](msg T) error { return nil } -func ReadMessage() (string, error) { +func ReadMessage(conn websocket.Conn, ctx context.Context) (string, error) { _, msg, err := conn.Read(ctx) if err != nil { return "", err diff --git a/internal/services/light.go b/internal/services/light.go new file mode 100644 index 0000000..2800a27 --- /dev/null +++ b/internal/services/light.go @@ -0,0 +1,50 @@ +package services + +import ( + "context" + + "github.com/saml-dev/gome-assistant/internal/network" + "nhooyr.io/websocket" +) + +type Light struct { + conn websocket.Conn + ctx context.Context +} + +type LightRequest struct { + Id int `json:"id"` + Type string `json:"type"` + Domain string `json:"domain"` + Service string `json:"service"` + Target struct { + EntityId string `json:"entity_id"` + } `json:"target"` +} + +func LightOnRequest(entityId string) LightRequest { + req := LightRequest{ + Id: 5, + Type: "call_service", + Domain: "light", + Service: "turn_on", + } + req.Target.EntityId = entityId + return req +} + +func LightOffRequest(entityId string) LightRequest { + req := LightOnRequest(entityId) + req.Service = "turn_off" + return req +} + +func (l Light) TurnOn(entityId string) { + req := LightOnRequest(entityId) + network.WriteMessage(req, l.conn, l.ctx) +} + +func (l Light) TurnOff(entityId string) { + req := LightOffRequest(entityId) + network.WriteMessage(req, l.conn, l.ctx) +} diff --git a/person.go b/person.go new file mode 100644 index 0000000..fcf9bbe --- /dev/null +++ b/person.go @@ -0,0 +1,100 @@ +package gomeassistant + +import "fmt" + +type Person struct { + // Personal details + name, address, pin string + // Job details + workAddress, company, position string + salary int +} + +// PersonBuilder struct +type PersonBuilder struct { + person *Person +} + +// PersonAddressBuilder facet of PersonBuilder +type PersonAddressBuilder struct { + PersonBuilder +} + +// PersonJobBuilder facet of PersonBuilder +type PersonJobBuilder struct { + PersonBuilder +} + +// NewPersonBuilder constructor for PersonBuilder +func NewPersonBuilder() *PersonBuilder { + return &PersonBuilder{person: &Person{}} +} + +// Lives chains to type *PersonBuilder and returns a *PersonAddressBuilder +func (b *PersonBuilder) Lives() *PersonAddressBuilder { + return &PersonAddressBuilder{*b} +} + +// Works chains to type *PersonBuilder and returns a *PersonJobBuilder +func (b *PersonBuilder) Works() *PersonJobBuilder { + return &PersonJobBuilder{*b} +} + +// At adds address to person +func (a *PersonAddressBuilder) At(address string) *PersonAddressBuilder { + a.person.address = address + return a +} + +// WithPostalCode adds postal code to person +func (a *PersonAddressBuilder) WithPostalCode(pin string) *PersonAddressBuilder { + a.person.pin = pin + return a +} + +// As adds position to person +func (j *PersonJobBuilder) As(position string) *PersonJobBuilder { + j.person.position = position + return j +} + +// For adds company to person +func (j *PersonJobBuilder) For(company string) *PersonJobBuilder { + j.person.company = company + return j +} + +// In adds company address to person +func (j *PersonJobBuilder) In(companyAddress string) *PersonJobBuilder { + j.person.workAddress = companyAddress + return j +} + +// WithSalary adds salary to person +func (j *PersonJobBuilder) WithSalary(salary int) *PersonJobBuilder { + j.person.salary = salary + return j +} + +// Build builds a person from PersonBuilder +func (b *PersonBuilder) Build() *Person { + return b.person +} + +// RunBuilderFacet example +func RunBuilderFacet() { + pb := NewPersonBuilder() + pb. + Lives(). + At("Bangalore"). + WithPostalCode("560102"). + Works(). + As("Software Engineer"). + For("IBM"). + In("Bangalore"). + WithSalary(150000) + + person := pb.Build() + + fmt.Println(person) +} diff --git a/schedule.go b/schedule.go new file mode 100644 index 0000000..96a4c10 --- /dev/null +++ b/schedule.go @@ -0,0 +1,116 @@ +package gomeassistant + +import ( + "fmt" + "reflect" + "runtime" + "time" +) + +type hourMinute struct { + Hour int + Minute int +} + +func HourMinute(Hour, Minute int) hourMinute { + return hourMinute{Hour, Minute} +} + +type scheduleCallback func(Service) + +type Schedule struct { + /* + frequency is a time.Duration representing how often you want to run your function. + + Some examples: + time.Second * 5 // runs every 5 seconds at 00:00:00, 00:00:05, etc. + time.Hour * 12 // runs at offset, +12 hours, +24 hours, etc. + gomeassistant.Daily // runs at offset, +24 hours, +48 hours, etc. Daily is a const helper for time.Hour * 24 + // Helpers include Daily, Hourly, Minutely + */ + frequency time.Duration + callback scheduleCallback + /* + offset is 4 character string representing hours and minutes + in a 24-hr format. + It is the base that your frequency will be added to. + Defaults to "0000" (which is probably fine for most cases). + + Example: Run in the 3rd minute of every hour. + Schedule{ + frequency: gomeassistant.Hourly // helper const for time.Hour + offset: "0003" + } + */ + offset hourMinute + /* + This will be set rather than returning an error to avoid checking err for nil on every schedule :) + RegisterSchedule will panic if the error is set. + */ + err error +} + +type scheduleBuilder struct { + schedule Schedule +} + +type scheduleBuilderCall struct { + schedule Schedule +} + +type scheduleBuilderDaily struct { + schedule Schedule +} + +type scheduleBuilderCustom struct { + schedule Schedule +} + +type scheduleBuilderEnd struct { + schedule Schedule +} + +func ScheduleBuilder() scheduleBuilder { + return scheduleBuilder{Schedule{}} +} + +func (s Schedule) String() string { + return fmt.Sprintf("Run %q every %v with offset %s", getFunctionName(s.callback), s.frequency, s.offset) +} + +func (sb scheduleBuilder) Call(callback scheduleCallback) scheduleBuilderCall { + sb.schedule.callback = callback + return scheduleBuilderCall(sb) +} + +func (sb scheduleBuilderCall) Daily() scheduleBuilderDaily { + sb.schedule.frequency = time.Hour * 24 + return scheduleBuilderDaily(sb) +} + +func (sb scheduleBuilderDaily) At(t hourMinute) scheduleBuilderEnd { + sb.schedule.offset = t + return scheduleBuilderEnd(sb) +} + +func (sb scheduleBuilderCall) Every(d time.Duration) scheduleBuilderCustom { + sb.schedule.frequency = d + return scheduleBuilderCustom(sb) +} + +func (sb scheduleBuilderCustom) Offset(o hourMinute) scheduleBuilderEnd { + sb.schedule.offset = o + return scheduleBuilderEnd(sb) +} + +func (sb scheduleBuilderCustom) Build() Schedule { + return sb.schedule +} + +func (sb scheduleBuilderEnd) Build() Schedule { + return sb.schedule +} + +func getFunctionName(i interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() +} diff --git a/service.go b/service.go new file mode 100644 index 0000000..ee5a72d --- /dev/null +++ b/service.go @@ -0,0 +1,23 @@ +package gomeassistant + +import ( + "context" + + "github.com/saml-dev/gome-assistant/internal/services" + "nhooyr.io/websocket" +) + +type Service struct { + HomeAssistant homeAssistant + Light services.Light +} + +type homeAssistant struct { + conn websocket.Conn + ctx context.Context +} + +// type light struct { +// conn websocket.Conn +// ctx context.Context +// }