Last active
June 28, 2024 01:22
-
-
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: "35.60095" | |
Longitude: "-82.55402" | |
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 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