Skip to content

Instantly share code, notes, and snippets.

@houstonhaynes
Last active June 28, 2024 01:22
Show Gist options
  • Save houstonhaynes/823b992410fa5b0a5eab5dd705506da1 to your computer and use it in GitHub Desktop.
Save houstonhaynes/823b992410fa5b0a5eab5dd705506da1 to your computer and use it in GitHub Desktop.
Modules for reading from the OpenWeatherMap API (BYOK)
# 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: "35.60095"
Longitude: "-82.55402"
Api_Key: "YOUR_OPENWEATHERMAP_API_KEY"
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")
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
}
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 an east-facing half-oval centered on the specified latitude and longitude
open CoordinateSharp
let getLatLon (coord: Coordinate) =
let latitude = coord.Latitude.DecimalDegree * 1e6 |> round |> fun x -> x / 1e6
let longitude = coord.Longitude.DecimalDegree * 1e6 |> round |> fun x -> x / 1e6
(latitude, longitude)
let createHalfCircle (latitude: float, longitude: float, radius: float) =
let originalCenter = Coordinate(latitude, longitude)
let center = Coordinate(latitude, longitude)
center.Move((radius * 1000.0), 0, Shape.Sphere)
let gd = GeoFence.Drawer(center, Shape.Sphere, 280)
let points = [getLatLon originalCenter] // add original center as the first point
for i in 0..5 do
let angle = (i - 30 + 360) % 360
gd.Draw(Distance(radius/2.0), angle)
gd.Close()
let points = points @ (gd.Points |> List.ofSeq |> List.map getLatLon) // add the rest of the points
let points = points |> List.take (List.length points - 1) // remove the last point
let points = points @ [getLatLon originalCenter] // add original center as the last point
points
// |> List.map (fun (lat, lon) -> $"{lat},{lon},")
// |> String.concat "\n"
// This code is used to create an east-facing cone centered on the specified latitude and longitude
let createCone (latitude: float, longitude: float, radius: float) =
let originalCenter = Coordinate(latitude, longitude)
let center = Coordinate(latitude, longitude)
center.Move((radius * 1000.0), 290, Shape.Sphere)
let gd = GeoFence.Drawer(center, Shape.Sphere, 210)
let points = [getLatLon originalCenter]
for i in 0..4 do
let angle = (i - 10 + 360) % 360
gd.Draw(Distance(radius/8.0), angle)
gd.Close()
let points = points @ (gd.Points |> List.ofSeq |> List.map getLatLon)
let points = points |> List.take (List.length points - 1)
let points = points @ [getLatLon originalCenter]
points
// |> List.map (fun (lat, lon) -> $"{lat},{lon},") // format the points as multi-line strings to verify at this site:
// |> String.concat "\n" // https://mobisoftinfotech.com/tools/plot-multiple-points-on-map/
// The functions above (or some variant) will be used to feed the Weather Trigger API with geojson and weather codes
// to track when specific weather events occur during a time range - typically 1-3 hours look-ahead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment