Compare commits

...

15 Commits

Author SHA1 Message Date
ad4630e495 Attempting to add the form for passing the weather location but it's been failing to render. Hardcoded field sizes for now. Still figuring out how the expense tracker app is working without this. 2025-05-14 18:46:39 -07:00
2ee8eaecc8 Correct "errors". 2025-05-14 08:34:23 -07:00
13c532e87f Add another README. 2025-04-26 10:15:03 -07:00
70eaa8a4fb Add README's to a few folders. 2025-04-26 10:13:59 -07:00
3ae27fdda4 Add notes for my own comfort calculations, where >= 10 will be considered uncomfortable. 2025-04-26 10:12:21 -07:00
4132aa0f83 Add the wind annoyance factor from Buddy and placeholders for a new Feels Like calculation. 2025-04-26 10:10:29 -07:00
6e2d293cdc Add the heat index as well as some error handling. 2025-04-26 08:37:35 -07:00
7ffcfee1d3 Add wind chill. 2025-04-25 17:33:11 -07:00
7e53a2b4e2 Successfully pulling a good weather report. Now to add the heat index and wind chill calculations. 2025-04-25 17:10:22 -07:00
b1d01c6915 Begin a refactor of code to make it more reusable and get it out of main. Going to stop worrying about this for now though and just get the data formatted and presented well, then refactor once it's working correctly. 2025-04-25 15:59:53 -07:00
c2995dac6d Add variables for the Open Weather config rather than holding it in the min file. 2025-04-25 15:59:02 -07:00
4483c1ebb0 Add file for future JSON related code. 2025-04-25 15:58:26 -07:00
8c8b848090 Change magic numbers to be a variable. :) 2025-04-25 08:41:03 -07:00
54ae255e9e Add var for number of seconds between data refreshes. Rename locals file to config and also end example files in .dart. 2025-04-25 08:39:56 -07:00
7a48d137e0 Do not allow the weather to be refreshed very quickly. 2025-04-25 08:34:19 -07:00
11 changed files with 411 additions and 37 deletions

5
.gitignore vendored
View File

@ -244,7 +244,8 @@ app.*.map.json
/android/app/release /android/app/release
# Do not post secrets! # Do not post secrets!
secrets.dart lib/var/secrets.dart
# Keep locals local. # Keep locals local.
locals.dart lib/var/local.dart
lib/var/config.dart

3
assets/README.md Normal file
View File

@ -0,0 +1,3 @@
# Assets
Images and other media for Buddy.

4
bin/README.md Normal file
View File

@ -0,0 +1,4 @@
# bin
Helper scripts forthose new to Flutter or for myself if I take a break and need
reminders. ;D

51
lib/helpers/http.dart Normal file
View File

@ -0,0 +1,51 @@
// File for helper functions.
import '/var/config.dart';
import '/var/secrets.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
//import 'dart:convert';
// Generic method to hit a GET request and return the response.
Future<String> hitAPI(String url) async {
if (debug) debugPrint("DEBUG: URL is '$url'.");
try {
http.Response response = await http.get(Uri.parse(url));
String data = "";
if (response.statusCode == 200) {
data = response.body;
//var decodedData = jsonDecode(data);
//data = decodedData.toString();
//data = jsonEncode(decodedData);
if (debugVerbose) {
debugPrint("DEBUG-VERBOSE: Response data: \n\n$data\n");
}
} else {
if (debug) {
debugPrint(
"DEBUG: Response failed with code '${response.statusCode.toString()}'",
);
}
}
return data;
} catch (e) {
return "Sorry! It seems we have an issue: '${e.toString()}'.";
}
}
// How to guide: https://openweathermap.org/current#zip
Future<String> hitOpenWeatherZip(String zipCode, String? countryCode) async {
countryCode ??= "US";
String url =
'$openWeatherAPI'
'?zip=$zipCode,$countryCode'
'&units=imperial'
'&appid=$openWeatherKey';
String data = await hitAPI(url);
return data;
}

177
lib/helpers/json.dart Normal file
View File

@ -0,0 +1,177 @@
// Functions related to parsing JSON strings and objects.
import '/var/config.dart';
import 'package:flutter/material.dart';
import 'dart:math';
String formatOpenWeatherData(var data) {
String location = pullOpenWeatherCity(data),
temp = pullOpenWeatherTemp(data),
conditions = pullOpenWeatherConditions(data),
windSpeed = pullOpenWeatherWind(data),
humidity = pullOpenWeatherHumidity(data);
String windChill = getWindChill(temp, windSpeed),
heatIndex = getHeatIndex(temp, humidity),
windAnnoyance = getWindAnnoyance(temp, windSpeed),
feelsLike = getUniversalThermalClimateIndex();
String comfort = "";
String text =
"$location is $temp$tempUnits and $conditions"
" with a wind speed of $windSpeed$windUnits"
" and humidity of $humidity$humidityUnits."
" $windChill"
" $heatIndex"
" $windAnnoyance"
" $feelsLike"
" $comfort";
final String doubleSpace = " ", singleSpace = " ";
while (text.contains(doubleSpace)) {
text = text.replaceAll(doubleSpace, singleSpace);
}
return text;
}
String pullOpenWeatherCity(var data) {
String location = "${data['city']['name']} (${data['city']['country']})";
if (debug) {
debugPrint("DEBUG: location = '$location'");
}
return location;
}
String pullOpenWeatherTemp(var data) {
String temp = data['list'][0]['main']['temp'].toString();
if (debug) {
debugPrint("DEBUG: temp = '$temp'");
}
return temp;
}
String pullOpenWeatherConditions(var data) {
String conditions = data['list'][0]['weather'][0]['description'].toString();
if (debug) {
debugPrint("DEBUG: conditions = '$conditions'");
}
return conditions;
}
String pullOpenWeatherWind(var data) {
String wind = data['list'][0]['wind']['speed'].toString();
if (debug) {
debugPrint("DEBUG: wind = '$wind'");
}
return wind;
}
String pullOpenWeatherHumidity(var data) {
String humidity = data['list'][0]['main']['humidity'].toString();
if (debug) {
debugPrint("DEBUG: humidity = '$humidity'");
}
return humidity;
}
double calcWindChill(double temp, double windSpeed) {
// ## Wind Chill ##
// # Wind speed as noted in: https://answers.yahoo.com/question/index?qid=20091020183148AAHm3kB&guccounter=1
// # More official source: https://www.weather.gov/media/epz/wxcalc/windChill.pdf
double windChill =
35.74 +
(0.6215 * temp) -
(35.75 * pow(windSpeed, 0.16)) +
(0.4275 * temp * pow(windSpeed, 0.16));
if (debug) {
debugPrint("DEBUG: windChill = '$windChill'");
}
return windChill;
}
String getWindChill(String temp, String windSpeed) {
double temperature = double.parse(temp);
double wind = double.parse(windSpeed);
if ((temperature < 50 && wind > 5) || debug) {
double windChill = calcWindChill((temperature), (wind));
return "My guess is that's a wind chill of $windChill$tempUnits.";
}
return "";
}
double calcHeatIndex(double temp, double humidity) {
// ## Heat Index ##
// # Official formula: https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml
double heatIndex =
-42.379 +
2.04901523 * temp +
10.14333127 * humidity -
0.22475541 * temp * humidity -
0.00683783 * temp * temp -
0.05481717 * humidity * humidity +
0.00122874 * temp * temp * humidity +
0.00085282 * temp * humidity * humidity -
0.00000199 * temp * temp * humidity * humidity;
if (humidity < 13 && temp >= 80 && temp <= 112) {
heatIndex -= ((13 - humidity) / 4) * sqrt((17 - (temp - 95.0).abs()) / 17);
}
if (humidity > 85 && temp >= 80 && temp <= 87) {
heatIndex += ((humidity - 85) / 10) * ((87 - temp) / 5);
}
if (heatIndex < 80) {
heatIndex =
0.5 * (temp + 61.0 + ((temp - 68.0) * 1.2) + (humidity * 0.094));
heatIndex = (heatIndex + temp) / 2;
}
if (debug) {
debugPrint("DEBUG: heatIndex = '$heatIndex'");
}
return heatIndex;
}
String getHeatIndex(String temp, String humidity) {
double temperature = double.parse(temp);
double humid = double.parse(humidity);
if (temperature > 80 || debug) {
double heatIndex = calcHeatIndex((temperature), (humid));
return "My guess is that's a heat index of $heatIndex$tempUnits.";
}
return "";
}
double calcWindAnnoyance(double temp, double windSpeed) {
double windAnnoyance = (temp / (windSpeed * (windSpeed * 0.05)));
if (debug) {
debugPrint("DEBUG: windAnnoyance = '$windAnnoyance'");
}
return windAnnoyance;
}
String getWindAnnoyance(String temp, String windSpeed) {
double temperature = double.parse(temp);
double wind = double.parse(windSpeed);
double windAnnoyance = calcWindAnnoyance(temperature, wind);
if (windAnnoyance < 3) {
return "Wind may be a bit much for the temperature.";
}
return "";
}
double calcUniversalThermalClimateIndex() {
return 0;
}
String getUniversalThermalClimateIndex() {
return "";
}

