Skip to content

Commit

Permalink
Merge pull request #38 from Bramgus12/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
Bramgus12 authored Jun 10, 2020
2 parents b233283 + f5c106f commit bd76821
Show file tree
Hide file tree
Showing 68 changed files with 319 additions and 49 deletions.
20 changes: 19 additions & 1 deletion client/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="nl.bylivingart.plantexpert">

<!-- The INTERNET permission is required for development. Specifically,
flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />


<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
Expand All @@ -8,7 +19,7 @@
<application
android:name="io.flutter.app.FlutterApplication"
android:label="Plant Expert"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
Expand Down Expand Up @@ -38,6 +49,13 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
</intent-filter>
</receiver>

<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/images/app_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions client/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = launcher_icon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = CN495B7KTS;
Expand Down Expand Up @@ -520,7 +520,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = launcher_icon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = CN495B7KTS;
Expand Down Expand Up @@ -553,7 +553,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = launcher_icon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = CN495B7KTS;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"images":[{"size":"20x20","idiom":"iphone","filename":"launcher_icon-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"launcher_icon-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"launcher_icon-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"launcher_icon-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"launcher_icon-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"launcher_icon-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"launcher_icon-40x40@3x.png","scale":"3x"},{"size":"60x60","idiom":"iphone","filename":"launcher_icon-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"launcher_icon-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"launcher_icon-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"launcher_icon-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"launcher_icon-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"launcher_icon-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"launcher_icon-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"launcher_icon-40x40@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"launcher_icon-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"launcher_icon-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"launcher_icon-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"launcher_icon-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
35 changes: 32 additions & 3 deletions client/lib/Utility.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// File for utility functions that might be usefull anywhere in the code.abstract
import 'dart:async';
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:plantexpert/api/Forecast.dart';
import 'package:rxdart/rxdart.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'dart:io';

import 'package:flutter/material.dart';
Expand All @@ -16,7 +20,8 @@ String randomString(int length) {
Random random = Random(DateTime.now().millisecondsSinceEpoch);
String generatedString = "";
for (int i = 0; i < length; i++) {
generatedString += possibleCharacters[random.nextInt(possibleCharacters.length)];
generatedString +=
possibleCharacters[random.nextInt(possibleCharacters.length)];
}
return generatedString;
}
Expand All @@ -26,12 +31,36 @@ String formatDate(DateTime date) {
if (date == null) {
throw new ArgumentError('date is null');
}

var year = date.year;
var month = date.month.toString().padLeft(2, '0');
var day = date.day.toString().padLeft(2, '0');
var hour = date.hour.toString().padLeft(2, '0');
var minute = date.minute.toString().padLeft(2, '0');

return '$day-$month-$year $hour:$minute';
}
}

// Verbruik water = (((waterhoeveelheid per dag van de plant * inhoud plant pot modifier) * dag) * (raam afstand modifier * zonkans))
/// Calculate the next date the plant will need water
/// [waterAmountPerDay] is in ml/day the plant needs
/// [potVolume] is the volume in liters
/// [windowDistance] is the distance of the plant from the window, on a scale of 1 to 5, where 5 is close to the window and 5 is far from the window
Future<DateTime> calculateNextWateringDate(int waterAmountPerDay, double potVolume, int windowDistance, int waterScale, { DateTime date}) async {
var api = new ApiConnection();
List<Forecast> forecasts = (await api.fetchWeatherForecast());
double volumePlantPotModifier = 0;
[{10: 2.6}, {8: 2.2}, {5: 1.8}, {3: 1.4}, {1: 1.0}].forEach((element) { element.forEach((key, value) { if(key <= potVolume.ceil()) { volumePlantPotModifier = value; } }); });
double windowRangeModifier = 0;
[{1: 0.8}, {2: 0.85}, {3: 0.9}, {4: 0.95}, {5: 1.0}].forEach((element) { if(element.containsKey(windowDistance)) windowRangeModifier = element[windowDistance]; });
double sunChance = 0;
double totalWaterNeeded = waterScale * 35.0; //35-175ml
forecasts.forEach((element) { sunChance += element.sunChanse; });
sunChance /= forecasts.length;
int waterPerDay = ( ( (waterAmountPerDay * volumePlantPotModifier) * (windowRangeModifier + (sunChance ~/ 100) ) ) ).ceil();
print('water per day: $waterPerDay');
int daysToNextWatering = totalWaterNeeded ~/ waterPerDay;
print('days to next watering: $daysToNextWatering');
if(date != null)
return date.add(Duration(days: daysToNextWatering));
return DateTime.now().add(Duration(days: daysToNextWatering));
}
15 changes: 14 additions & 1 deletion client/lib/api/ApiConnection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:plantexpert/Utility.dart';
import 'package:plantexpert/api/JsonSerializeable.dart';
import 'package:plantexpert/api/Plant.dart';
import 'package:plantexpert/api/User.dart';
import 'package:plantexpert/api/Forecast.dart';
import 'package:plantexpert/api/UserPlant.dart';
import 'package:plantexpert/api/WeatherStation.dart';
import 'package:http_parser/http_parser.dart';
Expand Down Expand Up @@ -209,7 +210,19 @@ class ApiConnection {
Future<http.Response> putAdminUser(User user) async {
return await _putJson("admin/users/${user.id}/", user);
}


// Weather forecast
Future<List<Forecast>> fetchWeatherForecast() async {
Map<String, dynamic> jsonWeatherStations = await _fetchJsonObject('weather/forecast/');
List<Forecast> forecasts = List<Forecast>();
jsonWeatherStations.forEach((key, value) {
if(key.toLowerCase().contains('dagplus')){
forecasts.add(Forecast.fromJson( value ));
}
});
return forecasts;
}

// Weather stations
Future<List<WeatherStation>> fetchWeatherStations(String region) async {
Iterable jsonWeatherStations = await _fetchJsonList('weather/$region/');
Expand Down
62 changes: 62 additions & 0 deletions client/lib/api/Forecast.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
class Forecast {

String dateString;
String weekday;
int sunChanse;
int rainChanse;
int minMmRain;
int maxMmRain;
int minTemp;
int maxTemp;
String windDirection;
int windForce;

Forecast({
this.dateString,
this.weekday,
this.sunChanse,
this.rainChanse,
this.minMmRain,
this.maxMmRain,
this.minTemp,
this.maxTemp,
this.windDirection,
this.windForce,
});

factory Forecast.fromJson(Map<String, dynamic> jsonForecast) {
return Forecast(
dateString: jsonForecast['datum'],
weekday: jsonForecast['dagweek'],
sunChanse: int.tryParse(jsonForecast['kanszon']),
rainChanse: int.tryParse(jsonForecast['kansregen']),
minMmRain: int.tryParse(jsonForecast['minmmregen']),
maxMmRain: int.tryParse(jsonForecast['maxmmregen']),
minTemp: int.tryParse(jsonForecast['mintemp']),
maxTemp: int.tryParse(jsonForecast['maxtempmax']),
windDirection: jsonForecast['windrichting'],
windForce: int.tryParse(jsonForecast['windkracht']),
);
}

@override
String toString() {
return
'''[ Forecast $dateString ]
date:\t\t$dateString
weekday:\t\t$weekday
sunChanse:\t\t$sunChanse
rainChanse:\t\t$rainChanse
minMmRain:\t\t$minMmRain
maxMmRain:\t\t$maxMmRain
minTemp:\t\t$minTemp
maxTemp:\t\t$maxTemp
windDirection:\t\t$windDirection
windForce:\t\t$windForce
''';
}
}


//"sneeuwcms": "0"
//}
4 changes: 3 additions & 1 deletion client/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
import 'package:global_configuration/global_configuration.dart';
import 'package:plantexpert/pages/CameraPlantDetailScreen.dart';
import 'package:plantexpert/pages/account/Account.dart';
import 'package:plantexpert/widgets/NotificationManager.dart';

import 'pages/Camera.dart';
import 'pages/home/Home.dart';
import 'pages/plant-list.dart';
import 'pages/plant-detail.dart';
import 'pages/add-plant.dart';

Future<void> main() async {
Expand All @@ -20,13 +20,15 @@ Future<void> main() async {
// Running in debug mode
await GlobalConfiguration().loadFromAsset("debug");
}
initializeNotifications();

runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {

return MaterialApp(
title: 'Plant Expert',
theme: ThemeData(
Expand Down
47 changes: 47 additions & 0 deletions client/lib/pages/add-plant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import 'package:plantexpert/api/UserPlant.dart';
import 'package:plantexpert/Utility.dart';
import 'package:plantexpert/pages/LocationSelectionMap.dart';
import 'package:plantexpert/widgets/InputTextField.dart';
import 'package:plantexpert/widgets/NotificationManager.dart';
import 'dart:convert';

import 'package:flutter_cache_manager/flutter_cache_manager.dart';

Expand Down Expand Up @@ -158,11 +160,56 @@ class _AddPlant extends State<AddPlant> {
});
print(e);
}
on StatusCodeException catch (e) {
String errorMessage = "Onverwachte fout bij het opslaan van gegevens.";
if (e.reponse.statusCode == 400) {
try {
Map<String, dynamic> validationErrors = json.decode(utf8.decode(e.reponse.bodyBytes))["validationErrors"];
errorMessage = "Fouten bij opslaan van plant:";
validationErrors.forEach((key, value) {
errorMessage += "\n• " + value;
});
} catch(e) {
print(e);
}
}
setState(() {
_serverErrorMessage = errorMessage;
result = null;
});
print(e);
}


Navigator.pop(context);

if (result != null) {
var tempPlant = listOfPlants.firstWhere((plant) => plant.id == newPlant.plantId);

if (!hideDatePicker) {
calculateNextWateringDate(
tempPlant.waterNumber.toInt(),
newPlant.potVolume,
newPlant.distanceToWindow.toInt(),
tempPlant.waterScale.toInt()
).then((value) => scheduleNotification(
(value.millisecondsSinceEpoch ~/ 1000) - (DateTime.now().millisecondsSinceEpoch ~/ 1000) <= 0 ?
5 :
(value.millisecondsSinceEpoch ~/ 1000) - (DateTime.now().millisecondsSinceEpoch ~/ 1000) ,
'\'${newPlant.nickname}\' Heeft water nodig!',
'Het is al weer een paar dagen geleden sinds \'${newPlant.nickname}\' water heeft gehad, vergeet hem geen water te geven.',
'Water geven',
'Notificaties voor het water geven van de plant'));
} else {
scheduleNotification(
((DateTime.now().add(Duration(seconds: 5)).millisecondsSinceEpoch ~/ 1000) - (DateTime.now().millisecondsSinceEpoch ~/ 1000)) ,
'Geef \'${newPlant.nickname}\' voor het eerst water!',
'\'${newPlant.nickname}\' heeft nog geen water gehad, geef hem voor het eerst een klein beetje water, zodat de bodem vochtig is.',
'Water geven',
'Notificaties voor het water geven van de plant');
}


// SocketException occurs before the loading dialog is shown,
// use try catch to prevent `The getter 'focusScopeNode' was called on null`
Navigator.pop(context, "addedPlant");
Expand Down
21 changes: 16 additions & 5 deletions client/lib/pages/home/PriorityPlantsCard.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:plantexpert/Utility.dart';
import 'package:plantexpert/api/ApiConnection.dart';
import 'package:plantexpert/api/ApiConnectionException.dart';
import 'package:plantexpert/api/Plant.dart';
import 'package:plantexpert/api/UserPlant.dart';
import 'package:http/http.dart' as http;

Expand Down Expand Up @@ -70,7 +72,7 @@ class _PriorityPlantsCardState extends State<PriorityPlantsCard> {
),
),
title: Text(userPlants[index].nickname),
subtitle: Text("${sinceLastWaterTime.inDays} dagen geleden."),
subtitle: Text(userPlants[index].lastWaterDate.millisecondsSinceEpoch == 0 ? 'Deze plant heeft nog geen water gehad.' : "${sinceLastWaterTime.inDays} dagen geleden."),
trailing: OutlineButton(
onPressed: () => updateLastWaterDate(userPlants[index], context),
child: Row(
Expand Down Expand Up @@ -231,8 +233,10 @@ class _PriorityPlantsCardState extends State<PriorityPlantsCard> {
status = _Status.loading;
});
List<UserPlant> allUserPlants;
List<Plant> allPlants;
try {
allUserPlants = await ApiConnection().fetchUserPlants();
allPlants = await ApiConnection().fetchPlants();
} on InvalidCredentialsException catch(e) {
if(!this.mounted)
return;
Expand Down Expand Up @@ -269,10 +273,15 @@ class _PriorityPlantsCardState extends State<PriorityPlantsCard> {
if(!this.mounted)
return;

// Filter list, keep only user plants that haven't received water in the last 24 hours.
// TODO: Change this to be calculated per plant type with a formula.
DateTime tooLongWithoutWater = DateTime.now().subtract(Duration(days: 1));
List<UserPlant> urgentUserPlants = allUserPlants.where((userPlant) => userPlant.lastWaterDate.millisecondsSinceEpoch < tooLongWithoutWater.millisecondsSinceEpoch ).toList();
List<UserPlant> urgentUserPlants = List<UserPlant>();
for(var userPlant in allUserPlants) {
Plant plant = allPlants.firstWhere((plant) => plant.id == userPlant.plantId);
if(plant != null){
var nextWaterDate = await calculateNextWateringDate(plant.waterNumber.toInt(), userPlant.potVolume, userPlant.distanceToWindow.toInt(), plant.waterScale.toInt(), date: userPlant.lastWaterDate);
if(DateTime.now().difference(nextWaterDate).inDays >= 0)
urgentUserPlants.add(userPlant);
}
}

if(urgentUserPlants.length == 0) {
setState(() {
Expand All @@ -283,6 +292,8 @@ class _PriorityPlantsCardState extends State<PriorityPlantsCard> {
}
urgentUserPlants.sort((userPlant1, userPlant2) => userPlant1.lastWaterDate.millisecondsSinceEpoch > userPlant2.lastWaterDate.millisecondsSinceEpoch ? 1 : 0 );

if(!this.mounted)
return;
setState(() {
userPlants = urgentUserPlants;
status = _Status.done;
Expand Down
Loading

0 comments on commit bd76821

Please sign in to comment.