diff --git a/docs/modules.md b/docs/modules.md index c33db9bc4a..370cfe2d96 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -116,3 +116,16 @@ The schema module provides configuration options for your app. See the [schema d Example: See [examples/schema_hello_world.star](../examples/schema_hello_world.star) for an example. + +## Pixlet module: Sunrise + +The `sunrise` module calculates sunrise and sunset times for a given set of GPS coordinates and timestamp. + +| Function | Description | +| --- | --- | +| `sunrise(lat, lng, date)` | Calculates the sunrise time for a given location and date. | +| `sunset(lat, lng, date)` | Calculates the sunset time for a given location and date. | + +Example: + +See [examples/sunrise.star](../examples/sunrise.star) for an example. \ No newline at end of file diff --git a/examples/sunrise.star b/examples/sunrise.star new file mode 100644 index 0000000000..289392cf95 --- /dev/null +++ b/examples/sunrise.star @@ -0,0 +1,65 @@ +load("render.star", "render") +load("schema.star", "schema") +load("time.star", "time") +load("encoding/json.star", "json") +load("sunrise.star", "sunrise") + +DEFAULT_LOCATION = """ +{ + "lat": "40.6781784", + "lng": "-73.9441579", + "description": "Brooklyn, NY, USA", + "locality": "Brooklyn", + "place_id": "ChIJCSF8lBZEwokRhngABHRcdoI", + "timezone": "America/New_York" +} +""" + +def main(config): + location = config.get("location", DEFAULT_LOCATION) + loc = json.decode(location) + lat, lng = float(loc["lat"]), float(loc["lng"]) + + now = time.now() + rise = sunrise.sunrise(lat, lng, now) + set = sunrise.sunset(lat, lng, now) + + # Check if the sun does not rise or set today. This would happen if the + # location of the deivce is close to the north or south pole where there are + # many days of light or darkness. Maybe someone brought their Tidbyt to the + # Amundsen-Scott South Pole Station! How cool would that be? + if rise == None or set == None: + return render.Root( + child = render.Column( + children = [ + render.Text("Now: %s" % now.in_location(loc["timezone"]).format("3:04 PM")), + render.Marquee( + width = 64, + child = render.Text("Sun doesn't rise or set today."), + ), + ], + ), + ) + + return render.Root( + child = render.Column( + children = [ + render.Text("Now: %s" % now.in_location(loc["timezone"]).format("3:04 PM")), + render.Text("Rise: %s" % rise.in_location(loc["timezone"]).format("3:04 PM")), + render.Text("Set: %s" % set.in_location(loc["timezone"]).format("3:04 PM")), + ], + ), + ) + +def get_schema(): + return schema.Schema( + version = "1", + fields = [ + schema.Location( + id = "location", + name = "Location", + desc = "Location for which to display time.", + icon = "place", + ), + ], + ) diff --git a/go.mod b/go.mod index 0de7c10feb..4a3b7d13e7 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/harukasan/go-libwebp v0.0.0-20190703060927-68562c9c99af github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mitchellh/hashstructure/v2 v2.0.2 + github.com/nathan-osman/go-sunrise v1.0.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/pkg/errors v0.9.1 github.com/qri-io/starlib v0.5.1-0.20211102160121-ae835e29cd41 diff --git a/go.sum b/go.sum index 3ae3a7414a..761106d071 100644 --- a/go.sum +++ b/go.sum @@ -354,6 +354,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nathan-osman/go-sunrise v1.0.0 h1:mvjoVmXjmiHDSwKRA5t4T/1rNuHQDhodfQoxrUU39ck= +github.com/nathan-osman/go-sunrise v1.0.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp78RSjSWxiDowmlM= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= diff --git a/runtime/modules/sunrise/sunrise.go b/runtime/modules/sunrise/sunrise.go new file mode 100644 index 0000000000..cedacf4baf --- /dev/null +++ b/runtime/modules/sunrise/sunrise.go @@ -0,0 +1,94 @@ +package sunrise + +import ( + "fmt" + "sync" + "time" + + gosunrise "github.com/nathan-osman/go-sunrise" + startime "go.starlark.net/lib/time" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +const ( + ModuleName = "sunrise" +) + +var ( + once sync.Once + module starlark.StringDict + empty time.Time +) + +func LoadModule() (starlark.StringDict, error) { + once.Do(func() { + module = starlark.StringDict{ + ModuleName: &starlarkstruct.Module{ + Name: ModuleName, + Members: starlark.StringDict{ + "sunrise": starlark.NewBuiltin("sunrise", sunrise), + "sunset": starlark.NewBuiltin("sunset", sunset), + }, + }, + } + }) + + return module, nil +} + +func sunrise(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var ( + starLat starlark.Float + starLng starlark.Float + starDate startime.Time + ) + + if err := starlark.UnpackArgs( + "sunrise", + args, kwargs, + "lat", &starLat, + "lng", &starLng, + "date", &starDate, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for sunrise: %s", err) + } + + lat := float64(starLat) + lng := float64(starLng) + date := time.Time(starDate) + rise, _ := gosunrise.SunriseSunset(lat, lng, date.Year(), date.Month(), date.Day()) + if rise == empty { + return starlark.None, nil + } + + return startime.Time(rise), nil +} + +func sunset(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var ( + starLat starlark.Float + starLng starlark.Float + starDate startime.Time + ) + + if err := starlark.UnpackArgs( + "sunset", + args, kwargs, + "lat", &starLat, + "lng", &starLng, + "date", &starDate, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for sunset: %s", err) + } + + lat := float64(starLat) + lng := float64(starLng) + date := time.Time(starDate) + _, set := gosunrise.SunriseSunset(lat, lng, date.Year(), date.Month(), date.Day()) + if set == empty { + return starlark.None, nil + } + + return startime.Time(set), nil +} diff --git a/runtime/modules/sunrise/sunrise_test.go b/runtime/modules/sunrise/sunrise_test.go new file mode 100644 index 0000000000..ed53ff2f1b --- /dev/null +++ b/runtime/modules/sunrise/sunrise_test.go @@ -0,0 +1,47 @@ +package sunrise_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "tidbyt.dev/pixlet/runtime" +) + +var sunSource = ` +load("time.star", "time") +load("sunrise.star", "sunrise") + +def assert(success, message=None): + if not success: + fail(message or "assertion failed") + + +# Setup. +format = "2006-01-02T15:04:05" +input = time.parse_time("2022-01-15T22:40:24", format = format) +expectedRise = time.parse_time("2022-01-15T12:17:29", format = format) +expectedSet = time.parse_time("2022-01-15T21:52:30", format = format) +lat = 40.6781784 +lng = -73.9441579 + +# Call methods. +rise = sunrise.sunrise(lat, lng, input) +set = sunrise.sunset(lat, lng, input) + +# Assert. +assert(rise == expectedRise) +assert(set == expectedSet) + +def main(): + return [] +` + +func TestSunrise(t *testing.T) { + app := &runtime.Applet{} + err := app.Load("sun.star", []byte(sunSource), nil) + assert.NoError(t, err) + + screens, err := app.Run(map[string]string{}) + assert.NoError(t, err) + assert.NotNil(t, screens) +} diff --git a/runtime/runtime.go b/runtime/runtime.go index aaeb921d6e..446de2a4a4 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -20,6 +20,7 @@ import ( "go.starlark.net/starlarkstruct" "tidbyt.dev/pixlet/render" + "tidbyt.dev/pixlet/runtime/modules/sunrise" "tidbyt.dev/pixlet/schema" "tidbyt.dev/pixlet/starlarkutil" ) @@ -321,6 +322,9 @@ func (a *Applet) loadModule(thread *starlark.Thread, module string) (starlark.St case "re.star": return starlibre.LoadModule() + case "sunrise.star": + return sunrise.LoadModule() + case "time.star": return starlark.StringDict{ starlibtime.Module.Name: starlibtime.Module,