View File

@ -1,11 +1,13 @@
// Local // Local
import '/var/secrets.dart'; import 'dart:convert';
import '/var/locals.dart';
//import '/var/secrets.dart';
import '/var/config.dart';
import 'helpers/http.dart';
import 'helpers/json.dart';
// Flutter / Dart // Flutter / Dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() { void main() {
runApp(const MainApp()); runApp(const MainApp());
@ -21,8 +23,17 @@ class MainApp extends StatefulWidget {
} }
class _MainAppState extends State<MainApp> { class _MainAppState extends State<MainApp> {
final _formKey = GlobalKey<FormState>();
String _zipcode = "";
String _city = "";
String _country = "";
String _latlong = "";
String weather = loadText; String weather = loadText;
bool keepLoading = false; bool keepLoading = false;
DateTime lastLoadTime = DateTime.now().subtract(
Duration(seconds: limitRefreshSeconds),
);
@override @override
void initState() { void initState() {
@ -37,6 +48,80 @@ class _MainAppState extends State<MainApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget form = Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 20,
width: 100,
child: TextFormField(
initialValue: _zipcode,
validator: (value) {
return null;
},
onSaved: (value) {
_zipcode = value!;
},
),
),
SizedBox(
height: 20,
width: 100,
child: TextFormField(
initialValue: _city,
validator: (value) {
return null;
},
onSaved: (value) {
_city = value!;
},
),
),
SizedBox(
height: 20,
width: 100,
child: TextFormField(
initialValue: _country,
validator: (value) {
return null;
},
onSaved: (value) {
_country = value!;
},
),
),
],
),
SizedBox(
height: 20,
width: 300,
child: TextFormField(
initialValue: _latlong,
validator: (value) {
return null;
},
onSaved: (value) {
_latlong = value!;
},
),
),
TextButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
}
},
child: Text("Save"),
),
],
),
);
Widget weatherLayout = Column( Widget weatherLayout = Column(
children: [Text(weather), CircularProgressIndicator()], children: [Text(weather), CircularProgressIndicator()],
); );
@ -66,45 +151,83 @@ class _MainAppState extends State<MainApp> {
return MaterialApp( return MaterialApp(
home: Scaffold( home: Scaffold(
body: Center( body: Center(
child: Column(children: [Text('Weather Today!'), weatherLayout]), child: Column(
children: [
Text('Weather Today!'),
form,
Expanded(child: weatherLayout),
],
),
), ),
), ),
); );
} }
// How to guide: // Convert the Future value into a usable value.
// https://openweathermap.org/current#zip pullWeather({
Future<String> _hitOpenWeather(String zipCode, String? countryCode) async { String? country,
countryCode ??= "US"; String? zip,
String? city,
String? lat,
String? long,
}) async {
country ??= "US";
String url = if (lat != null && long != null) {
'https://api.openweathermap.org/data/2.5/forecast' weather = "Lat / Long Not Yet Implemented";
'?zip=$zipCode,$countryCode' } else if (zip != null) {
'&units=imperial' weather = await hitOpenWeatherZip(zip, country);
'&appid=$openWeatherKey'; } else if (city != null) {
weather = "City Weather Not Yet Implemented";
if (debug) debugPrint(url);
http.Response response = await http.get(Uri.parse(url));
String data = "";
if (response.statusCode == 200) {
data = response.body;
var decodedData = jsonDecode(data);
if (debug) debugPrint(decodedData.toString());
data = decodedData.toString();
} else { } else {
debugPrint(response.statusCode.toString()); weather = "Please enter a location.";
} }
return data; if (weather.toString().contains("Sorry!")) {
return weather;
} }
_callOpenWeather() async { if (weather != loadText) {
weather = await _hitOpenWeather("47630", "US"); if (debug) {
debugPrint("DEBUG: Formatting text.");
}
var weatherObject = jsonDecode(weather);
String weatherString = formatOpenWeatherData(weatherObject);
weather = weatherString;
if (debug) {
debugPrint("DEBUG: Set to formatted weather string.");
}
} else {
if (debug) {
debugPrint("DEBUG: Skipping text formatting.");
}
}
} }
// Call the API and put the desired information into the screen variable.
_refreshWeather() { _refreshWeather() {
var lastReloadSeconds = DateTime.now().difference(lastLoadTime).inSeconds;
if (debug) {
debugPrint("DEBUG: Refresh was '$lastReloadSeconds' seconds ago.");
}
if (lastReloadSeconds < limitRefreshSeconds) {
debugPrint("DEBUG: Skipping reload.");
// TODO / TBD: Show a toast / scaffold snackbar that it cannot reload yet,
// or change the button text to "Please wait X seconds!".
return;
}
if (debug) {
debugPrint("DEBUG: Pulling weather...");
}
weather = loadText; weather = loadText;
_callOpenWeather(); pullWeather(country: "US", zip: "47630");
lastLoadTime = DateTime.now();
if (debug) {
debugPrint("DEBUG: Weather pulled and date is set.");
}
} }
} }

View File

@ -0,0 +1,15 @@
// This file needs renamed `local.dart` when implemented.
// Basic
// Project-wide configuration variables for both testing and production.
const bool debug = true;
const bool debugVerbose = false;
const int limitRefreshSeconds = 60;
// OpenWeather Constants
// Settings for how to use the OpenWeather API.
const String openWeatherProtocol = "https://";
const String openWeatherHost = "api.openweathermap.org";
const String openWeatherURI = "/data/2.5/forecast";
const String openWeatherAPI =
"$openWeatherProtocol$openWeatherHost$openWeatherURI";

View File

@ -1,3 +0,0 @@
// This file needs renamed `local.dart` if implemented.
const bool debug = false;

View File

@ -1,3 +1,3 @@
// This file needs renamed `secrets.dart` and filled out properly if implemented. // This file needs renamed `secrets.dart` and filled out properly if implemented.
final openWeatherKey = "abc123"; final String openWeatherKey = "abc123";

3
notes/README.md Normal file
View File

@ -0,0 +1,3 @@
# Notes
Any calculations, tests, or general ideas for functionality within Buddy. :)

Binary file not shown.