mirror of
https://github.com/Xevion/go-ha.git
synced 2026-01-31 06:24:27 -06:00
websocket initialization done in App
This commit is contained in:
@@ -2,22 +2,48 @@ package gomeassistant
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/saml-dev/gome-assistant/internal/setup"
|
||||||
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type app struct {
|
type app struct {
|
||||||
url string
|
ctx context.Context
|
||||||
ctx context.Context
|
ctxCancel context.CancelFunc
|
||||||
schedules []Schedule
|
conn *websocket.Conn
|
||||||
listeners []Listener
|
schedules []schedule
|
||||||
|
entityListeners []entityListener
|
||||||
}
|
}
|
||||||
|
|
||||||
func App(url string) (app, error) {
|
/*
|
||||||
// TODO: connect to websocket, return error if fails
|
App establishes the websocket connection and returns an object
|
||||||
return app{url: url}, nil
|
you can use to register schedules and listeners.
|
||||||
|
*/
|
||||||
|
func App(connString string) (app, error) {
|
||||||
|
conn, ctx, ctxCancel, err := setup.SetupConnection(connString)
|
||||||
|
if err != nil {
|
||||||
|
return app{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app{
|
||||||
|
conn: conn,
|
||||||
|
ctx: ctx,
|
||||||
|
ctxCancel: ctxCancel,
|
||||||
|
schedules: []schedule{},
|
||||||
|
entityListeners: []entityListener{},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a app) RegisterSchedule(s Schedule) {
|
func (a *app) Cleanup() {
|
||||||
|
if a.ctxCancel != nil {
|
||||||
|
a.ctxCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *app) RegisterSchedule(s schedule) {
|
||||||
|
fmt.Println(a.schedules)
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
panic(s.err) // something wasn't configured properly when the schedule was built
|
panic(s.err) // something wasn't configured properly when the schedule was built
|
||||||
}
|
}
|
||||||
@@ -40,7 +66,8 @@ func (a app) RegisterSchedule(s Schedule) {
|
|||||||
startTime = startTime.Add(s.frequency)
|
startTime = startTime.Add(s.frequency)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: save realStartTime or _startTime to s, add to list of Schedules
|
s.realStartTime = startTime
|
||||||
|
a.schedules = append(a.schedules, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -50,105 +77,3 @@ const (
|
|||||||
Hourly time.Duration = time.Hour
|
Hourly time.Duration = time.Hour
|
||||||
Minutely time.Duration = time.Minute
|
Minutely time.Duration = time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
type Listener struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
_0000 string = "0000"
|
|
||||||
_0015 string = "0015"
|
|
||||||
_0030 string = "0030"
|
|
||||||
_0045 string = "0045"
|
|
||||||
_0100 string = "0100"
|
|
||||||
_0115 string = "0115"
|
|
||||||
_0130 string = "0130"
|
|
||||||
_0145 string = "0145"
|
|
||||||
_0200 string = "0200"
|
|
||||||
_0215 string = "0215"
|
|
||||||
_0230 string = "0230"
|
|
||||||
_0245 string = "0245"
|
|
||||||
_0300 string = "0300"
|
|
||||||
_0315 string = "0315"
|
|
||||||
_0330 string = "0330"
|
|
||||||
_0345 string = "0345"
|
|
||||||
_0400 string = "0400"
|
|
||||||
_0415 string = "0415"
|
|
||||||
_0430 string = "0430"
|
|
||||||
_0445 string = "0445"
|
|
||||||
_0500 string = "0500"
|
|
||||||
_0515 string = "0515"
|
|
||||||
_0530 string = "0530"
|
|
||||||
_0545 string = "0545"
|
|
||||||
_0600 string = "0600"
|
|
||||||
_0615 string = "0615"
|
|
||||||
_0630 string = "0630"
|
|
||||||
_0645 string = "0645"
|
|
||||||
_0700 string = "0700"
|
|
||||||
_0715 string = "0715"
|
|
||||||
_0730 string = "0730"
|
|
||||||
_0745 string = "0745"
|
|
||||||
_0800 string = "0800"
|
|
||||||
_0815 string = "0815"
|
|
||||||
_0830 string = "0830"
|
|
||||||
_0845 string = "0845"
|
|
||||||
_0900 string = "0900"
|
|
||||||
_0915 string = "0915"
|
|
||||||
_0930 string = "0930"
|
|
||||||
_0945 string = "0945"
|
|
||||||
_1000 string = "1000"
|
|
||||||
_1015 string = "1015"
|
|
||||||
_1030 string = "1030"
|
|
||||||
_1045 string = "1045"
|
|
||||||
_1100 string = "1100"
|
|
||||||
_1115 string = "1115"
|
|
||||||
_1130 string = "1130"
|
|
||||||
_1145 string = "1145"
|
|
||||||
_1200 string = "1200"
|
|
||||||
_1215 string = "1215"
|
|
||||||
_1230 string = "1230"
|
|
||||||
_1245 string = "1245"
|
|
||||||
_1300 string = "1300"
|
|
||||||
_1315 string = "1315"
|
|
||||||
_1330 string = "1330"
|
|
||||||
_1345 string = "1345"
|
|
||||||
_1400 string = "1400"
|
|
||||||
_1415 string = "1415"
|
|
||||||
_1430 string = "1430"
|
|
||||||
_1445 string = "1445"
|
|
||||||
_1500 string = "1500"
|
|
||||||
_1515 string = "1515"
|
|
||||||
_1530 string = "1530"
|
|
||||||
_1545 string = "1545"
|
|
||||||
_1600 string = "1600"
|
|
||||||
_1615 string = "1615"
|
|
||||||
_1630 string = "1630"
|
|
||||||
_1645 string = "1645"
|
|
||||||
_1700 string = "1700"
|
|
||||||
_1715 string = "1715"
|
|
||||||
_1730 string = "1730"
|
|
||||||
_1745 string = "1745"
|
|
||||||
_1800 string = "1800"
|
|
||||||
_1815 string = "1815"
|
|
||||||
_1830 string = "1830"
|
|
||||||
_1845 string = "1845"
|
|
||||||
_1900 string = "1900"
|
|
||||||
_1915 string = "1915"
|
|
||||||
_1930 string = "1930"
|
|
||||||
_1945 string = "1945"
|
|
||||||
_2000 string = "2000"
|
|
||||||
_2015 string = "2015"
|
|
||||||
_2030 string = "2030"
|
|
||||||
_2045 string = "2045"
|
|
||||||
_2100 string = "2100"
|
|
||||||
_2115 string = "2115"
|
|
||||||
_2130 string = "2130"
|
|
||||||
_2145 string = "2145"
|
|
||||||
_2200 string = "2200"
|
|
||||||
_2215 string = "2215"
|
|
||||||
_2230 string = "2230"
|
|
||||||
_2245 string = "2245"
|
|
||||||
_2300 string = "2300"
|
|
||||||
_2315 string = "2315"
|
|
||||||
_2330 string = "2330"
|
|
||||||
_2345 string = "2345"
|
|
||||||
)
|
|
||||||
|
|||||||
+6
-5
@@ -9,11 +9,14 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app, err := ga.App("192.168.86.67:8123")
|
app, err := ga.App("192.168.86.67:8123")
|
||||||
|
defer app.Cleanup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
fmt.Println(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := ga.ScheduleBuilder().Call(lightsOut).Daily().At(ga.HourMinute(22, 00)).Build()
|
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()
|
s2 := ga.ScheduleBuilder().Call(lightsOut).Every(time.Hour*4 + time.Minute*30).Offset(ga.HourMinute(1, 0)).Build()
|
||||||
app.RegisterSchedule(s2)
|
app.RegisterSchedule(s2)
|
||||||
// err = app.Start()
|
// err = app.Start()
|
||||||
|
|
||||||
@@ -23,9 +26,7 @@ func main() {
|
|||||||
OnlyBetween(ga.HourMinute(22, 00), ga.HourMinute(07, 00))
|
OnlyBetween(ga.HourMinute(22, 00), ga.HourMinute(07, 00))
|
||||||
fmt.Println(simpleListener)
|
fmt.Println(simpleListener)
|
||||||
|
|
||||||
// p := ga.NewPersonBuilder().Lives().At("lskdjflskf").WithPostalCode("kdjf").Works().As("SWE")
|
fmt.Println(s, "\n", s2)
|
||||||
|
|
||||||
fmt.Println(s, s2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func lightsOut(service ga.Service) {
|
func lightsOut(service ga.Service) {
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package gomeassistant
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/saml-dev/gome-assistant/internal/network"
|
|
||||||
"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)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// sched := Schedule{
|
|
||||||
// RunEvery: Daily,
|
|
||||||
// }
|
|
||||||
defer ctxCancel()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer conn.Close(websocket.StatusInternalError, "the sky is falling")
|
|
||||||
// _, _, err = c.Reader(ctx)
|
|
||||||
// if err != nil {
|
|
||||||
// fmt.Println("err1")
|
|
||||||
// fmt.Println(err)
|
|
||||||
// }
|
|
||||||
msg, err := network.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("err2")
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
fmt.Println(string(msg))
|
|
||||||
|
|
||||||
err = network.SendAuthMessage()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err = network.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
fmt.Println(string(msg))
|
|
||||||
err = network.WriteMessage(NewLightOnRequest("group.living_room_lamps"))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.Close(websocket.StatusNormalClosure, "")
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"nhooyr.io/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuthMessage struct {
|
|
||||||
MsgType string `json:"type"`
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = conn.Write(ctx, websocket.MessageText, msgJson)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = conn.Write(ctx, websocket.MessageText, msgJson)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadMessage(conn websocket.Conn, ctx context.Context) (string, error) {
|
|
||||||
_, msg, err := conn.Read(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(msg), nil
|
|
||||||
}
|
|
||||||
@@ -3,12 +3,12 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/saml-dev/gome-assistant/internal/network"
|
"github.com/saml-dev/gome-assistant/internal/setup"
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Light struct {
|
type Light struct {
|
||||||
conn websocket.Conn
|
conn *websocket.Conn
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,10 +41,10 @@ func LightOffRequest(entityId string) LightRequest {
|
|||||||
|
|
||||||
func (l Light) TurnOn(entityId string) {
|
func (l Light) TurnOn(entityId string) {
|
||||||
req := LightOnRequest(entityId)
|
req := LightOnRequest(entityId)
|
||||||
network.WriteMessage(req, l.conn, l.ctx)
|
setup.WriteMessage(req, l.conn, l.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l Light) TurnOff(entityId string) {
|
func (l Light) TurnOff(entityId string) {
|
||||||
req := LightOffRequest(entityId)
|
req := LightOffRequest(entityId)
|
||||||
network.WriteMessage(req, l.conn, l.ctx)
|
setup.WriteMessage(req, l.conn, l.ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthMessage struct {
|
||||||
|
MsgType string `json:"type"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.Write(ctx, websocket.MessageText, msgJson)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadMessage(conn *websocket.Conn, ctx context.Context) (string, error) {
|
||||||
|
_, msg, err := conn.Read(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(msg), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupConnection(connString string) (*websocket.Conn, context.Context, context.CancelFunc, error) {
|
||||||
|
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
// Init websocket connection
|
||||||
|
conn, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://%s/api/websocket", connString), nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ERROR: Failed to connect to websocket at ws://%s/api/websocket. Check IP address and port\n", connString)
|
||||||
|
ctxCancel()
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read auth_required message
|
||||||
|
_, err = ReadMessage(conn, ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctxCancel()
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send auth message
|
||||||
|
err = SendAuthMessage(conn, ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctxCancel()
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify auth message
|
||||||
|
err = VerifyAuthResponse(conn, ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("ERROR: Auth token is invalid. Please double check it or create a new token in your Home Assistant profile")
|
||||||
|
ctxCancel()
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, ctx, ctxCancel, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendAuthMessage(conn *websocket.Conn, ctx context.Context) error {
|
||||||
|
token := os.Getenv("AUTH_TOKEN")
|
||||||
|
err := WriteMessage(AuthMessage{MsgType: "auth", AccessToken: token}, conn, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type authResponse struct {
|
||||||
|
MsgType string `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyAuthResponse(conn *websocket.Conn, ctx context.Context) error {
|
||||||
|
_, msg, err := conn.Read(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var authResp authResponse
|
||||||
|
json.Unmarshal(msg, &authResp)
|
||||||
|
fmt.Println(authResp)
|
||||||
|
if authResp.MsgType != "auth_ok" {
|
||||||
|
return errors.New("invalid auth token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+29
-12
@@ -16,9 +16,13 @@ func HourMinute(Hour, Minute int) hourMinute {
|
|||||||
return hourMinute{Hour, Minute}
|
return hourMinute{Hour, Minute}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hm hourMinute) String() string {
|
||||||
|
return fmt.Sprintf("%02d:%02d", hm.Hour, hm.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
type scheduleCallback func(Service)
|
type scheduleCallback func(Service)
|
||||||
|
|
||||||
type Schedule struct {
|
type schedule struct {
|
||||||
/*
|
/*
|
||||||
frequency is a time.Duration representing how often you want to run your function.
|
frequency is a time.Duration representing how often you want to run your function.
|
||||||
|
|
||||||
@@ -47,35 +51,48 @@ type Schedule struct {
|
|||||||
This will be set rather than returning an error to avoid checking err for nil on every schedule :)
|
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.
|
RegisterSchedule will panic if the error is set.
|
||||||
*/
|
*/
|
||||||
err error
|
err error
|
||||||
|
realStartTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type scheduleBuilder struct {
|
type scheduleBuilder struct {
|
||||||
schedule Schedule
|
schedule schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
type scheduleBuilderCall struct {
|
type scheduleBuilderCall struct {
|
||||||
schedule Schedule
|
schedule schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
type scheduleBuilderDaily struct {
|
type scheduleBuilderDaily struct {
|
||||||
schedule Schedule
|
schedule schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
type scheduleBuilderCustom struct {
|
type scheduleBuilderCustom struct {
|
||||||
schedule Schedule
|
schedule schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
type scheduleBuilderEnd struct {
|
type scheduleBuilderEnd struct {
|
||||||
schedule Schedule
|
schedule schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
func ScheduleBuilder() scheduleBuilder {
|
func ScheduleBuilder() scheduleBuilder {
|
||||||
return scheduleBuilder{Schedule{}}
|
return scheduleBuilder{schedule{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Schedule) String() string {
|
func (s schedule) String() string {
|
||||||
return fmt.Sprintf("Run %q every %v with offset %s", getFunctionName(s.callback), s.frequency, s.offset)
|
return fmt.Sprintf("Run %q %s %s",
|
||||||
|
getFunctionName(s.callback),
|
||||||
|
frequencyToString(s.frequency),
|
||||||
|
s.offset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func frequencyToString(d time.Duration) string {
|
||||||
|
fmt.Println(d.Hours(), d.Minutes(), d.Seconds())
|
||||||
|
if d.Hours() == 24 {
|
||||||
|
return "daily at"
|
||||||
|
}
|
||||||
|
return "every " + d.String() + " with offset"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb scheduleBuilder) Call(callback scheduleCallback) scheduleBuilderCall {
|
func (sb scheduleBuilder) Call(callback scheduleCallback) scheduleBuilderCall {
|
||||||
@@ -103,11 +120,11 @@ func (sb scheduleBuilderCustom) Offset(o hourMinute) scheduleBuilderEnd {
|
|||||||
return scheduleBuilderEnd(sb)
|
return scheduleBuilderEnd(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb scheduleBuilderCustom) Build() Schedule {
|
func (sb scheduleBuilderCustom) Build() schedule {
|
||||||
return sb.schedule
|
return sb.schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb scheduleBuilderEnd) Build() Schedule {
|
func (sb scheduleBuilderEnd) Build() schedule {
|
||||||
return sb.schedule
|
return sb.schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user