Last active
July 3, 2024 02:02
-
-
Save houstonhaynes/823b992410fa5b0a5eab5dd705506da1 to your computer and use it in GitHub Desktop.
Modules for reading from the OpenWeatherMap API (BYOK)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Uncomment additional options as needed. | |
# To learn more about these config options, including custom application configuration settings, check out the Application Settings Configuration documentation. | |
# http://developer.wildernesslabs.co/Meadow/Meadow.OS/Configuration/Application_Settings_Configuration/ | |
# App lifecycle configuration. | |
Lifecycle: | |
# Control whether Meadow will restart when an unhandled app exception occurs. Combine with Lifecycle > AppFailureRestartDelaySeconds to control restart timing. | |
RestartOnAppFailure: false | |
# When app set to restart automatically on app failure, | |
AppFailureRestartDelaySeconds: 15 | |
# Logging configuration. | |
Logging: | |
# Adjust the level of logging detail. | |
LogLevel: | |
# Trace, Debug, Information, Warning, or Error | |
Default: Trace | |
# Meadow.Cloud configuration. | |
MeadowCloud: | |
# Enable Logging, Events, Command + Control | |
Enabled: false | |
# Enable Over-the-air Updates | |
EnableUpdates: false | |
# Enable Health Metrics | |
EnableHealthMetrics: false | |
# How often to send metrics to Meadow.Cloud | |
HealthMetricsIntervalMinutes: 60 | |
# These are custom values that are read via Secrets.fs | |
WeatherService: | |
Latitude: "YOUR_LATITUDE_AS_DECIMAL" | |
Longitude: "YOUR_LONGITUDE_AS_DECIMAL" | |
Api_Key: "YOUR_OPENWEATHERMAP_API_KEY" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Secrets | |
open Meadow | |
type AppSettingsCollector() = | |
inherit ConfigurableObject() | |
member this.GetConfiguredString(key: string) = | |
let settings = Resolver.App.Settings | |
if settings.ContainsKey(key) then | |
let value = settings.[key] | |
value | |
else | |
null | |
let LATITUDE = AppSettingsCollector().GetConfiguredString("WeatherService.Latitude") | |
let LONGITUDE = AppSettingsCollector().GetConfiguredString("WeatherService.Longitude") | |
let WEATHER_API_KEY = AppSettingsCollector().GetConfiguredString("WeatherService.Api_Key") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module WeatherReading | |
open System | |
type Coordinates = | |
{ | |
lon: float | |
lat: float | |
} | |
type Weather = | |
{ | |
id: int | |
main: string | |
description: string | |
icon: string | |
} | |
type MainCurrent = | |
{ | |
temp: float | |
feels_like: float | |
temp_min: float | |
temp_max: float | |
pressure: int | |
humidity: int | |
} | |
type Wind = | |
{ | |
speed: decimal | |
deg: int | |
gust: float option | |
} | |
type Clouds = | |
{ | |
all: int | |
} | |
type SystemCurrent = | |
{ | |
``type``: int | |
id: int | |
country: string | |
sunrise: int64 | |
sunset: int64 | |
} | |
type ExtendedSystemCurrent = | |
{ | |
``type``: int | |
id: int | |
country: string | |
sunrise: int64 | |
local_sunrise_dt: DateTime | |
sunset: int64 | |
local_sunset_dt: DateTime | |
} | |
type Current = | |
{ | |
coord: Coordinates | |
weather: Weather array | |
main: MainCurrent | |
visibility: int | |
wind: Wind | |
clouds: Clouds | |
dt: int | |
sys: SystemCurrent | |
timezone: int64 | |
id: int | |
name: string | |
cod: int | |
} | |
type ExtendedCurrentResponse = | |
{ | |
coord: Coordinates | |
weather: Weather array | |
main: MainCurrent | |
visibility: int | |
wind: Wind | |
clouds: Clouds | |
dt: int | |
local_dt: DateTime | |
sys: ExtendedSystemCurrent | |
timezone: int64 | |
id: int | |
name: string | |
cod: int | |
} | |
type MainForecast = | |
{ | |
temp: float | |
feels_like: float | |
temp_min: float | |
temp_max: float | |
pressure: int | |
sea_level: int | |
grnd_level: int | |
humidity: int | |
temp_kf: float | |
} | |
type CloudsForecast = | |
{ | |
all: int | |
} | |
type WindForecast = | |
{ | |
speed: float | |
deg: int | |
gust: float option | |
} | |
type SysForecast = | |
{ | |
pod: string | |
} | |
type ListForecast = | |
{ | |
dt: int64 | |
main: MainForecast | |
weather: Weather array | |
clouds: CloudsForecast | |
wind: WindForecast | |
visibility: int | |
pop: float | |
sys: SysForecast | |
dt_txt: string | |
} | |
type ExtendedForecastResponse = | |
{ | |
dt: int64 | |
forecast_local_dt: DateTime | |
main: MainForecast | |
weather: Weather array | |
clouds: CloudsForecast | |
wind: WindForecast | |
visibility: int | |
pop: float | |
sys: SysForecast | |
dt_txt: string | |
} | |
type ForecastResponse = | |
{ | |
cod: string | |
message: int | |
cnt: int | |
list: ListForecast array | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module WeatherService | |
open System | |
open System.Threading.Tasks | |
open System.Net.Http | |
open FSharp.Json | |
open Meadow | |
open WeatherReading | |
open Secrets | |
let epoch = DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) | |
let localDateTime (unixTimestamp : int64) = | |
epoch.AddSeconds(float unixTimestamp).ToLocalTime() | |
let weatherUri = "https://api.openweathermap.org/data/2.5/" | |
let printReading<'T> (reading: 'T) = | |
let properties = typeof<'T>.GetProperties() | |
for prop in properties do | |
if prop.PropertyType = typeof<Weather[]> then | |
let weatherArray = prop.GetValue(reading, null) :?> Weather[] | |
for weather in weatherArray do | |
printfn "weather:" | |
let weatherProperties = typeof<Weather>.GetProperties() | |
for weatherProp in weatherProperties do | |
let value = weatherProp.GetValue(weather, null) | |
printfn $" %s{weatherProp.Name}: {value}" | |
else | |
let value = prop.GetValue(reading, null) | |
printfn $"%s{prop.Name}: {value}" | |
let GetWeatherConditions() : Task<ExtendedCurrentResponse option> = | |
async { | |
use client = new HttpClient() | |
client.Timeout <- TimeSpan(0, 5, 0) | |
try | |
let! response = client.GetAsync (weatherUri + | |
"weather" + | |
"?lat=" + LATITUDE + | |
"&lon=" + LONGITUDE + | |
"&appid=" + WEATHER_API_KEY + | |
"&units=imperial") |> Async.AwaitTask | |
response.EnsureSuccessStatusCode() |> ignore | |
let! json = response.Content.ReadAsStringAsync() |> Async.AwaitTask | |
let values = Json.deserialize<Current>(json) | |
let reading_local_dt = localDateTime values.dt | |
let local_sunrise_dt = localDateTime values.sys.sunrise | |
let local_sunset_dt = localDateTime values.sys.sunset | |
let extendedValues = | |
{ coord = values.coord | |
weather = values.weather | |
main = values.main | |
visibility = values.visibility | |
wind = values.wind | |
clouds = values.clouds | |
dt = values.dt | |
local_dt = reading_local_dt | |
sys = { ``type`` = values.sys.``type`` | |
id = values.sys.id | |
country = values.sys.country | |
sunrise = values.sys.sunrise | |
local_sunrise_dt = local_sunrise_dt | |
sunset = values.sys.sunset | |
local_sunset_dt = local_sunset_dt } | |
timezone = values.timezone | |
id = values.id | |
name = values.name | |
cod = values.cod | |
} | |
Resolver.Log.Info("Current Weather Task ...") | |
printReading<ExtendedCurrentResponse> extendedValues | |
return Some extendedValues | |
with | |
| :? TaskCanceledException -> | |
Resolver.Log.Info("Conditions request timed out.") | |
return None | |
| e -> | |
Resolver.Log.Info $"Conditions request went sideways: %s{e.Message}" | |
return None | |
} |> Async.StartAsTask | |
let GetWeatherForecast() : Task<ExtendedForecastResponse option> = | |
async { | |
use client = new HttpClient() | |
client.Timeout <- TimeSpan(0, 5, 0) | |
try | |
let! response = client.GetAsync (weatherUri + | |
"forecast" + | |
"?lat=" + Secrets.LATITUDE + | |
"&lon=" + Secrets.LONGITUDE + | |
"&appid=" + Secrets.WEATHER_API_KEY + | |
"&units=imperial&cnt=2") |> Async.AwaitTask | |
response.EnsureSuccessStatusCode() |> ignore | |
let! json = response.Content.ReadAsStringAsync() |> Async.AwaitTask | |
let forecastResponse = Json.deserialize<ForecastResponse>(json) | |
if forecastResponse.list.Length > 1 then | |
let mutable values = forecastResponse.list.[0] | |
let forecast_local_dt = localDateTime values.dt | |
let current_local_dt = DateTime.Now | |
if Math.Abs((forecast_local_dt - current_local_dt).TotalHours) <= 1.0 then | |
Resolver.Log.Info("Using Second Forecast ...") | |
values <- forecastResponse.list.[1] | |
else | |
Resolver.Log.Info("Using First Forecast ...") | |
let extendedForecastValues = | |
{ dt = values.dt | |
forecast_local_dt = localDateTime values.dt | |
main = values.main | |
weather = values.weather | |
clouds = values.clouds | |
wind = values.wind | |
visibility = values.visibility | |
pop = values.pop | |
sys = { pod = values.sys.pod } | |
dt_txt = values.dt_txt | |
} | |
Resolver.Log.Info("Weather Forecast Task ...") | |
printReading<ExtendedForecastResponse> extendedForecastValues | |
return Some extendedForecastValues | |
else | |
Resolver.Log.Info("No forecast data available.") | |
return None | |
with | |
| :? TaskCanceledException -> | |
Resolver.Log.Info("Forecast request timed out.") | |
return None | |
| e -> | |
Resolver.Log.Info $"Forecast request went sideways: %s{e.Message}" | |
return None | |
} |> Async.StartAsTask | |
// This code is used to create geo-fenced areas for the Weather Triggers API | |
open CoordinateSharp | |
GlobalSettings.Default_CoordinateFormatOptions.Round = 6 |> ignore | |
type rawLatitude = float | |
type rawLongitude = float | |
type rightHandPoint = rawLongitude * rawLatitude | |
type GeoJson = { | |
``type``: string | |
coordinates: (float * float) list list | |
} | |
let createGeoJsonPolygon (coordinates: rightHandPoint list) = | |
let geoJson = { ``type`` = "Polygon"; coordinates = coordinates :: [] } | |
Json.serialize geoJson | |
let calculateNewGeoJsonPoint (point : GeoFence.Point, headingDeg: double, distanceKm: double) = | |
let newCoordinate = Coordinate(point.Latitude, point.Longitude) | |
newCoordinate.Move(distanceKm * 1000.0, headingDeg, Shape.Sphere) | |
let newRightHandPoint : rightHandPoint = newCoordinate.Longitude.DecimalDegree, newCoordinate.Latitude.DecimalDegree | |
newRightHandPoint | |
let createPieSlice (lat: float, lng: float, radiusKm: float, bearingDeg: float, angleDeg: float) = | |
let startPoint : rightHandPoint = (lng * 1e6 |> round |> fun x -> x / 1e6), (lat * 1e6 |> round |> fun x -> x / 1e6) | |
let mutable points : rightHandPoint list = [] | |
points <- startPoint :: points // Prepend startPoint to the list | |
let bearingMin = (bearingDeg - (angleDeg / 2.0)) % 360.0 | |
// Calculate and add intermediate points | |
for newBearing in [bearingMin..5.0..(bearingMin + angleDeg)] do | |
let newPoint = calculateNewGeoJsonPoint(GeoFence.Point(startPoint |> snd, startPoint |> fst), newBearing, radiusKm) | |
points <- newPoint :: points | |
points <- startPoint :: points // Postpend startPoint to the list | |
points |> createGeoJsonPolygon | |
let createCircle (lat: float, lng: float, radiusKm: float) = | |
let startPoint = GeoFence.Point((lat * 1e6 |> round |> fun x -> x / 1e6), (lng * 1e6 |> round |> fun x -> x / 1e6)) | |
|> fun point -> point.Longitude, point.Latitude | |
let mutable points : (float * float) list = [] | |
// Calculate and add points around a circular boundary from the origin | |
for newBearing in [0..5..360] do | |
let newPoint = calculateNewGeoJsonPoint(GeoFence.Point(startPoint |> snd, startPoint |> fst), newBearing, radiusKm) | |
points <- newPoint :: points | |
points |> createGeoJsonPolygon |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment