Skip to content

Instantly share code, notes, and snippets.

@houstonhaynes
Last active July 3, 2024 02:02
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: "YOUR_LATITUDE_AS_DECIMAL"
Longitude: "YOUR_LONGITUDE_AS_DECIMAL"
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 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