first commit

This commit is contained in:
combatfarm 2025-05-20 14:08:57 +07:00
commit 9e953f3db0
173 changed files with 14266 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
- platform: android
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
- platform: ios
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
- platform: linux
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
- platform: macos
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
- platform: web
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
- platform: windows
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# jago
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

77
android/app/build.gradle Normal file
View File

@ -0,0 +1,77 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "com.google.gms.google-services"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
if (flutterVersionCode == null) {
flutterVersionCode = "1"
}
def flutterVersionName = localProperties.getProperty("flutter.versionName")
if (flutterVersionName == null) {
flutterVersionName = "1.0"
}
android {
namespace = "com.example.jago"
compileSdk = 34
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
defaultConfig {
applicationId = "com.example.jago"
minSdk = 23
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
signingConfig signingConfigs.debug
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
shrinkResources false
}
}
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:deprecation"
}
}
flutter {
source = "../.."
}
dependencies {
implementation platform('com.google.firebase:firebase-bom:32.7.4')
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-database-ktx' // Pastikan Firebase Database ada
implementation 'com.google.firebase:firebase-storage-ktx' // Jika menggunakan Firebase Storage
implementation 'com.google.firebase:firebase-auth-ktx' // Jika menggunakan Firebase Authentication
}

View File

@ -0,0 +1,30 @@
{
"project_info": {
"project_number": "973274985069",
"firebase_url": "https://jago-9a9a6-default-rtdb.firebaseio.com",
"project_id": "jago-9a9a6",
"storage_bucket": "jago-9a9a6.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:973274985069:android:e1382cbb4d2e6c79de4564",
"android_client_info": {
"package_name": "com.example.jago"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyD5Rn71-iCp3CZUnatbCaNJWzGv4S3wVk4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

11
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,11 @@
# Flutter wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
# Firebase
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,96 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.jago">
<!-- 🔹 Izin yang diperlukan -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- Untuk Firebase Cloud Messaging -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- 🔹 Untuk Android 10+ (API 29) -->
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<application
android:label="Jago"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:usesCleartextTraffic="true"
tools:replace="android:allowBackup"
android:exported="true"
android:enableOnBackInvokedCallback="true">
<!-- 🔹 Firebase Analytics -->
<meta-data
android:name="com.google.firebase.analytics.enabled"
android:value="true" />
<!-- 🔹 Firebase Crashlytics -->
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="true" />
<!-- 🔹 Konfigurasi Flutter -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- 🔹 Konfigurasi Firebase Cloud Messaging -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="default_channel" />
<!-- Tambahkan konfigurasi jaringan -->
<meta-data
android:name="firebase_performance_logcat_enabled"
android:value="true" />
<!-- Network Security Config -->
<meta-data
android:name="android.networkSecurityConfig"
android:resource="@xml/network_security_config" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 🔹 Firebase Messaging Service -->
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
</application>
<!-- 🔹 Untuk mendeteksi aplikasi yang bisa memproses teks -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.example.jago
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">firebaseio.com</domain>
<domain includeSubdomains="true">googleapis.com</domain>
<domain includeSubdomains="true">firebase.com</domain>
</domain-config>
</network-security-config>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

32
android/build.gradle Normal file
View File

@ -0,0 +1,32 @@
buildscript {
ext.kotlin_version = '1.9.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.3.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.4.1'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' // Tambahan untuk crashlytics
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,5 @@
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
org.gradle.java.home=C:/Program Files/Eclipse Adoptium/jdk-17.0.14.7-hotspot

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

28
android/settings.gradle Normal file
View File

@ -0,0 +1,28 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
plugins {
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
}
include ":app"

BIN
assets/Logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

BIN
assets/icon/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
assets/jago.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
assets/kipas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/kipas_on.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

21
index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<title>Log Sensor, WiFi, dan LCD</title>
</head>
<body>
<div class="container">
<h1>Log Sensor, WiFi, dan LCD</h1>
<div id="logs">
<h2>Logs</h2>
<div id="sensor-log">Sensor Log: <span id="sensor-data"></span></div>
<div id="wifi-log">WiFi Log: <span id="wifi-data"></span></div>
<div id="lcd-log">LCD Log: <span id="lcd-data"></span></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.jago;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.jago.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.jago.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.jago.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.jago;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.jago;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Jago</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>jago</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

739
lib/IOT.ino Normal file
View File

@ -0,0 +1,739 @@
#include <ESP8266WiFi.h>
#include <FirebaseESP8266.h>
#include <DHT.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
// Konfigurasi WiFi
const char* ssid = "Combat2024";
const char* password = "12345678";
// Konfigurasi Firebase
#define FIREBASE_HOST "https://jago-9a9a6-default-rtdb.firebaseio.com/"
#define FIREBASE_AUTH "Q3i1jDmc3Xh9UiUTaGUmOw4tVztV5TJ3INU9RoWN"
// NTP Client untuk mendapatkan waktu
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
FirebaseData firebaseData;
FirebaseConfig config;
FirebaseAuth auth;
#define DHTPIN D5
#define RELAY_TEMP D3 // Relay untuk suhu (kipas)
#define RELAY_HUMIDITY D6 // Relay untuk kelembapan (lampu)
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
LiquidCrystal_I2C lcd(0x27, 16, 2); // Pastikan alamat sesuai dengan modul yang digunakan
int ageInWeeks = 1; // Default, nanti diambil dari Firebase
// Variabel untuk mode kontrol
bool autoModeFan = true; // Default: mode otomatis untuk kipas
bool autoModeLight = true; // Default: mode otomatis untuk lampu
bool fanStatus = false; // Status kipas (ON/OFF)
bool lightStatus = false; // Status lampu (ON/OFF)
// Variabel untuk rentang suhu dan kelembapan dinamis
struct Range {
float min;
float max;
float target;
};
Range tempRanges[4] = {
{33, 35, 34}, // Minggu 1
{30, 33, 31.5}, // Minggu 2
{28, 30, 29}, // Minggu 3
{25, 28, 26.5} // Minggu 4
};
Range humidityRanges[4] = {
{60, 70, 65}, // Minggu 1
{60, 65, 62.5}, // Minggu 2
{60, 65, 62.5}, // Minggu 3
{55, 60, 57.5} // Minggu 4
};
// Variabel untuk pengaturan tampilan LCD
unsigned long lastDisplayToggle = 0;
bool showMainDisplay = true; // Untuk mengganti tampilan LCD
// Variabel untuk reconnect
unsigned long lastReconnectAttempt = 0;
const unsigned long reconnectInterval = 30000; // 30 detik
// Variabel untuk menyimpan status koneksi WiFi sebelumnya
bool previousWiFiStatus = false;
// Fungsi untuk logging ke Firebase dengan timestamp
void logToFirebase(const String& path, const String& message) {
String timestamp = getCurrentTimestamp();
String logMessage = timestamp + ": " + message;
// Kirim log ke Firebase, menggunakan setString untuk memperbarui entri
Firebase.setString(firebaseData, path, logMessage);
}
// Fungsi untuk mendapatkan waktu dari NTP
String getCurrentTimestamp() {
timeClient.update();
return String(timeClient.getEpochTime());
}
// Variabel untuk memeriksa koneksi WiFi
unsigned long lastWiFiCheck = 0;
const unsigned long wifiCheckInterval = 2000; // 2 detik
// Fungsi untuk memeriksa dan memulihkan koneksi WiFi
void checkWiFiConnection() {
if (WiFi.status() != WL_CONNECTED) {
unsigned long currentMillis = millis();
// Coba reconnect setiap interval tertentu
if (currentMillis - lastReconnectAttempt >= reconnectInterval) {
logToFirebase("/logs/wifi", "WiFi Disconnected. Attempting to Reconnect");
// Matikan WiFi dan nyalakan kembali
WiFi.disconnect();
delay(1000);
WiFi.begin(ssid, password);
lastReconnectAttempt = currentMillis;
}
// Tambahkan log ketika WiFi terputus
if (previousWiFiStatus) {
logToFirebase("/logs/wifi", "WiFi Connection Failed");
previousWiFiStatus = false;
}
} else {
// Jika WiFi terhubung, periksa apakah statusnya berubah
if (!previousWiFiStatus) {
logToFirebase("/logs/wifi", "WiFi Connected Successfully");
Serial.println("\nWiFi connected");
previousWiFiStatus = true;
}
}
}
void setup() {
Serial.begin(115200);
// Inisialisasi pin relay
pinMode(RELAY_TEMP, OUTPUT);
pinMode(RELAY_HUMIDITY, OUTPUT);
digitalWrite(RELAY_TEMP, HIGH); // Pastikan relay mulai dalam keadaan OFF
digitalWrite(RELAY_HUMIDITY, HIGH);
// Inisialisasi LCD dengan deteksi kesalahan
logToFirebase("/logs/lcd", "LCD Initialization Started");
// Coba inisialisasi LCD dengan timeout
bool lcdInitSuccess = false;
Wire.begin();
// Periksa apakah LCD merespons
Wire.beginTransmission(0x27); // Alamat I2C LCD
byte error = Wire.endTransmission();
if (error == 0) {
// LCD ditemukan, coba inisialisasi
lcd.init();
lcd.backlight();
lcd.clear();
lcd.begin(16,2);
lcd.print("Initializing...");
lcdInitSuccess = true;
}
if (lcdInitSuccess) {
logToFirebase("/logs/lcd", "LCD Ready and Initialized");
Serial.println("LCD initialized successfully");
} else {
logToFirebase("/logs/lcd", "LCD Initialization Failed - Check Connections");
Serial.println("LCD initialization failed! Check wiring (GND, VCC, SDA, SCL)");
}
// Inisialisasi WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
// Tunggu koneksi WiFi
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
logToFirebase("/logs/wifi", "WiFi Connected Successfully");
Serial.println("\nWiFi connected");
// Inisialisasi NTP Client
timeClient.begin();
timeClient.setTimeOffset(25200); // Offset untuk WIB (GMT+7)
} else {
logToFirebase("/logs/wifi", "WiFi Connection Failed");
Serial.println("\nWiFi connection failed");
}
// Inisialisasi DHT
dht.begin();
logToFirebase("/logs/sensor", "DHT Sensor Initialized");
// Konfigurasi Firebase
config.host = FIREBASE_HOST;
config.signer.tokens.legacy_token = FIREBASE_AUTH;
Firebase.begin(&config, &auth);
Firebase.reconnectWiFi(true);
// Inisialisasi variabel kontrol di Firebase
Firebase.setBool(firebaseData, "/control/fan/auto", autoModeFan);
Firebase.setBool(firebaseData, "/control/light/auto", autoModeLight);
Firebase.setBool(firebaseData, "/control/fan/status", fanStatus);
Firebase.setBool(firebaseData, "/control/light/status", lightStatus);
// Inisialisasi rentang di Firebase jika belum ada
for (int i = 0; i < 4; i++) {
String tempPath = "/ranges/week" + String(i+1) + "/temperature";
String humidityPath = "/ranges/week" + String(i+1) + "/humidity";
// Inisialisasi rentang suhu
Firebase.setFloat(firebaseData, tempPath + "/min", tempRanges[i].min);
Firebase.setFloat(firebaseData, tempPath + "/max", tempRanges[i].max);
Firebase.setFloat(firebaseData, tempPath + "/target", tempRanges[i].target);
// Inisialisasi rentang kelembapan
Firebase.setFloat(firebaseData, humidityPath + "/min", humidityRanges[i].min);
Firebase.setFloat(firebaseData, humidityPath + "/max", humidityRanges[i].max);
Firebase.setFloat(firebaseData, humidityPath + "/target", humidityRanges[i].target);
}
}
void loop() {
unsigned long currentMillis = millis();
// Periksa koneksi WiFi setiap 2 detik
if (currentMillis - lastWiFiCheck >= wifiCheckInterval) {
checkWiFiConnection();
lastWiFiCheck = currentMillis;
}
// Periksa koneksi LCD secara berkala
checkLCDConnection();
// Update NTP Client
timeClient.update();
// Baca sensor suhu dan kelembapan
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
// Periksa pembacaan sensor
if (isnan(temperature) || isnan(humidity)) {
logToFirebase("/logs/sensor", "Failed to Read from DHT Sensor");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Sensor Error!");
lcd.setCursor(0, 1);
lcd.print("Check DHT sensor");
delay(2000);
return;
} else {
// Log pembacaan sensor berhasil (setiap 2 detik)
static unsigned long lastSensorLog = 0;
if (millis() - lastSensorLog > 2000) { // Log setiap 2 detik
logToFirebase("/logs/sensor", "DHT Sensor Reading Successful");
lastSensorLog = millis();
}
}
// Ambil umur ayam dari Firebase
if (Firebase.getInt(firebaseData, "/chicken_age")) {
ageInWeeks = firebaseData.intData();
Serial.print("Age: ");
Serial.println(ageInWeeks);
} else {
Serial.print("Failed to get age from Firebase: ");
Serial.println(firebaseData.errorReason());
// Inisialisasi jika data belum ada
if (firebaseData.errorReason() == "path not exist") {
Firebase.setInt(firebaseData, "/chicken_age", 1);
logToFirebase("/logs/system", "Initialized /chicken_age with default value 1");
}
}
// Periksa mode kontrol (manual atau otomatis)
checkControlMode();
// Ganti tampilan LCD setiap 5 detik
if (currentMillis - lastDisplayToggle >= 5000) {
showMainDisplay = !showMainDisplay;
lastDisplayToggle = currentMillis;
}
// Update tampilan LCD berdasarkan mode
if (showMainDisplay) {
updateMainDisplay(temperature, humidity);
} else {
updateModeDisplay();
}
// Kontrol suhu dan kelembapan berdasarkan mode
if (autoModeFan) {
// Mode otomatis untuk kipas (menggunakan fuzzy logic)
controlTemperature(temperature);
} else {
// Mode manual untuk kipas
if (fanStatus) {
digitalWrite(RELAY_TEMP, LOW); // Kipas ON
} else {
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
}
}
if (autoModeLight) {
// Mode otomatis untuk lampu (menggunakan fuzzy logic)
controlHumidity(humidity, temperature);
} else {
// Mode manual untuk lampu
if (lightStatus) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON
} else {
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF
}
}
// Kirim data ke Firebase
Firebase.setFloat(firebaseData, "/sensor/temperature", temperature);
Firebase.setFloat(firebaseData, "/sensor/Humidity", humidity);
Firebase.setBool(firebaseData, "/relay/Kipas", digitalRead(RELAY_TEMP) == LOW);
Firebase.setBool(firebaseData, "/relay/Lampu", digitalRead(RELAY_HUMIDITY) == LOW);
// Perbarui rentang dari Firebase
updateRangesFromFirebase();
delay(1000); // Tunggu 1 detik antara siklus
}
// Fungsi untuk tampilan utama (Temp, Hum, Age)
void updateMainDisplay(float temperature, float humidity) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Temp:");
lcd.print(temperature, 1);
lcd.print((char)223); // Simbol derajat
lcd.print("C");
lcd.setCursor(0, 1);
lcd.print("Hum:");
lcd.print(humidity, 1);
lcd.print("% Age:");
lcd.print(ageInWeeks);
lcd.print("w");
}
// Fungsi untuk tampilan mode (Auto/Manual)
void updateModeDisplay() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Fan: ");
lcd.print(autoModeFan ? "AUTO" : "MANUAL");
lcd.print(" ");
lcd.print(digitalRead(RELAY_TEMP) == LOW ? "ON" : "OFF");
lcd.setCursor(0, 1);
lcd.print("Light: ");
lcd.print(autoModeLight ? "AUTO" : "MANUAL");
lcd.print(" ");
lcd.print(digitalRead(RELAY_HUMIDITY) == LOW ? "ON" : "OFF");
}
// Definisi fungsi fuzzy logic untuk kontrol suhu
void controlTemperature(float temp) {
float minTemp = tempRanges[ageInWeeks-1].min;
float maxTemp = tempRanges[ageInWeeks-1].max;
float targetTemp = tempRanges[ageInWeeks-1].target;
// Menghitung derajat keanggotaan untuk berbagai kondisi suhu
float veryLow = 0; // Suhu sangat rendah (kondisi kritis)
float tooLow = 0; // Suhu terlalu rendah
float optimal = 0; // Suhu optimal
float tooHigh = 0; // Suhu terlalu tinggi
float veryHigh = 0; // Suhu sangat tinggi (kondisi kritis)
// Keanggotaan "sangat rendah" - kondisi kritis
if (temp < minTemp - 2) {
veryLow = 1.0; // Sangat rendah, kondisi kritis
} else if (temp >= minTemp - 2 && temp < minTemp - 1) {
veryLow = (minTemp - 1 - temp); // Agak sangat rendah
veryLow = constrain(veryLow, 0, 1);
}
// Keanggotaan "terlalu rendah"
if (temp >= minTemp - 1.5 && temp < minTemp) {
tooLow = (minTemp - temp) / 1.5; // Agak rendah
tooLow = constrain(tooLow, 0, 1);
}
// Keanggotaan "optimal"
if (temp >= minTemp && temp <= maxTemp) {
// Semakin dekat ke targetTemp, semakin optimal
optimal = 1.0 - abs(temp - targetTemp) / ((maxTemp - minTemp) / 2.0);
optimal = constrain(optimal, 0, 1);
}
// Keanggotaan "terlalu tinggi"
if (temp > maxTemp && temp <= maxTemp + 1.5) {
tooHigh = (temp - maxTemp) / 1.5; // Agak tinggi
tooHigh = constrain(tooHigh, 0, 1);
}
// Keanggotaan "sangat tinggi" - kondisi kritis
if (temp > maxTemp + 2) {
veryHigh = 1.0; // Sangat tinggi, kondisi kritis
} else if (temp > maxTemp + 1 && temp <= maxTemp + 2) {
veryHigh = (temp - (maxTemp + 1)) / 1.0; // Agak sangat tinggi
veryHigh = constrain(veryHigh, 0, 1);
}
// Simpan status suhu untuk digunakan oleh fungsi controlHumidity
bool isTempVeryCold = (veryLow > 0.3);
bool isTempTooCold = (tooLow > 0.3);
bool isTempVeryHot = (veryHigh > 0.3);
// Kirim nilai fuzzy ke Firebase untuk monitoring
Firebase.setFloat(firebaseData, "/fuzzy/temperature/veryLow", veryLow);
Firebase.setFloat(firebaseData, "/fuzzy/temperature/tooLow", tooLow);
Firebase.setFloat(firebaseData, "/fuzzy/temperature/optimal", optimal);
Firebase.setFloat(firebaseData, "/fuzzy/temperature/tooHigh", tooHigh);
Firebase.setFloat(firebaseData, "/fuzzy/temperature/veryHigh", veryHigh);
// Keputusan berdasarkan derajat keanggotaan dan prioritas
// Prioritas: Sangat tinggi > Sangat rendah > Terlalu tinggi > Optimal > Terlalu rendah
if (veryHigh > 0.3) { // Jika suhu sangat tinggi (kondisi kritis)
digitalWrite(RELAY_TEMP, LOW); // Kipas ON pada kecepatan maksimal
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Sangat Tinggi - Kipas ON (Kritis)");
Firebase.setBool(firebaseData, "/emergency/high_temperature", true);
} else if (veryLow > 0.3) { // Jika suhu sangat rendah (kondisi kritis)
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
// Nyalakan lampu untuk membantu meningkatkan suhu
// Ini akan di-override oleh controlHumidity jika diperlukan
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Sangat Rendah - Kipas OFF, Lampu ON");
Firebase.setBool(firebaseData, "/emergency/low_temperature", true);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
} else if (tooHigh > 0.3) { // Jika suhu terlalu tinggi
digitalWrite(RELAY_TEMP, LOW); // Kipas ON
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Tinggi - Kipas ON");
Firebase.setBool(firebaseData, "/emergency/low_temperature", false);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
} else if (optimal > 0.7) { // Jika suhu sangat optimal
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Optimal - Kipas OFF");
Firebase.setBool(firebaseData, "/emergency/low_temperature", false);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
} else if (tooLow > 0.3) { // Jika suhu terlalu rendah
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Rendah - Kipas OFF");
Firebase.setBool(firebaseData, "/emergency/low_temperature", true);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
} else {
// Kasus lain, gunakan pendekatan proporsional
// Jika suhu mendekati batas atas rentang, nyalakan kipas
float distanceToMax = maxTemp - temp;
if (distanceToMax < 1.0) {
digitalWrite(RELAY_TEMP, LOW); // Kipas ON
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Mendekati Tinggi - Kipas ON");
} else {
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Normal - Kipas OFF");
}
Firebase.setBool(firebaseData, "/emergency/low_temperature", false);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
}
}
// Definisi fungsi fuzzy logic untuk kontrol kelembapan
void controlHumidity(float humidity, float temperature) {
float minHumidity = humidityRanges[ageInWeeks-1].min;
float maxHumidity = humidityRanges[ageInWeeks-1].max;
float targetHumidity = humidityRanges[ageInWeeks-1].target;
float minTemp = tempRanges[ageInWeeks-1].min;
float maxTemp = tempRanges[ageInWeeks-1].max;
// Periksa apakah suhu terlalu rendah (kondisi darurat)
bool isTemperatureEmergency = false;
if (temperature < minTemp - 1.5) {
isTemperatureEmergency = true;
}
// Menghitung derajat keanggotaan untuk berbagai kondisi kelembapan
float veryLow = 0; // Kelembapan sangat rendah (kondisi kritis)
float tooLow = 0; // Kelembapan terlalu rendah
float optimal = 0; // Kelembapan optimal
float tooHigh = 0; // Kelembapan terlalu tinggi
float veryHigh = 0; // Kelembapan sangat tinggi (kondisi kritis)
// Keanggotaan "sangat rendah" - kondisi kritis
if (humidity < minHumidity - 5) {
veryLow = 1.0; // Sangat rendah, kondisi kritis
} else if (humidity >= minHumidity - 5 && humidity < minHumidity - 3) {
veryLow = (minHumidity - 3 - humidity) / 2.0; // Agak sangat rendah
veryLow = constrain(veryLow, 0, 1);
}
// Keanggotaan "terlalu rendah"
if (humidity >= minHumidity - 3 && humidity < minHumidity) {
tooLow = (minHumidity - humidity) / 3.0; // Agak rendah
tooLow = constrain(tooLow, 0, 1);
}
// Keanggotaan "optimal"
if (humidity >= minHumidity && humidity <= maxHumidity) {
// Semakin dekat ke targetHumidity, semakin optimal
optimal = 1.0 - abs(humidity - targetHumidity) / ((maxHumidity - minHumidity) / 2.0);
optimal = constrain(optimal, 0, 1);
}
// Keanggotaan "terlalu tinggi"
if (humidity > maxHumidity && humidity <= maxHumidity + 3) {
tooHigh = (humidity - maxHumidity) / 3.0; // Agak tinggi
tooHigh = constrain(tooHigh, 0, 1);
}
// Keanggotaan "sangat tinggi" - kondisi kritis
if (humidity > maxHumidity + 5) {
veryHigh = 1.0; // Sangat tinggi, kondisi kritis
} else if (humidity > maxHumidity + 3 && humidity <= maxHumidity + 5) {
veryHigh = (humidity - (maxHumidity + 3)) / 2.0; // Agak sangat tinggi
veryHigh = constrain(veryHigh, 0, 1);
}
// Kirim nilai fuzzy ke Firebase untuk monitoring
Firebase.setFloat(firebaseData, "/fuzzy/humidity/veryLow", veryLow);
Firebase.setFloat(firebaseData, "/fuzzy/humidity/tooLow", tooLow);
Firebase.setFloat(firebaseData, "/fuzzy/humidity/optimal", optimal);
Firebase.setFloat(firebaseData, "/fuzzy/humidity/tooHigh", tooHigh);
Firebase.setFloat(firebaseData, "/fuzzy/humidity/veryHigh", veryHigh);
// Keputusan berdasarkan derajat keanggotaan dan prioritas
// Jika suhu dalam kondisi darurat, prioritaskan pemanasan
if (isTemperatureEmergency) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Suhu Darurat - Lampu ON untuk Pemanasan");
Firebase.setBool(firebaseData, "/emergency/low_humidity", false); // Reset emergency flag
Firebase.setBool(firebaseData, "/emergency/high_humidity", false); // Reset emergency flag
return; // Keluar dari fungsi, prioritaskan penanganan suhu
}
// Prioritas: Kelembapan sangat tinggi > Kelembapan sangat rendah > Terlalu tinggi > Optimal > Terlalu rendah
if (veryHigh > 0.3) { // Jika kelembapan sangat tinggi (kondisi kritis)
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk mengurangi kelembapan
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Sangat Tinggi - Lampu ON (Kritis)");
Firebase.setBool(firebaseData, "/emergency/high_humidity", true);
Firebase.setBool(firebaseData, "/emergency/low_humidity", false);
} else if (veryLow > 0.3) { // Jika kelembapan sangat rendah (kondisi kritis)
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF untuk mempertahankan kelembapan
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Sangat Rendah - Lampu OFF");
Firebase.setBool(firebaseData, "/emergency/low_humidity", true);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
} else if (tooHigh > 0.4) { // Jika kelembapan terlalu tinggi
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk mengurangi kelembapan
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Tinggi - Lampu ON");
Firebase.setBool(firebaseData, "/emergency/low_humidity", false);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
} else if (optimal > 0.6) { // Jika kelembapan optimal
// Periksa juga suhu - jika suhu rendah, nyalakan lampu meskipun kelembapan optimal
if (temperature < minTemp) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Optimal, Suhu Rendah - Lampu ON");
}
// Jika kelembapan mendekati batas atas rentang optimal, nyalakan lampu
else if (humidity > targetHumidity + ((maxHumidity - minHumidity) / 4)) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Optimal Tinggi - Lampu ON");
} else {
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Optimal - Lampu OFF");
}
Firebase.setBool(firebaseData, "/emergency/low_humidity", false);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
} else if (tooLow > 0.3) { // Jika kelembapan terlalu rendah
// Periksa juga suhu - jika suhu sangat rendah, prioritaskan pemanasan
if (temperature < minTemp - 1) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Rendah, Suhu Sangat Rendah - Lampu ON");
} else {
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF untuk mempertahankan kelembapan
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Rendah - Lampu OFF");
}
Firebase.setBool(firebaseData, "/emergency/low_humidity", true);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
} else {
// Kasus lain, gunakan pendekatan proporsional
// Periksa juga suhu - jika suhu rendah, nyalakan lampu
if (temperature < minTemp) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Normal, Suhu Rendah - Lampu ON");
}
// Jika kelembapan mendekati batas atas, nyalakan lampu
else if (humidity > maxHumidity - 1.5) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Mendekati Tinggi - Lampu ON");
} else {
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Normal - Lampu OFF");
}
Firebase.setBool(firebaseData, "/emergency/low_humidity", false);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
}
}
// Fungsi untuk memeriksa mode kontrol dari Firebase
void checkControlMode() {
// Periksa mode kipas (otomatis/manual)
if (Firebase.getBool(firebaseData, "/control/fan/auto")) {
bool newAutoModeFan = firebaseData.boolData();
if (newAutoModeFan != autoModeFan) {
autoModeFan = newAutoModeFan;
}
}
// Periksa mode lampu (otomatis/manual)
if (Firebase.getBool(firebaseData, "/control/light/auto")) {
bool newAutoModeLight = firebaseData.boolData();
if (newAutoModeLight != autoModeLight) {
autoModeLight = newAutoModeLight;
}
}
// Jika dalam mode manual, ambil status dari Firebase
if (!autoModeFan) {
if (Firebase.getBool(firebaseData, "/control/fan/status")) {
bool newFanStatus = firebaseData.boolData();
if (newFanStatus != fanStatus) {
fanStatus = newFanStatus;
}
}
}
if (!autoModeLight) {
if (Firebase.getBool(firebaseData, "/control/light/status")) {
bool newLightStatus = firebaseData.boolData();
if (newLightStatus != lightStatus) {
lightStatus = newLightStatus;
}
}
}
}
// Tambahkan fungsi untuk memeriksa LCD secara berkala
void checkLCDConnection() {
static unsigned long lastLCDCheck = 0;
static bool lastLCDStatus = true;
// Periksa LCD setiap 30 detik
if (millis() - lastLCDCheck > 30000) {
Wire.beginTransmission(0x27); // Alamat I2C LCD
byte error = Wire.endTransmission();
bool currentLCDStatus = (error == 0);
// Jika status berubah, kirim log
if (currentLCDStatus != lastLCDStatus) {
if (currentLCDStatus) {
// LCD terdeteksi kembali, jalankan proses inisialisasi lengkap
logToFirebase("/logs/lcd", "LCD Connection Restored");
logToFirebase("/logs/lcd", "LCD Initialization Started");
// Coba inisialisasi LCD
lcd.init(); // Panggil init() tanpa memeriksa nilai kembali
lcd.backlight();
lcd.clear();
lcd.begin(16,2);
lcd.print("Reconnected...");
// Anggap inisialisasi berhasil jika kita sampai di sini
logToFirebase("/logs/lcd", "LCD Ready and Initialized");
Serial.println("LCD reinitialized successfully");
} else {
logToFirebase("/logs/lcd", "LCD Connection Lost - Check Wiring");
}
lastLCDStatus = currentLCDStatus;
}
lastLCDCheck = millis();
}
}
// Fungsi untuk memperbarui rentang dari Firebase
void updateRangesFromFirebase() {
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 5000) { // Update setiap 5 detik
String path = "/ranges/week" + String(ageInWeeks);
// Ambil rentang suhu
if (Firebase.getFloat(firebaseData, path + "/temperature/min")) {
tempRanges[ageInWeeks-1].min = firebaseData.floatData();
}
if (Firebase.getFloat(firebaseData, path + "/temperature/max")) {
tempRanges[ageInWeeks-1].max = firebaseData.floatData();
}
if (Firebase.getFloat(firebaseData, path + "/temperature/target")) {
tempRanges[ageInWeeks-1].target = firebaseData.floatData();
}
// Ambil rentang kelembapan
if (Firebase.getFloat(firebaseData, path + "/humidity/min")) {
humidityRanges[ageInWeeks-1].min = firebaseData.floatData();
}
if (Firebase.getFloat(firebaseData, path + "/humidity/max")) {
humidityRanges[ageInWeeks-1].max = firebaseData.floatData();
}
if (Firebase.getFloat(firebaseData, path + "/humidity/target")) {
humidityRanges[ageInWeeks-1].target = firebaseData.floatData();
}
lastUpdate = millis();
}
}

196
lib/background_service.dart Normal file
View File

@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'dart:async'; // Import for Timer
import 'services/notification_service.dart'; // Import layanan notifikasi
String lastLcdStatus = ""; // Menyimpan status terakhir LCD
String lastSensorStatus = ""; // Menyimpan status terakhir Sensor
String lastWifiStatus = ""; // Menyimpan status terakhir WiFi
DateTime? lastSensorResponseTime; // Menyimpan waktu terakhir sensor merespon
Timer? wifiCheckTimer; // Timer untuk memeriksa koneksi WiFi berdasarkan respons sensor
void backgroundService() {
WidgetsFlutterBinding.ensureInitialized();
FlutterBackgroundService.initialize(onStart);
print("Background service initialized...");
print("Background service is running...");
// Inisialisasi Firebase Database
final DatabaseReference lcdRef = FirebaseDatabase.instance.ref().child('logs/lcd');
final DatabaseReference sensorRef = FirebaseDatabase.instance.ref().child('logs/sensor');
// Memulai timer untuk memeriksa koneksi WiFi setiap 30 detik
wifiCheckTimer = Timer.periodic(Duration(seconds: 30), (timer) {
checkWifiStatusBasedOnSensor();
});
// Inisialisasi layanan notifikasi untuk monitoring suhu dan kelembapan
NotificationService().initialize().then((_) {
print("✅ Layanan notifikasi suhu dan kelembapan berhasil diinisialisasi di background service");
});
lcdRef.onValue.listen((event) {
print("Listener untuk LCD dipanggil");
if (event.snapshot.value != null) {
if (event.snapshot.value is String) {
var value = event.snapshot.value as String;
print("Data LCD diterima: $value"); // Log data LCD
var parts = value.split(':'); // Memecah string menjadi bagian-bagian
if (parts.length > 1) {
var status = parts[1].trim(); // Mengambil status dari bagian kedua
print("Status LCD: $status");
if (status.contains("Initializing")) {
print("LCD Initialization Started");
if (lastLcdStatus != status) {
updateNotification("LCD Status", "Inisialisasi LCD dimulai", 1);
lastLcdStatus = status;
}
} else if (status.contains("Ready")) {
print("LCD Ready and Initialized");
if (lastLcdStatus != status) {
updateNotification("LCD Status", "LCD siap dan terinisialisasi", 1);
lastLcdStatus = status;
}
} else if (status.contains("Failed")) {
print("LCD Initialization Failed");
if (lastLcdStatus != status) {
updateNotification("LCD Status", "Inisialisasi LCD gagal - Periksa koneksi", 1);
lastLcdStatus = status;
}
} else if (status.contains("Lost")) {
print("LCD Connection Lost");
if (lastLcdStatus != status) {
updateNotification("LCD Status", "Koneksi LCD terputus - Periksa kabel", 1);
lastLcdStatus = status;
}
} else if (status.contains("Restored")) {
print("LCD Connection Restored");
if (lastLcdStatus != status) {
updateNotification("LCD Status", "Koneksi LCD dipulihkan", 1);
lastLcdStatus = status;
}
} else {
print("LCD Status not recognized: $status"); // Debugging line
}
} else {
print("Invalid LCD data format"); // Debugging line
}
} else {
print("Data LCD tidak dalam format String"); // Debugging line
}
} else {
print("Data LCD tidak ada");
}
});
sensorRef.onValue.listen((event) {
if (event.snapshot.value != null) {
// Update waktu terakhir sensor merespon
lastSensorResponseTime = DateTime.now();
print("Sensor merespon pada: $lastSensorResponseTime");
// Periksa status WiFi berdasarkan respons sensor
checkWifiStatusBasedOnSensor();
if (event.snapshot.value is String) {
var value = event.snapshot.value as String;
print("Data Sensor diterima: $value"); // Log data sensor
var parts = value.split(':'); // Memecah string menjadi bagian-bagian
if (parts.length > 1) {
var status = parts[1].trim(); // Mengambil status dari bagian kedua
print("Status Sensor: $status"); // Menambahkan log untuk status sensor
if (status == 'Failed to Read from DHT Sensor') {
print("Failed to Read from DHT Sensor");
if (lastSensorStatus != status) {
updateNotification("Sensor Status", "Gagal membaca dari sensor DHT", 2);
lastSensorStatus = status;
}
} else if (status == 'DHT Sensor Reading Successful') {
print("DHT Sensor Reading Successful");
if (lastSensorStatus != status) {
updateNotification("Sensor Status", "Pembacaan sensor DHT berhasil", 2);
lastSensorStatus = status;
}
} else {
print("Sensor Status not recognized: $status"); // Debugging line
}
} else {
print("Invalid Sensor data format"); // Debugging line
}
} else {
print("Data Sensor tidak dalam format String"); // Debugging line
}
} else {
print("Data Sensor tidak ada");
}
});
}
// Fungsi untuk memeriksa status WiFi berdasarkan respons sensor
void checkWifiStatusBasedOnSensor() {
if (lastSensorResponseTime == null) {
// Jika belum ada respons sensor sama sekali
if (lastWifiStatus != "Disconnected") {
updateNotification("WiFi Status", "WiFi terputus", 3);
lastWifiStatus = "Disconnected";
print("WiFi dianggap terputus karena belum ada respons sensor");
}
return;
}
// Hitung selisih waktu antara sekarang dan respons sensor terakhir
Duration timeSinceLastResponse = DateTime.now().difference(lastSensorResponseTime!);
print("Waktu sejak respons sensor terakhir: ${timeSinceLastResponse.inSeconds} detik");
// Jika sensor tidak merespon dalam 60 detik, anggap WiFi terputus
if (timeSinceLastResponse.inSeconds > 60) {
if (lastWifiStatus != "Disconnected") {
updateNotification("WiFi Status", "WiFi terputus", 3);
lastWifiStatus = "Disconnected";
print("WiFi dianggap terputus karena sensor tidak merespon selama 60 detik");
}
} else {
// Jika sensor merespon dalam 60 detik terakhir, anggap WiFi terhubung
if (lastWifiStatus != "Connected") {
updateNotification("WiFi Status", "WiFi terhubung", 3);
lastWifiStatus = "Connected";
print("WiFi dianggap terhubung karena sensor merespon dalam 60 detik terakhir");
}
}
}
void onStart() {
updateNotification("Service Status", "Background Service sedang berjalan...", 0);
// Inisialisasi layanan notifikasi untuk monitoring suhu dan kelembapan
NotificationService().initialize().then((_) {
print("✅ Layanan notifikasi suhu dan kelembapan berhasil diinisialisasi di onStart");
});
backgroundService();
}
void updateNotification(String title, String body, int notificationId) {
print("Menampilkan notifikasi: $title - $body");
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails(
'Jago',
'Notification',
channelDescription: 'Cek Notification!!!',
importance: Importance.max,
priority: Priority.high,
);
const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
flutterLocalNotificationsPlugin.show(
notificationId,
title, // Judul notifikasi yang relevan
body, // Isi notifikasi yang relevan
platformChannelSpecifics,
payload: 'item x',
);
}

24
lib/firebase_options.dart Normal file
View File

@ -0,0 +1,24 @@
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform;
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyD5Rn71-iCp3CZUnatbCaNJWzGv4S3wVk4',
appId: '1:973274985069:android:e1382cbb4d2e6c79de4564',
messagingSenderId: '973274985069',
projectId: 'jago-9a9a6',
databaseURL: 'https://jago-9a9a6-default-rtdb.firebaseio.com',
storageBucket: 'jago-9a9a6.firebasestorage.app',
);
}

694
lib/kode.ino Normal file
View File

@ -0,0 +1,694 @@
#include <ESP8266WiFi.h>
#include <FirebaseESP8266.h>
#include <DHT.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
// Konfigurasi WiFi
const char* ssid = "Combat2024";
const char* password = "12345678";
// Konfigurasi Firebase
#define FIREBASE_HOST "https://jago-9a9a6-default-rtdb.firebaseio.com/"
#define FIREBASE_AUTH "Q3i1jDmc3Xh9UiUTaGUmOw4tVztV5TJ3INU9RoWN"
// NTP Client untuk mendapatkan waktu
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
FirebaseData firebaseData;
FirebaseConfig config;
FirebaseAuth auth;
#define DHTPIN D5
#define RELAY_TEMP D3 // Relay untuk suhu (kipas)
#define RELAY_HUMIDITY D6 // Relay untuk kelembapan (lampu)
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
LiquidCrystal_I2C lcd(0x27, 16, 2); // Pastikan alamat sesuai dengan modul yang digunakan
int ageInWeeks = 1; // Default, nanti diambil dari Firebase
// Variabel untuk mode kontrol
bool autoModeFan = true; // Default: mode otomatis untuk kipas
bool autoModeLight = true; // Default: mode otomatis untuk lampu
bool fanStatus = false; // Status kipas (ON/OFF)
bool lightStatus = false; // Status lampu (ON/OFF)
// Variabel untuk pengaturan tampilan LCD
unsigned long lastDisplayToggle = 0;
bool showMainDisplay = true; // Untuk mengganti tampilan LCD
// Variabel untuk reconnect
unsigned long lastReconnectAttempt = 0;
const unsigned long reconnectInterval = 30000; // 30 detik
// Variabel untuk menyimpan status koneksi WiFi sebelumnya
bool previousWiFiStatus = false;
// Fungsi untuk logging ke Firebase dengan timestamp
void logToFirebase(const String& path, const String& message) {
String timestamp = getCurrentTimestamp();
String logMessage = timestamp + ": " + message;
// Kirim log ke Firebase, menggunakan setString untuk memperbarui entri
Firebase.setString(firebaseData, path, logMessage);
}
// Fungsi untuk mendapatkan waktu dari NTP
String getCurrentTimestamp() {
timeClient.update();
return String(timeClient.getEpochTime());
}
// Variabel untuk memeriksa koneksi WiFi
unsigned long lastWiFiCheck = 0;
const unsigned long wifiCheckInterval = 2000; // 2 detik
// Fungsi untuk memeriksa dan memulihkan koneksi WiFi
void checkWiFiConnection() {
if (WiFi.status() != WL_CONNECTED) {
unsigned long currentMillis = millis();
// Coba reconnect setiap interval tertentu
if (currentMillis - lastReconnectAttempt >= reconnectInterval) {
logToFirebase("/logs/wifi", "WiFi Disconnected. Attempting to Reconnect");
// Matikan WiFi dan nyalakan kembali
WiFi.disconnect();
delay(1000);
WiFi.begin(ssid, password);
lastReconnectAttempt = currentMillis;
}
// Tambahkan log ketika WiFi terputus
if (previousWiFiStatus) {
logToFirebase("/logs/wifi", "WiFi Connection Failed");
previousWiFiStatus = false;
}
} else {
// Jika WiFi terhubung, periksa apakah statusnya berubah
if (!previousWiFiStatus) {
logToFirebase("/logs/wifi", "WiFi Connected Successfully");
Serial.println("\nWiFi connected");
previousWiFiStatus = true;
}
}
}
void setup() {
Serial.begin(115200);
// Inisialisasi pin relay
pinMode(RELAY_TEMP, OUTPUT);
pinMode(RELAY_HUMIDITY, OUTPUT);
digitalWrite(RELAY_TEMP, HIGH); // Pastikan relay mulai dalam keadaan OFF
digitalWrite(RELAY_HUMIDITY, HIGH);
// Inisialisasi LCD dengan deteksi kesalahan
logToFirebase("/logs/lcd", "LCD Initialization Started");
// Coba inisialisasi LCD dengan timeout
bool lcdInitSuccess = false;
Wire.begin();
// Periksa apakah LCD merespons
Wire.beginTransmission(0x27); // Alamat I2C LCD
byte error = Wire.endTransmission();
if (error == 0) {
// LCD ditemukan, coba inisialisasi
lcd.init();
lcd.backlight();
lcd.clear();
lcd.begin(16,2);
lcd.print("Initializing...");
lcdInitSuccess = true;
}
if (lcdInitSuccess) {
logToFirebase("/logs/lcd", "LCD Ready and Initialized");
Serial.println("LCD initialized successfully");
} else {
logToFirebase("/logs/lcd", "LCD Initialization Failed - Check Connections");
Serial.println("LCD initialization failed! Check wiring (GND, VCC, SDA, SCL)");
}
// Inisialisasi WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
// Tunggu koneksi WiFi
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
logToFirebase("/logs/wifi", "WiFi Connected Successfully");
Serial.println("\nWiFi connected");
// Inisialisasi NTP Client
timeClient.begin();
timeClient.setTimeOffset(25200); // Offset untuk WIB (GMT+7)
} else {
logToFirebase("/logs/wifi", "WiFi Connection Failed");
Serial.println("\nWiFi connection failed");
}
// Inisialisasi DHT
dht.begin();
logToFirebase("/logs/sensor", "DHT Sensor Initialized");
// Konfigurasi Firebase
config.host = FIREBASE_HOST;
config.signer.tokens.legacy_token = FIREBASE_AUTH;
Firebase.begin(&config, &auth);
Firebase.reconnectWiFi(true);
// Inisialisasi variabel kontrol di Firebase
Firebase.setBool(firebaseData, "/control/fan/auto", autoModeFan);
Firebase.setBool(firebaseData, "/control/light/auto", autoModeLight);
Firebase.setBool(firebaseData, "/control/fan/status", fanStatus);
Firebase.setBool(firebaseData, "/control/light/status", lightStatus);
}
void loop() {
unsigned long currentMillis = millis();
// Periksa koneksi WiFi setiap 2 detik
if (currentMillis - lastWiFiCheck >= wifiCheckInterval) {
checkWiFiConnection();
lastWiFiCheck = currentMillis;
}
// Periksa koneksi LCD secara berkala
checkLCDConnection();
// Update NTP Client
timeClient.update();
// Baca sensor suhu dan kelembapan
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
// Periksa pembacaan sensor
if (isnan(temperature) || isnan(humidity)) {
logToFirebase("/logs/sensor", "Failed to Read from DHT Sensor");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Sensor Error!");
lcd.setCursor(0, 1);
lcd.print("Check DHT sensor");
delay(2000);
return;
} else {
// Log pembacaan sensor berhasil (setiap 2 detik)
static unsigned long lastSensorLog = 0;
if (millis() - lastSensorLog > 2000) { // Log setiap 2 detik
logToFirebase("/logs/sensor", "DHT Sensor Reading Successful");
lastSensorLog = millis();
}
}
// Ambil umur ayam dari Firebase
if (Firebase.getInt(firebaseData, "/chicken_age")) {
ageInWeeks = firebaseData.intData();
Serial.print("Age: ");
Serial.println(ageInWeeks);
} else {
Serial.print("Failed to get age from Firebase: ");
Serial.println(firebaseData.errorReason());
// Inisialisasi jika data belum ada
if (firebaseData.errorReason() == "path not exist") {
Firebase.setInt(firebaseData, "/chicken_age", 1);
logToFirebase("/logs/system", "Initialized /chicken_age with default value 1");
}
}
// Periksa mode kontrol (manual atau otomatis)
checkControlMode();
// Ganti tampilan LCD setiap 5 detik
if (currentMillis - lastDisplayToggle >= 5000) {
showMainDisplay = !showMainDisplay;
lastDisplayToggle = currentMillis;
}
// Update tampilan LCD berdasarkan mode
if (showMainDisplay) {
updateMainDisplay(temperature, humidity);
} else {
updateModeDisplay();
}
// Kontrol suhu dan kelembapan berdasarkan mode
if (autoModeFan) {
// Mode otomatis untuk kipas (menggunakan fuzzy logic)
controlTemperature(temperature);
} else {
// Mode manual untuk kipas
if (fanStatus) {
digitalWrite(RELAY_TEMP, LOW); // Kipas ON
} else {
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
}
}
if (autoModeLight) {
// Mode otomatis untuk lampu (menggunakan fuzzy logic)
controlHumidity(humidity, temperature);
} else {
// Mode manual untuk lampu
if (lightStatus) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON
} else {
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF
}
}
// Kirim data ke Firebase
Firebase.setFloat(firebaseData, "/sensor/temperature", temperature);
Firebase.setFloat(firebaseData, "/sensor/Humidity", humidity);
Firebase.setBool(firebaseData, "/relay/Kipas", digitalRead(RELAY_TEMP) == LOW);
Firebase.setBool(firebaseData, "/relay/Lampu", digitalRead(RELAY_HUMIDITY) == LOW);
delay(1000); // Tunggu 1 detik antara siklus
}
// Fungsi untuk tampilan utama (Temp, Hum, Age)
void updateMainDisplay(float temperature, float humidity) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Temp:");
lcd.print(temperature, 1);
lcd.print((char)223); // Simbol derajat
lcd.print("C");
lcd.setCursor(0, 1);
lcd.print("Hum:");
lcd.print(humidity, 1);
lcd.print("% Age:");
lcd.print(ageInWeeks);
lcd.print("w");
}
// Fungsi untuk tampilan mode (Auto/Manual)
void updateModeDisplay() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Fan: ");
lcd.print(autoModeFan ? "AUTO" : "MANUAL");
lcd.print(" ");
lcd.print(digitalRead(RELAY_TEMP) == LOW ? "ON" : "OFF");
lcd.setCursor(0, 1);
lcd.print("Light: ");
lcd.print(autoModeLight ? "AUTO" : "MANUAL");
lcd.print(" ");
lcd.print(digitalRead(RELAY_HUMIDITY) == LOW ? "ON" : "OFF");
}
// Definisi fungsi fuzzy logic untuk kontrol suhu
void controlTemperature(float temp) {
float minTemp, maxTemp, targetTemp;
// Rentang suhu ideal berdasarkan umur ayam
switch (ageInWeeks) {
case 1: minTemp = 33; maxTemp = 35; targetTemp = 34; break;
case 2: minTemp = 30; maxTemp = 33; targetTemp = 31.5; break;
case 3: minTemp = 28; maxTemp = 30; targetTemp = 29; break;
case 4: minTemp = 25; maxTemp = 28; targetTemp = 26.5; break;
default: minTemp = 25; maxTemp = 28; targetTemp = 26.5;
}
// Menghitung derajat keanggotaan untuk berbagai kondisi suhu
float veryLow = 0; // Suhu sangat rendah (kondisi kritis)
float tooLow = 0; // Suhu terlalu rendah
float optimal = 0; // Suhu optimal
float tooHigh = 0; // Suhu terlalu tinggi
float veryHigh = 0; // Suhu sangat tinggi (kondisi kritis)
// Keanggotaan "sangat rendah" - kondisi kritis
if (temp < minTemp - 2) {
veryLow = 1.0; // Sangat rendah, kondisi kritis
} else if (temp >= minTemp - 2 && temp < minTemp - 1) {
veryLow = (minTemp - 1 - temp); // Agak sangat rendah
veryLow = constrain(veryLow, 0, 1);
}
// Keanggotaan "terlalu rendah"
if (temp >= minTemp - 1.5 && temp < minTemp) {
tooLow = (minTemp - temp) / 1.5; // Agak rendah
tooLow = constrain(tooLow, 0, 1);
}
// Keanggotaan "optimal"
if (temp >= minTemp && temp <= maxTemp) {
// Semakin dekat ke targetTemp, semakin optimal
optimal = 1.0 - abs(temp - targetTemp) / ((maxTemp - minTemp) / 2.0);
optimal = constrain(optimal, 0, 1);
}
// Keanggotaan "terlalu tinggi"
if (temp > maxTemp && temp <= maxTemp + 1.5) {
tooHigh = (temp - maxTemp) / 1.5; // Agak tinggi
tooHigh = constrain(tooHigh, 0, 1);
}
// Keanggotaan "sangat tinggi" - kondisi kritis
if (temp > maxTemp + 2) {
veryHigh = 1.0; // Sangat tinggi, kondisi kritis
} else if (temp > maxTemp + 1 && temp <= maxTemp + 2) {
veryHigh = (temp - (maxTemp + 1)) / 1.0; // Agak sangat tinggi
veryHigh = constrain(veryHigh, 0, 1);
}
// Simpan status suhu untuk digunakan oleh fungsi controlHumidity
bool isTempVeryCold = (veryLow > 0.3);
bool isTempTooCold = (tooLow > 0.3);
bool isTempVeryHot = (veryHigh > 0.3);
// Kirim nilai fuzzy ke Firebase untuk monitoring
Firebase.setFloat(firebaseData, "/fuzzy/temperature/veryLow", veryLow);
Firebase.setFloat(firebaseData, "/fuzzy/temperature/tooLow", tooLow);
Firebase.setFloat(firebaseData, "/fuzzy/temperature/optimal", optimal);
Firebase.setFloat(firebaseData, "/fuzzy/temperature/tooHigh", tooHigh);
Firebase.setFloat(firebaseData, "/fuzzy/temperature/veryHigh", veryHigh);
// Keputusan berdasarkan derajat keanggotaan dan prioritas
// Prioritas: Sangat tinggi > Sangat rendah > Terlalu tinggi > Optimal > Terlalu rendah
if (veryHigh > 0.3) { // Jika suhu sangat tinggi (kondisi kritis)
digitalWrite(RELAY_TEMP, LOW); // Kipas ON pada kecepatan maksimal
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Sangat Tinggi - Kipas ON (Kritis)");
Firebase.setBool(firebaseData, "/emergency/high_temperature", true);
} else if (veryLow > 0.3) { // Jika suhu sangat rendah (kondisi kritis)
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
// Nyalakan lampu untuk membantu meningkatkan suhu
// Ini akan di-override oleh controlHumidity jika diperlukan
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Sangat Rendah - Kipas OFF, Lampu ON");
Firebase.setBool(firebaseData, "/emergency/low_temperature", true);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
} else if (tooHigh > 0.3) { // Jika suhu terlalu tinggi
digitalWrite(RELAY_TEMP, LOW); // Kipas ON
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Tinggi - Kipas ON");
Firebase.setBool(firebaseData, "/emergency/low_temperature", false);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
} else if (optimal > 0.7) { // Jika suhu sangat optimal
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Optimal - Kipas OFF");
Firebase.setBool(firebaseData, "/emergency/low_temperature", false);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
} else if (tooLow > 0.3) { // Jika suhu terlalu rendah
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Rendah - Kipas OFF");
Firebase.setBool(firebaseData, "/emergency/low_temperature", true);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
} else {
// Kasus lain, gunakan pendekatan proporsional
// Jika suhu mendekati batas atas rentang, nyalakan kipas
float distanceToMax = maxTemp - temp;
if (distanceToMax < 1.0) {
digitalWrite(RELAY_TEMP, LOW); // Kipas ON
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Mendekati Tinggi - Kipas ON");
} else {
digitalWrite(RELAY_TEMP, HIGH); // Kipas OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/temperature", "Normal - Kipas OFF");
}
Firebase.setBool(firebaseData, "/emergency/low_temperature", false);
Firebase.setBool(firebaseData, "/emergency/high_temperature", false);
}
}
// Definisi fungsi fuzzy logic untuk kontrol kelembapan
void controlHumidity(float humidity, float temperature) {
float minHumidity, maxHumidity, targetHumidity;
float minTemp, maxTemp;
// Rentang kelembapan ideal berdasarkan umur ayam
switch (ageInWeeks) {
case 1:
minHumidity = 60; maxHumidity = 70; targetHumidity = 65.0;
minTemp = 33; maxTemp = 35;
break;
case 2:
minHumidity = 60; maxHumidity = 65; targetHumidity = 62.5;
minTemp = 30; maxTemp = 33;
break;
case 3:
minHumidity = 60; maxHumidity = 65; targetHumidity = 62.5;
minTemp = 28; maxTemp = 30;
break;
case 4:
minHumidity = 55; maxHumidity = 60; targetHumidity = 57.5;
minTemp = 25; maxTemp = 28;
break;
default:
minHumidity = 55; maxHumidity = 60; targetHumidity = 57.5;
minTemp = 25; maxTemp = 28;
}
// Periksa apakah suhu terlalu rendah (kondisi darurat)
bool isTemperatureEmergency = false;
if (temperature < minTemp - 1.5) {
isTemperatureEmergency = true;
}
// Menghitung derajat keanggotaan untuk berbagai kondisi kelembapan
float veryLow = 0; // Kelembapan sangat rendah (kondisi kritis)
float tooLow = 0; // Kelembapan terlalu rendah
float optimal = 0; // Kelembapan optimal
float tooHigh = 0; // Kelembapan terlalu tinggi
float veryHigh = 0; // Kelembapan sangat tinggi (kondisi kritis)
// Keanggotaan "sangat rendah" - kondisi kritis
if (humidity < minHumidity - 5) {
veryLow = 1.0; // Sangat rendah, kondisi kritis
} else if (humidity >= minHumidity - 5 && humidity < minHumidity - 3) {
veryLow = (minHumidity - 3 - humidity) / 2.0; // Agak sangat rendah
veryLow = constrain(veryLow, 0, 1);
}
// Keanggotaan "terlalu rendah"
if (humidity >= minHumidity - 3 && humidity < minHumidity) {
tooLow = (minHumidity - humidity) / 3.0; // Agak rendah
tooLow = constrain(tooLow, 0, 1);
}
// Keanggotaan "optimal"
if (humidity >= minHumidity && humidity <= maxHumidity) {
// Semakin dekat ke targetHumidity, semakin optimal
optimal = 1.0 - abs(humidity - targetHumidity) / ((maxHumidity - minHumidity) / 2.0);
optimal = constrain(optimal, 0, 1);
}
// Keanggotaan "terlalu tinggi"
if (humidity > maxHumidity && humidity <= maxHumidity + 3) {
tooHigh = (humidity - maxHumidity) / 3.0; // Agak tinggi
tooHigh = constrain(tooHigh, 0, 1);
}
// Keanggotaan "sangat tinggi" - kondisi kritis
if (humidity > maxHumidity + 5) {
veryHigh = 1.0; // Sangat tinggi, kondisi kritis
} else if (humidity > maxHumidity + 3 && humidity <= maxHumidity + 5) {
veryHigh = (humidity - (maxHumidity + 3)) / 2.0; // Agak sangat tinggi
veryHigh = constrain(veryHigh, 0, 1);
}
// Kirim nilai fuzzy ke Firebase untuk monitoring
Firebase.setFloat(firebaseData, "/fuzzy/humidity/veryLow", veryLow);
Firebase.setFloat(firebaseData, "/fuzzy/humidity/tooLow", tooLow);
Firebase.setFloat(firebaseData, "/fuzzy/humidity/optimal", optimal);
Firebase.setFloat(firebaseData, "/fuzzy/humidity/tooHigh", tooHigh);
Firebase.setFloat(firebaseData, "/fuzzy/humidity/veryHigh", veryHigh);
// Keputusan berdasarkan derajat keanggotaan dan prioritas
// Jika suhu dalam kondisi darurat, prioritaskan pemanasan
if (isTemperatureEmergency) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Suhu Darurat - Lampu ON untuk Pemanasan");
Firebase.setBool(firebaseData, "/emergency/low_humidity", false); // Reset emergency flag
Firebase.setBool(firebaseData, "/emergency/high_humidity", false); // Reset emergency flag
return; // Keluar dari fungsi, prioritaskan penanganan suhu
}
// Prioritas: Kelembapan sangat tinggi > Kelembapan sangat rendah > Terlalu tinggi > Optimal > Terlalu rendah
if (veryHigh > 0.3) { // Jika kelembapan sangat tinggi (kondisi kritis)
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk mengurangi kelembapan
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Sangat Tinggi - Lampu ON (Kritis)");
Firebase.setBool(firebaseData, "/emergency/high_humidity", true);
Firebase.setBool(firebaseData, "/emergency/low_humidity", false);
} else if (veryLow > 0.3) { // Jika kelembapan sangat rendah (kondisi kritis)
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF untuk mempertahankan kelembapan
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Sangat Rendah - Lampu OFF");
Firebase.setBool(firebaseData, "/emergency/low_humidity", true);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
} else if (tooHigh > 0.4) { // Jika kelembapan terlalu tinggi
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk mengurangi kelembapan
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Tinggi - Lampu ON");
Firebase.setBool(firebaseData, "/emergency/low_humidity", false);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
} else if (optimal > 0.6) { // Jika kelembapan optimal
// Periksa juga suhu - jika suhu rendah, nyalakan lampu meskipun kelembapan optimal
if (temperature < minTemp) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Optimal, Suhu Rendah - Lampu ON");
}
// Jika kelembapan mendekati batas atas rentang optimal, nyalakan lampu
else if (humidity > targetHumidity + ((maxHumidity - minHumidity) / 4)) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Optimal Tinggi - Lampu ON");
} else {
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Optimal - Lampu OFF");
}
Firebase.setBool(firebaseData, "/emergency/low_humidity", false);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
} else if (tooLow > 0.3) { // Jika kelembapan terlalu rendah
// Periksa juga suhu - jika suhu sangat rendah, prioritaskan pemanasan
if (temperature < minTemp - 1) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Rendah, Suhu Sangat Rendah - Lampu ON");
} else {
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF untuk mempertahankan kelembapan
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Rendah - Lampu OFF");
}
Firebase.setBool(firebaseData, "/emergency/low_humidity", true);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
} else {
// Kasus lain, gunakan pendekatan proporsional
// Periksa juga suhu - jika suhu rendah, nyalakan lampu
if (temperature < minTemp) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON untuk membantu meningkatkan suhu
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Normal, Suhu Rendah - Lampu ON");
}
// Jika kelembapan mendekati batas atas, nyalakan lampu
else if (humidity > maxHumidity - 1.5) {
digitalWrite(RELAY_HUMIDITY, LOW); // Lampu ON
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Mendekati Tinggi - Lampu ON");
} else {
digitalWrite(RELAY_HUMIDITY, HIGH); // Lampu OFF
// Log status ke Firebase
Firebase.setString(firebaseData, "/status/humidity", "Normal - Lampu OFF");
}
Firebase.setBool(firebaseData, "/emergency/low_humidity", false);
Firebase.setBool(firebaseData, "/emergency/high_humidity", false);
}
}
// Fungsi untuk memeriksa mode kontrol dari Firebase
void checkControlMode() {
// Periksa mode kipas (otomatis/manual)
if (Firebase.getBool(firebaseData, "/control/fan/auto")) {
bool newAutoModeFan = firebaseData.boolData();
if (newAutoModeFan != autoModeFan) {
autoModeFan = newAutoModeFan;
}
}
// Periksa mode lampu (otomatis/manual)
if (Firebase.getBool(firebaseData, "/control/light/auto")) {
bool newAutoModeLight = firebaseData.boolData();
if (newAutoModeLight != autoModeLight) {
autoModeLight = newAutoModeLight;
}
}
// Jika dalam mode manual, ambil status dari Firebase
if (!autoModeFan) {
if (Firebase.getBool(firebaseData, "/control/fan/status")) {
bool newFanStatus = firebaseData.boolData();
if (newFanStatus != fanStatus) {
fanStatus = newFanStatus;
}
}
}
if (!autoModeLight) {
if (Firebase.getBool(firebaseData, "/control/light/status")) {
bool newLightStatus = firebaseData.boolData();
if (newLightStatus != lightStatus) {
lightStatus = newLightStatus;
}
}
}
}
// Tambahkan fungsi untuk memeriksa LCD secara berkala
void checkLCDConnection() {
static unsigned long lastLCDCheck = 0;
static bool lastLCDStatus = true;
// Periksa LCD setiap 30 detik
if (millis() - lastLCDCheck > 30000) {
Wire.beginTransmission(0x27); // Alamat I2C LCD
byte error = Wire.endTransmission();
bool currentLCDStatus = (error == 0);
// Jika status berubah, kirim log
if (currentLCDStatus != lastLCDStatus) {
if (currentLCDStatus) {
// LCD terdeteksi kembali, jalankan proses inisialisasi lengkap
logToFirebase("/logs/lcd", "LCD Connection Restored");
logToFirebase("/logs/lcd", "LCD Initialization Started");
// Coba inisialisasi LCD
lcd.init(); // Panggil init() tanpa memeriksa nilai kembali
lcd.backlight();
lcd.clear();
lcd.begin(16,2);
lcd.print("Reconnected...");
// Anggap inisialisasi berhasil jika kita sampai di sini
logToFirebase("/logs/lcd", "LCD Ready and Initialized");
Serial.println("LCD reinitialized successfully");
} else {
logToFirebase("/logs/lcd", "LCD Connection Lost - Check Wiring");
}
lastLCDStatus = currentLCDStatus;
}
lastLCDCheck = millis();
}
}

106
lib/main.dart Normal file
View File

@ -0,0 +1,106 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'screens/splash.dart'; // Import splash.dart
import 'firebase_options.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'background_service.dart'; // Import the background service
import 'package:firebase_app_check/firebase_app_check.dart';
import 'services/notification_service.dart'; // Import layanan notifikasi baru
import 'services/history_service.dart'; // Import layanan history
// Pastikan path ini sesuai
// Initialize the FlutterLocalNotificationsPlugin
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
Future<void> main() async {
try {
WidgetsFlutterBinding.ensureInitialized(); // Pastikan Flutter binding sudah diinisialisasi
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// Tambahkan ini setelah Firebase.initializeApp()
await FirebaseAppCheck.instance.activate(
// Untuk Android
androidProvider: AndroidProvider.playIntegrity,
);
// Initialize local notifications
const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// Tampilkan notifikasi saat aplikasi aktif
_showNotification(message);
});
backgroundService(); // Start the background service
// Inisialisasi layanan notifikasi untuk monitoring suhu dan kelembapan
await NotificationService().initialize();
print("✅ Layanan notifikasi suhu dan kelembapan berhasil diinisialisasi");
// Inisialisasi layanan history untuk pencatatan data history
await HistoryService().initialize();
print("✅ Layanan history berhasil diinisialisasi");
runApp(MyApp());
} catch (e) {
print('Error initializing Firebase: $e');
// You might want to show an error screen instead
runApp(ErrorApp());
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Jago',
home: SplashScreen(), // Set SplashScreen sebagai halaman awal
);
}
}
// Add an error app widget to handle initialization failures
class ErrorApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Text(
'Failed to initialize app. Please check your connection and try again.',
textAlign: TextAlign.center,
),
),
),
);
}
}
Future<void> _showNotification(RemoteMessage message) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails(
'Jago',
'Notification',
channelDescription: 'Cek Notification!!!',
importance: Importance.max,
priority: Priority.high,
);
const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
message.notification?.title,
message.notification?.body,
platformChannelSpecifics,
payload: 'item x',
);
}

View File

@ -0,0 +1,32 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class SensorData {
final String name;
final double value;
final DateTime timestamp;
SensorData({
required this.name,
required this.value,
required this.timestamp,
});
// Konversi dari Firestore ke SensorData
factory SensorData.fromFirestore(DocumentSnapshot doc) {
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
return SensorData(
name: data['name'] ?? '',
value: (data['value'] ?? 0.0).toDouble(),
timestamp: (data['timestamp'] as Timestamp).toDate(),
);
}
// Konversi ke format Map untuk Firestore
Map<String, dynamic> toMap() {
return {
'name': name,
'value': value,
'timestamp': Timestamp.fromDate(timestamp),
};
}
}

6
lib/models/user.dart Normal file
View File

@ -0,0 +1,6 @@
class User {
final String username;
final String password;
User({required this.username, required this.password});
}

View File

@ -0,0 +1,495 @@
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
class AgeControlPage extends StatefulWidget {
@override
_AgeControlPageState createState() => _AgeControlPageState();
}
class _AgeControlPageState extends State<AgeControlPage> {
final DatabaseReference _dbRef = FirebaseDatabase.instance.ref();
int ageInWeeks = 1; // Default umur ayam
Map<String, dynamic> ranges = {};
bool showRangeControl = false;
@override
void initState() {
super.initState();
_fetchAgeFromFirebase();
_fetchRangesFromFirebase();
}
// Ambil umur ayam dari Firebase
void _fetchAgeFromFirebase() {
_dbRef.child("chicken_age").onValue.listen((event) {
final data = event.snapshot.value;
if (data != null) {
setState(() {
ageInWeeks = int.tryParse(data.toString()) ?? 1;
});
}
});
}
// Ambil rentang dari Firebase
void _fetchRangesFromFirebase() {
_dbRef.child("ranges").onValue.listen((event) {
final data = event.snapshot.value;
if (data != null) {
setState(() {
ranges = Map<String, dynamic>.from(data as Map);
});
}
});
}
// Update umur ayam ke Firebase
Future<void> _updateAge(int newAge) async {
if (newAge < 1 || newAge > 4) return; // Batasan umur ayam (1-4 minggu)
await _dbRef.child("chicken_age").set(newAge); // Simpan ke Firebase
setState(() {
ageInWeeks = newAge;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Umur ayam diperbarui ke $newAge minggu"),
backgroundColor: Color(0xFFA82429),
behavior: SnackBarBehavior.floating,
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {},
),
),
);
}
// Update rentang ke Firebase
Future<void> _updateRange(String type, String value, double newValue) async {
String path = "ranges/week$ageInWeeks/$type/$value";
await _dbRef.child(path).set(newValue);
}
// Widget untuk slider rentang
Widget _buildRangeSlider(String type, String value, double min, double max) {
Color sliderColor = type == 'temperature' ? Color(0xFFA82429) : Colors.blue;
IconData typeIcon = type == 'temperature' ? Icons.thermostat : Icons.water_drop;
double currentValue = ranges['week$ageInWeeks']?[type]?[value]?.toDouble() ?? min;
String valueName = value == 'min' ? 'Minimum' : value == 'max' ? 'Maksimum' : 'Target';
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Icon(typeIcon, color: sliderColor),
SizedBox(width: 10),
Expanded(
child: Text(
"${type == 'temperature' ? 'Suhu' : 'Kelembapan'} $valueName",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: sliderColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
"${currentValue.toStringAsFixed(1)}${type == 'temperature' ? '°C' : '%'}",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: sliderColor,
),
),
),
],
),
SizedBox(height: 16),
SliderTheme(
data: SliderThemeData(
activeTrackColor: sliderColor,
inactiveTrackColor: sliderColor.withOpacity(0.2),
thumbColor: sliderColor,
overlayColor: sliderColor.withOpacity(0.3),
valueIndicatorColor: sliderColor,
valueIndicatorTextStyle: TextStyle(color: Colors.white),
),
child: Slider(
value: currentValue,
min: min,
max: max,
divisions: (max - min).toInt() * 2,
label: currentValue.toStringAsFixed(1),
onChanged: (newValue) {
setState(() {
// Update local state for immediate feedback
if (ranges['week$ageInWeeks'] == null) {
ranges['week$ageInWeeks'] = {};
}
if (ranges['week$ageInWeeks'][type] == null) {
ranges['week$ageInWeeks'][type] = {};
}
ranges['week$ageInWeeks'][type][value] = newValue;
});
_updateRange(type, value, newValue);
},
),
),
SizedBox(height: 10),
LinearPercentIndicator(
lineHeight: 8.0,
percent: ((currentValue - min) / (max - min)).clamp(0.0, 1.0),
progressColor: sliderColor,
backgroundColor: sliderColor.withOpacity(0.1),
barRadius: Radius.circular(4),
padding: EdgeInsets.symmetric(horizontal: 0),
animation: true,
animationDuration: 500,
),
],
),
),
);
}
Widget _buildAgeSelector() {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Text(
"Umur Ayam",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) {
final week = index + 1;
final isSelected = ageInWeeks == week;
return GestureDetector(
onTap: () => _updateAge(week),
child: Container(
width: 60,
margin: EdgeInsets.symmetric(horizontal: 8),
child: Column(
children: [
AnimatedContainer(
duration: Duration(milliseconds: 300),
width: 60,
height: 60,
decoration: BoxDecoration(
color: isSelected ? Color(0xFFA82429) : Colors.grey[200],
shape: BoxShape.circle,
boxShadow: isSelected ? [
BoxShadow(
color: Color(0xFFA82429).withOpacity(0.4),
blurRadius: 8,
offset: Offset(0, 4),
)
] : null,
),
child: Center(
child: Text(
"$week",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.white : Colors.grey[600],
),
),
),
),
SizedBox(height: 8),
Text(
"Minggu",
style: TextStyle(
fontSize: 12,
color: isSelected ? Color(0xFFA82429) : Colors.grey[600],
),
)
],
),
),
);
}),
),
SizedBox(height: 20),
LinearPercentIndicator(
lineHeight: 10.0,
percent: ageInWeeks / 4,
center: Text(
"$ageInWeeks dari 4 minggu",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
progressColor: Color(0xFFA82429),
backgroundColor: Colors.grey[200],
barRadius: Radius.circular(5),
padding: EdgeInsets.symmetric(horizontal: 0),
animation: true,
animationDuration: 500,
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFA82429),
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
Navigator.pop(context);
},
),
title: Text(
"Kontrol Umur & Rentang",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
actions: [
IconButton(
icon: Icon(
showRangeControl ? Icons.visibility_off : Icons.visibility,
color: Colors.white,
),
onPressed: () {
setState(() {
showRangeControl = !showRangeControl;
});
},
),
],
),
body: Column(
children: [
// Header curved
Container(
padding: EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Center(
child: Column(
children: [
SizedBox(height: 10),
CircleAvatar(
backgroundColor: Colors.white,
radius: 40,
child: Icon(
Icons.pets,
size: 50,
color: Color(0xFFA82429),
),
),
SizedBox(height: 10),
Text(
"Kontrol Umur & Pengaturan Rentang",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 5),
Text(
"Pengelolaan Umur Ayam dan Rentang Sensor",
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
),
),
),
// Content
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Age selector
_buildAgeSelector(),
SizedBox(height: 20),
// Range toggles
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
child: ListTile(
leading: Icon(
showRangeControl ? Icons.tune : Icons.tune_outlined,
color: Color(0xFFA82429),
),
title: Text(
"Pengaturan Rentang",
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"Atur rentang suhu dan kelembapan yang optimal berdasarkan umur ayam",
),
trailing: Switch(
value: showRangeControl,
activeColor: Color(0xFFA82429),
onChanged: (value) {
setState(() {
showRangeControl = value;
});
},
),
),
),
SizedBox(height: 20),
// Bagian kontrol rentang (muncul saat toggle diaktifkan)
if (showRangeControl) ...[
// Header untuk rentang suhu
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
child: Row(
children: [
Icon(Icons.thermostat, color: Color(0xFFA82429)),
SizedBox(width: 8),
Text(
"Rentang Suhu",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFFA82429),
),
),
],
),
),
SizedBox(height: 8),
_buildRangeSlider("temperature", "min", 20, 40),
SizedBox(height: 10),
_buildRangeSlider("temperature", "target", 20, 40),
SizedBox(height: 10),
_buildRangeSlider("temperature", "max", 20, 40),
SizedBox(height: 24),
// Header untuk rentang kelembapan
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
child: Row(
children: [
Icon(Icons.water_drop, color: Colors.blue),
SizedBox(width: 8),
Text(
"Rentang Kelembapan",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
),
SizedBox(height: 8),
_buildRangeSlider("humidity", "min", 30, 80),
SizedBox(height: 10),
_buildRangeSlider("humidity", "target", 30, 80),
SizedBox(height: 10),
_buildRangeSlider("humidity", "max", 30, 80),
SizedBox(height: 30),
// Info card
Card(
elevation: 2,
color: Colors.blue.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Icon(Icons.info, color: Colors.blue),
SizedBox(width: 8),
Text(
"Informasi Rentang",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
SizedBox(height: 10),
Text(
"Pengaturan rentang akan mempengaruhi perilaku otomatis dalam pengendalian suhu dan kelembapan kandang ayam. Pastikan nilainya sesuai dengan kondisi optimal untuk setiap umur.",
style: TextStyle(
color: Colors.black87,
),
),
],
),
),
),
],
],
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,748 @@
import 'package:flutter/material.dart';
import 'package:jago/services/db_helper.dart';
import 'package:excel/excel.dart' as excel;
import 'dart:io';
import 'package:intl/intl.dart';
class AngkaKematianPage extends StatefulWidget {
@override
_AngkaKematianPageState createState() => _AngkaKematianPageState();
}
class _AngkaKematianPageState extends State<AngkaKematianPage> {
final List<Map<String, String>> dataKematian = [];
final DatabaseHelper _dbHelper = DatabaseHelper();
String selectedDate = "";
// Tambahkan variabel untuk row pages
int _rowsPerPage = 10;
List<int> _rowsPerPageOptions = [5, 10, 20, 50, 100];
int _currentPage = 0;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
await _autoDeleteOldData();
List<Map<String, dynamic>> dbData = await _dbHelper.getAngkaKematian();
setState(() {
dataKematian.clear();
dataKematian.addAll(dbData.map((e) => {
'tanggal': e['tanggal'],
'angka': e['angka'].toString(),
'kloter': e['kloter'].toString(),
}));
});
}
Future<void> _saveDataToDB(String tanggal, String angka, String kloter) async {
await _dbHelper.insertAngkaKematian({'tanggal': tanggal, 'angka': int.parse(angka), 'kloter': int.parse(kloter)});
_loadData();
}
Future<void> _autoDeleteOldData() async {
final currentDate = DateTime.now();
final thresholdDate = currentDate.subtract(Duration(days: 30)); // Example: delete entries older than 30 days
List<Map<String, dynamic>> allData = await _dbHelper.getAngkaKematian();
for (var item in allData) {
DateTime itemDate = DateTime.parse(item['tanggal']);
if (itemDate.isBefore(thresholdDate)) {
await _dbHelper.deleteAngkaKematian(item['tanggal'], item['kloter']);
}
}
}
void _showAddDataDialog() {
TextEditingController dateController = TextEditingController();
TextEditingController angkaController = TextEditingController();
TextEditingController kloterController = TextEditingController();
// Set tanggal hari ini sebagai default
dateController.text = DateTime.now().toString().split(' ')[0];
showDialog(
context: context,
builder: (context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: Column(
children: [
Icon(Icons.analytics, color: Color(0xFFA82429), size: 40),
SizedBox(height: 10),
Text(
'Tambah Data Kematian',
style: TextStyle(
color: Color(0xFFA82429),
fontWeight: FontWeight.bold
)
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: dateController,
decoration: InputDecoration(
labelText: 'Tanggal:',
labelStyle: TextStyle(color: Color(0xFFA82429)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade400),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Color(0xFFA82429)),
),
suffixIcon: IconButton(
icon: Icon(Icons.calendar_today, color: Color(0xFFA82429)),
onPressed: () async {
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2101),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: Color(0xFFA82429),
onPrimary: Colors.white,
),
),
child: child!,
);
},
);
if (pickedDate != null) {
dateController.text = "${pickedDate.toLocal()}".split(' ')[0];
}
},
),
),
),
SizedBox(height: 16),
TextField(
controller: angkaController,
decoration: InputDecoration(
labelText: 'Angka kematian:',
labelStyle: TextStyle(color: Color(0xFFA82429)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade400),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Color(0xFFA82429)),
),
),
keyboardType: TextInputType.number,
),
SizedBox(height: 16),
TextField(
controller: kloterController,
decoration: InputDecoration(
labelText: 'Kloter (angka):',
labelStyle: TextStyle(color: Color(0xFFA82429)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade400),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Color(0xFFA82429)),
),
),
keyboardType: TextInputType.number,
),
SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Batal', style: TextStyle(color: Colors.grey[600])),
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
),
SizedBox(width: 16),
ElevatedButton(
onPressed: () {
// Validasi input
if (dateController.text.isEmpty ||
angkaController.text.isEmpty ||
kloterController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Semua kolom harus diisi'))
);
return;
}
_saveDataToDB(dateController.text, angkaController.text, kloterController.text);
Navigator.pop(context);
},
child: Text('Tambah', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFA82429),
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
],
),
],
),
);
},
);
}
void _selectDate() async {
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2101),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: Color(0xFFA82429),
onPrimary: Colors.white,
),
),
child: child!,
);
},
);
if (pickedDate != null) {
setState(() {
selectedDate = "${pickedDate.toLocal()}".split(' ')[0];
});
}
}
Future<void> _downloadDataAsExcel() async {
var excelFile = excel.Excel.createExcel();
var sheet = excelFile['Sheet1'];
sheet.appendRow(['Tanggal', 'Angka Kematian', 'Kloter']);
for (var item in dataKematian) {
sheet.appendRow([item['tanggal'], item['angka'], item['kloter']]);
}
final directory = Directory('/storage/emulated/0/Documents/Data Angka Kematian');
if (!(await directory.exists())) {
await directory.create(recursive: true);
}
String filePath = '${directory.path}/AngkaKematian.xlsx';
File(filePath).writeAsBytesSync(excelFile.encode()!);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Data angka kematian berhasil diunduh sebagai Excel'),
backgroundColor: Color(0xFFA82429),
behavior: SnackBarBehavior.floating,
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {},
),
)
);
}
// Tambahkan method untuk paginasi
List<Map<String, String>> _getPaginatedData() {
// Filter data berdasarkan tanggal jika dipilih
List<Map<String, String>> filteredData = selectedDate.isEmpty
? dataKematian
: dataKematian.where((item) => item['tanggal'] == selectedDate).toList();
if (filteredData.isEmpty) return [];
int startIndex = _currentPage * _rowsPerPage;
int endIndex = startIndex + _rowsPerPage;
if (startIndex >= filteredData.length) {
_currentPage = 0;
startIndex = 0;
endIndex = _rowsPerPage;
}
if (endIndex > filteredData.length) {
endIndex = filteredData.length;
}
return filteredData.sublist(startIndex, endIndex);
}
int getTotalKematian() {
int total = 0;
for (var item in dataKematian) {
total += int.parse(item['angka'] ?? '0');
}
return total;
}
@override
Widget build(BuildContext context) {
// Dapatkan data yang sudah dipaginasi
final paginatedData = _getPaginatedData();
// Hitung total halaman berdasarkan data yang sudah difilter
final filteredData = selectedDate.isEmpty
? dataKematian
: dataKematian.where((item) => item['tanggal'] == selectedDate).toList();
final int totalPages = (filteredData.length / _rowsPerPage).ceil();
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFA82429),
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Data Angka Kematian',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
)
),
actions: [
IconButton(
icon: Icon(Icons.file_download, color: Colors.white),
onPressed: _downloadDataAsExcel,
),
],
),
body: Column(
children: [
// Header dengan background curved
Container(
padding: EdgeInsets.fromLTRB(16, 0, 16, 20),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
children: [
// Filter tanggal
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: InkWell(
onTap: _selectDate,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
Icons.calendar_today,
color: Color(0xFFA82429),
size: 20,
),
SizedBox(width: 12),
Expanded(
child: Text(
selectedDate.isEmpty
? 'Semua Tanggal'
: 'Tanggal: ${DateFormat('dd/MM/yyyy').format(DateTime.parse(selectedDate))}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
if (selectedDate.isNotEmpty)
IconButton(
icon: Icon(Icons.close, color: Colors.grey, size: 20),
onPressed: () {
setState(() {
selectedDate = "";
});
},
),
],
),
),
),
),
SizedBox(height: 16),
// Card informasi
Card(
color: Colors.white,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Kematian',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
SizedBox(height: 4),
Text(
'${getTotalKematian()}',
style: TextStyle(
color: Color(0xFFA82429),
fontWeight: FontWeight.bold,
fontSize: 22,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jumlah Data',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
SizedBox(height: 4),
Text(
'${dataKematian.length}',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
fontSize: 22,
),
),
],
),
],
),
),
),
],
),
),
// Paginasi controls
Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'Baris per halaman: ',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _rowsPerPage,
isDense: true,
items: _rowsPerPageOptions.map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(value.toString()),
);
}).toList(),
onChanged: (newValue) {
if (newValue != null) {
setState(() {
_rowsPerPage = newValue;
_currentPage = 0; // Reset ke halaman pertama
});
}
},
),
),
),
],
),
if (totalPages > 0)
Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back_ios, size: 16),
color: _currentPage > 0 ? Color(0xFFA82429) : Colors.grey,
onPressed: _currentPage > 0
? () {
setState(() {
_currentPage--;
});
}
: null,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${_currentPage + 1} / $totalPages',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: Icon(Icons.arrow_forward_ios, size: 16),
color: _currentPage < totalPages - 1 ? Color(0xFFA82429) : Colors.grey,
onPressed: _currentPage < totalPages - 1
? () {
setState(() {
_currentPage++;
});
}
: null,
),
],
),
],
),
),
// List of kematian items
Expanded(
child: RefreshIndicator(
onRefresh: _loadData,
color: Color(0xFFA82429),
child: paginatedData.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.cloud_off,
size: 64,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
selectedDate.isEmpty
? 'Tidak ada data kematian'
: 'Tidak ada data kematian untuk tanggal ini',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
textAlign: TextAlign.center,
),
],
),
)
: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: paginatedData.length,
itemBuilder: (context, index) {
final item = paginatedData[index];
return Dismissible(
key: Key(item['tanggal']! + item['kloter']!),
background: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.delete, color: Colors.white),
Icon(Icons.delete, color: Colors.white),
],
),
),
confirmDismiss: (direction) async {
bool? result = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: Text(
'Hapus Data',
style: TextStyle(
color: Color(0xFFA82429),
fontWeight: FontWeight.bold
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.warning_amber_rounded,
color: Colors.orange,
size: 48,
),
SizedBox(height: 16),
Text('Apakah Anda yakin ingin menghapus data untuk kloter ${item['kloter']} pada tanggal ${DateFormat('dd/MM/yyyy').format(DateTime.parse(item['tanggal']!))}?'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Batal'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Hapus', style: TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
);
},
);
if (result == true) {
await _dbHelper.deleteAngkaKematian(item['tanggal']!, item['kloter']!);
_loadData();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Data untuk kloter ${item['kloter']} berhasil dihapus'),
backgroundColor: Color(0xFFA82429),
behavior: SnackBarBehavior.floating,
)
);
}
return result;
},
child: Card(
elevation: 3,
margin: EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.calendar_month, color: Color(0xFFA82429), size: 20),
SizedBox(width: 8),
Text(
DateFormat('dd/MM/yyyy').format(DateTime.parse(item['tanggal']!)),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Color(0xFFA82429).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Kloter ${item['kloter']}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFFA82429),
),
),
),
],
),
SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Jumlah Kematian:',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
Row(
children: [
Icon(Icons.remove_circle_outline,
color: int.parse(item['angka']!) > 5 ? Colors.red : Colors.orange,
size: 18),
SizedBox(width: 4),
Text(
'${item['angka']} ekor',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: int.parse(item['angka']!) > 5 ? Colors.red : Colors.orange,
),
),
],
),
],
),
],
),
),
),
);
},
),
),
),
],
),
floatingActionButton: FloatingActionButton(
backgroundColor: Color(0xFFA82429),
child: Icon(Icons.add, color: Colors.white),
onPressed: _showAddDataDialog,
),
);
}
}

91
lib/screens/control.dart Normal file
View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
class ControlPage extends StatefulWidget {
@override
_ControlPageState createState() => _ControlPageState();
}
class _ControlPageState extends State<ControlPage> {
bool isLightOn = false; // Status lampu
bool isFanOn = false; // Status kipas
void toggleLight(bool value) {
setState(() {
isLightOn = value; // Perbarui status lampu
});
// Tambahkan logika untuk mengontrol lampu di sini
if (isLightOn) {
print("Lampu dinyalakan");
} else {
print("Lampu dimatikan");
}
}
void toggleFan(bool value) {
setState(() {
isFanOn = value; // Perbarui status kipas
});
// Tambahkan logika untuk mengontrol kipas di sini
if (isFanOn) {
print("Kipas dinyalakan");
} else {
print("Kipas dimatikan");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFA82429), // Warna merah
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
Navigator.pop(context);
},
),
title: Text(
'Kontrol Manual',
style: TextStyle(color: Colors.white),
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Kontrol Lampu
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
title: Text('Lampu'),
trailing: Switch(
value: isLightOn,
onChanged: toggleLight, // Panggil fungsi toggleLight
),
),
),
SizedBox(height: 10),
// Kontrol Kipas
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
title: Text('Kipas'),
trailing: Switch(
value: isFanOn,
onChanged: toggleFan, // Panggil fungsi toggleFan
),
),
),
],
),
),
);
}
}

1408
lib/screens/dashboard.dart Normal file

File diff suppressed because it is too large Load Diff

892
lib/screens/history.dart Normal file
View File

@ -0,0 +1,892 @@
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:intl/intl.dart'; // Untuk format tanggal
import 'package:jago/services/db_helper.dart'; // Pastikan sesuai dengan nama proyek
import 'package:excel/excel.dart' as excel;
import 'dart:io';
import 'dart:async'; // Tambahkan import untuk StreamSubscription
class HistoryPage extends StatefulWidget {
@override
_HistoryPageState createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
List<Map<String, dynamic>> historyData = [];
List<Map<String, dynamic>> filteredHistory = [];
late DatabaseHelper dbHelper;
DateTime? selectedDate;
late DatabaseReference ageRef;
int ageInWeeks = 2; // Default age
int selectedAge = 0; // 0 untuk umur saat ini, 1-4 untuk umur spesifik
// Untuk filter tipe log
String selectedLogType = 'Semua';
List<String> logTypes = ['Semua', 'Relay', 'Kontrol', 'Status', 'Sensor', 'Sistem'];
// Tambahkan variabel untuk row pages
int _rowsPerPage = 10;
List<int> _rowsPerPageOptions = [5, 10, 20, 50, 100];
int _currentPage = 0;
// Daftar untuk menyimpan semua subscription
List<StreamSubscription> _subscriptions = [];
@override
void initState() {
super.initState();
dbHelper = DatabaseHelper();
ageRef = FirebaseDatabase.instance.ref("chicken_age");
_loadHistoryFromDB();
_fetchAgeFromFirebase();
}
@override
void dispose() {
// Membersihkan semua subscription saat widget di-dispose
for (var subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
super.dispose();
}
Future<void> _loadHistoryFromDB() async {
if (!mounted) return; // Cek apakah widget masih mounted sebelum melakukan operasi
List<Map<String, dynamic>> dbData = await dbHelper.getHistory();
if (mounted) { // Cek lagi setelah operasi async
setState(() {
historyData = dbData; // Gunakan semua data tanpa filter
_applyFilter();
});
}
}
Future<void> _refreshHistory() async {
await _loadHistoryFromDB();
}
void _applyFilter() {
setState(() {
// Filter berdasarkan tanggal jika dipilih
List<Map<String, dynamic>> dateFiltered;
if (selectedDate == null) {
dateFiltered = List.from(historyData);
} else {
String selectedDateString = DateFormat('yyyy-MM-dd').format(selectedDate!);
dateFiltered = historyData.where((data) {
DateTime dateTime = DateTime.parse(data['timestamp']);
return DateFormat('yyyy-MM-dd').format(dateTime) == selectedDateString;
}).toList();
}
// Filter berdasarkan tipe log jika bukan 'Semua'
List<Map<String, dynamic>> typeFiltered;
if (selectedLogType == 'Semua') {
typeFiltered = dateFiltered;
} else {
typeFiltered = dateFiltered.where((data) {
String eventText = data['event'] ?? '';
// Ekstrak tipe dari teks event
if (selectedLogType == 'Relay') {
return eventText.contains('[RELAY]');
} else if (selectedLogType == 'Kontrol') {
return eventText.contains('[CONTROL]');
} else if (selectedLogType == 'Status') {
return eventText.contains('[STATUS]');
} else if (selectedLogType == 'Sensor') {
return eventText.contains('[SENSOR]');
} else if (selectedLogType == 'Sistem') {
return eventText.contains('[SYSTEM]');
}
return false;
}).toList();
}
// Filter berdasarkan umur ayam yang dipilih
List<Map<String, dynamic>> ageFiltered = typeFiltered.where((data) {
String eventText = data['event'] ?? '';
// Cek apakah event memiliki tag umur
if (eventText.contains("[Umur:")) {
// Ekstrak umur dari log
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
int logAge = int.parse(match.group(1)!);
// Jika selectedAge adalah 0, gunakan umur ayam saat ini
// Jika tidak, gunakan umur yang dipilih
if (selectedAge == 0) {
return logAge == ageInWeeks;
} else {
return logAge == selectedAge;
}
}
return false;
} else {
// Untuk log lama yang tidak memiliki tag umur, tampilkan di umur 2 minggu
if (selectedAge == 0) {
// Jika memilih "umur saat ini", tampilkan data lama jika umur saat ini adalah 2
return ageInWeeks == 2;
} else {
// Jika memilih umur spesifik, tampilkan data lama hanya jika memilih umur 2
return selectedAge == 2;
}
}
}).toList();
// Hapus data duplikat berdasarkan konten event
final Map<String, Map<String, dynamic>> uniqueEvents = {};
// Urutkan berdasarkan timestamp terbaru
ageFiltered.sort((a, b) =>
DateTime.parse(b['timestamp']).compareTo(DateTime.parse(a['timestamp']))
);
// Ambil entri unik berdasarkan konten event
for (var item in ageFiltered) {
String eventText = item['event'] ?? '';
// Hapus timestamp dari pertimbangan keunikan jika ada
String uniqueKey = eventText.replaceAll(RegExp(r'\d{2}:\d{2}:\d{2}'), '').trim();
// Simpan hanya entri pertama (yang paling baru) untuk setiap event unik
if (!uniqueEvents.containsKey(uniqueKey)) {
uniqueEvents[uniqueKey] = item;
}
}
// Konversi kembali ke list dan urutkan berdasarkan timestamp terbaru
filteredHistory = uniqueEvents.values.toList();
filteredHistory.sort((a, b) =>
DateTime.parse(b['timestamp']).compareTo(DateTime.parse(a['timestamp']))
);
// Reset halaman saat filter berubah
_currentPage = 0;
});
}
// Tambahkan method untuk paginasi
List<Map<String, dynamic>> _getPaginatedData() {
if (filteredHistory.isEmpty) return [];
int startIndex = _currentPage * _rowsPerPage;
int endIndex = startIndex + _rowsPerPage;
if (startIndex >= filteredHistory.length) {
_currentPage = 0;
startIndex = 0;
endIndex = _rowsPerPage;
}
if (endIndex > filteredHistory.length) {
endIndex = filteredHistory.length;
}
return filteredHistory.sublist(startIndex, endIndex);
}
Future<void> _pickDate(BuildContext context) async {
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: selectedDate ?? DateTime.now(),
firstDate: DateTime(2024),
lastDate: DateTime(2030),
);
if (pickedDate != null) {
setState(() {
selectedDate = pickedDate;
_applyFilter();
});
}
}
void _fetchAgeFromFirebase() {
final subscription = ageRef.onValue.listen((event) {
final data = event.snapshot.value;
if (data != null && mounted) {
int newAge = int.parse(data.toString());
if (newAge != ageInWeeks) {
print("🐔 Umur ayam berubah dari $ageInWeeks ke $newAge");
setState(() {
ageInWeeks = newAge;
});
// Muat ulang history saat umur berubah
_loadHistoryFromDB();
}
}
});
// Tambahkan subscription ke daftar
_subscriptions.add(subscription);
}
void _downloadHistoryAsExcel() async {
// Terapkan filter duplikat pada historyData
final Map<String, Map<String, dynamic>> uniqueEvents = {};
// Buat salinan historyData dan urutkan berdasarkan timestamp terbaru
List<Map<String, dynamic>> sortedData = List.from(historyData);
sortedData.sort((a, b) =>
DateTime.parse(b['timestamp']).compareTo(DateTime.parse(a['timestamp']))
);
// Filter berdasarkan umur ayam yang dipilih
List<Map<String, dynamic>> ageFilteredData = sortedData.where((data) {
String eventText = data['event'] ?? '';
if (eventText.contains("[Umur:")) {
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
int logAge = int.parse(match.group(1)!);
// Jika selectedAge adalah 0, gunakan umur ayam saat ini
// Jika tidak, gunakan umur yang dipilih
if (selectedAge == 0) {
return logAge == ageInWeeks;
} else {
return logAge == selectedAge;
}
}
return false;
} else {
// Untuk log lama yang tidak memiliki tag umur, tampilkan di umur 2 minggu
if (selectedAge == 0) {
// Jika memilih "umur saat ini", tampilkan data lama jika umur saat ini adalah 2
return ageInWeeks == 2;
} else {
// Jika memilih umur spesifik, tampilkan data lama hanya jika memilih umur 2
return selectedAge == 2;
}
}
}).toList();
// Ambil entri unik berdasarkan konten event
for (var item in ageFilteredData) {
String eventText = item['event'] ?? '';
// Hapus timestamp dari pertimbangan keunikan jika ada
String uniqueKey = eventText.replaceAll(RegExp(r'\d{2}:\d{2}:\d{2}'), '').trim();
// Simpan hanya entri pertama (yang paling baru) untuk setiap event unik
if (!uniqueEvents.containsKey(uniqueKey)) {
uniqueEvents[uniqueKey] = item;
}
}
// Konversi kembali ke list dan urutkan berdasarkan timestamp
List<Map<String, dynamic>> uniqueData = uniqueEvents.values.toList();
uniqueData.sort((a, b) =>
DateTime.parse(b['timestamp']).compareTo(DateTime.parse(a['timestamp']))
);
var excelFile = excel.Excel.createExcel();
var sheet = excelFile['Sheet1'];
sheet.appendRow(['Timestamp', 'Tipe', 'Event', 'Lampu', 'Kipas', 'Umur Ayam']);
for (var data in uniqueData) {
String lampuStatus = data['lampu'] == 1 ? 'Hidup' : 'Mati';
String kipasStatus = data['kipas'] == 1 ? 'Hidup' : 'Mati';
String eventText = data['event'] ?? '';
// Ekstrak tipe dari teks event
String type = 'Umum';
if (eventText.contains('[RELAY]')) {
type = 'Relay';
} else if (eventText.contains('[CONTROL]')) {
type = 'Kontrol';
} else if (eventText.contains('[STATUS]')) {
type = 'Status';
} else if (eventText.contains('[SENSOR]')) {
type = 'Sensor';
} else if (eventText.contains('[SYSTEM]')) {
type = 'Sistem';
}
// Hapus tag tipe dari teks event untuk tampilan Excel
eventText = eventText.replaceAll(RegExp(r'\[(RELAY|CONTROL|STATUS|SENSOR|SYSTEM)\]\s*'), '');
// Ekstrak umur dari teks event jika ada
int eventAge = 2; // Default umur untuk data lama tanpa tag
if (eventText.contains("[Umur:")) {
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
eventAge = int.parse(match.group(1)!);
}
// Hapus tag umur dari tampilan
eventText = eventText.replaceAll(regExp, '').trim();
}
sheet.appendRow([
data['timestamp'],
type,
eventText,
lampuStatus,
kipasStatus,
eventAge
]);
}
// Tentukan nama file berdasarkan mode filter umur
int exportAge = selectedAge == 0 ? ageInWeeks : selectedAge;
final directory = Directory('/storage/emulated/0/Documents/Data History');
if (!(await directory.exists())) {
await directory.create(recursive: true);
}
String filePath = '${directory.path}/History_${DateFormat('yyyyMMdd').format(DateTime.now())}_Umur${exportAge}minggu.xlsx';
File(filePath).writeAsBytesSync(excelFile.encode()!);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Data history berhasil diunduh sebagai Excel di $filePath')));
}
@override
Widget build(BuildContext context) {
// Dapatkan data yang sudah dipaginasi
final paginatedData = _getPaginatedData();
final int totalPages = (filteredHistory.length / _rowsPerPage).ceil();
// Tentukan judul AppBar berdasarkan mode filter umur
String appBarTitle = selectedAge == 0
? 'Riwayat Aktivitas (Umur: $ageInWeeks minggu)'
: 'Riwayat Aktivitas (Umur: $selectedAge minggu)';
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFA82429),
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
appBarTitle,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
)
),
),
body: Column(
children: [
// Header section with curved background
Container(
padding: EdgeInsets.fromLTRB(16, 0, 16, 20),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
children: [
// Tanggal Filter
Row(
children: [
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Row(
children: [
Icon(
Icons.calendar_today,
color: Color(0xFFA82429),
size: 20,
),
SizedBox(width: 8),
Expanded(
child: Text(
selectedDate == null
? 'Semua Tanggal'
: DateFormat('dd/MM/yyyy').format(selectedDate!),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
TextButton(
onPressed: () => _pickDate(context),
child: Text(
'Pilih',
style: TextStyle(
color: Color(0xFFA82429),
fontWeight: FontWeight.bold,
),
),
style: TextButton.styleFrom(
minimumSize: Size(0, 0),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
),
),
],
),
),
),
],
),
SizedBox(height: 12),
// Filter umur ayam dan tipe log
Row(
children: [
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: selectedAge,
isExpanded: true,
icon: Icon(Icons.filter_list, color: Color(0xFFA82429)),
items: [
DropdownMenuItem<int>(
value: 0,
child: Text('Umur Saat Ini (${ageInWeeks} minggu)'),
),
DropdownMenuItem<int>(
value: 1,
child: Text('Umur 1 minggu'),
),
DropdownMenuItem<int>(
value: 2,
child: Text('Umur 2 minggu'),
),
DropdownMenuItem<int>(
value: 3,
child: Text('Umur 3 minggu'),
),
DropdownMenuItem<int>(
value: 4,
child: Text('Umur 4 minggu'),
),
],
onChanged: (int? newValue) {
if (newValue != null) {
setState(() {
selectedAge = newValue;
_applyFilter();
});
}
},
),
),
),
),
SizedBox(width: 10),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(Icons.file_download, color: Color(0xFFA82429)),
onPressed: _downloadHistoryAsExcel,
tooltip: 'Unduh Data Excel',
),
),
],
),
SizedBox(height: 12),
// Filter tipe log
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedLogType,
isExpanded: true,
icon: Icon(Icons.category, color: Color(0xFFA82429)),
items: logTypes.map((String type) {
IconData typeIcon;
switch (type) {
case 'Relay':
typeIcon = Icons.power;
break;
case 'Kontrol':
typeIcon = Icons.settings;
break;
case 'Status':
typeIcon = Icons.info;
break;
case 'Sensor':
typeIcon = Icons.sensors;
break;
case 'Sistem':
typeIcon = Icons.computer;
break;
default:
typeIcon = Icons.list;
}
return DropdownMenuItem<String>(
value: type,
child: Row(
children: [
Icon(typeIcon, size: 18, color: Color(0xFFA82429)),
SizedBox(width: 8),
Text(type),
],
),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
selectedLogType = newValue;
_applyFilter();
});
}
},
),
),
),
],
),
),
// Paginasi controls
Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'Baris per halaman: ',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _rowsPerPage,
isDense: true,
items: _rowsPerPageOptions.map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(value.toString()),
);
}).toList(),
onChanged: (newValue) {
if (newValue != null) {
setState(() {
_rowsPerPage = newValue;
_currentPage = 0; // Reset ke halaman pertama
});
}
},
),
),
),
],
),
if (totalPages > 0)
Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back_ios, size: 16),
color: _currentPage > 0 ? Color(0xFFA82429) : Colors.grey,
onPressed: _currentPage > 0
? () {
setState(() {
_currentPage--;
});
}
: null,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${_currentPage + 1} / $totalPages',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: Icon(Icons.arrow_forward_ios, size: 16),
color: _currentPage < totalPages - 1 ? Color(0xFFA82429) : Colors.grey,
onPressed: _currentPage < totalPages - 1
? () {
setState(() {
_currentPage++;
});
}
: null,
),
],
),
],
),
),
// List of history items
Expanded(
child: RefreshIndicator(
onRefresh: _refreshHistory,
color: Color(0xFFA82429),
child: paginatedData.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.history_toggle_off,
size: 64,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
selectedAge == 0
? 'Tidak ada riwayat untuk umur ayam $ageInWeeks minggu'
: 'Tidak ada riwayat untuk umur ayam $selectedAge minggu',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
textAlign: TextAlign.center,
),
],
),
)
: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: paginatedData.length,
itemBuilder: (context, index) {
final data = paginatedData[index];
final DateTime dateTime = DateTime.parse(data['timestamp']);
String eventText = data['event'] ?? 'Data tidak tersedia';
// Ekstrak tipe dari teks event
Color cardColor;
IconData iconData;
if (eventText.contains('[RELAY]')) {
eventText = eventText.replaceAll('[RELAY] ', '');
cardColor = Colors.blue;
iconData = Icons.power;
} else if (eventText.contains('[CONTROL]')) {
eventText = eventText.replaceAll('[CONTROL] ', '');
cardColor = Colors.green;
iconData = Icons.settings;
} else if (eventText.contains('[STATUS]')) {
eventText = eventText.replaceAll('[STATUS] ', '');
cardColor = Colors.orange;
iconData = Icons.info;
} else if (eventText.contains('[SENSOR]')) {
eventText = eventText.replaceAll('[SENSOR] ', '');
cardColor = Colors.purple;
iconData = Icons.sensors;
} else if (eventText.contains('[SYSTEM]')) {
eventText = eventText.replaceAll('[SYSTEM] ', '');
cardColor = Colors.red;
iconData = Icons.computer;
} else {
cardColor = Colors.blueGrey;
iconData = Icons.history;
}
// Ekstrak umur dari teks event jika ada
int eventAge = 2; // Default umur untuk data lama tanpa tag
if (eventText.contains("[Umur:")) {
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
eventAge = int.parse(match.group(1)!);
}
// Hapus tag umur dari tampilan
eventText = eventText.replaceAll(regExp, '').trim();
}
return Card(
elevation: 3,
margin: EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Column(
children: [
// Colored header
Container(
color: cardColor,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
iconData,
color: Colors.white,
size: 20,
),
SizedBox(width: 8),
Expanded(
child: Text(
DateFormat('dd/MM/yyyy HH:mm').format(dateTime),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Umur: $eventAge minggu',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
),
// Status perangkat
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
eventText,
style: TextStyle(
fontSize: 16,
color: Colors.black87,
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildDeviceStatus(
'Lampu',
data['lampu'] == 1,
Icons.lightbulb,
Colors.amber
),
_buildDeviceStatus(
'Kipas',
data['kipas'] == 1,
Icons.propane,
Colors.blue
),
],
),
],
),
),
],
),
),
);
},
),
),
),
],
),
);
}
Widget _buildDeviceStatus(String title, bool isActive, IconData icon, Color activeColor) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isActive ? activeColor.withOpacity(0.2) : Colors.grey.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Icon(
icon,
size: 16,
color: isActive ? activeColor : Colors.grey,
),
SizedBox(width: 4),
Text(
'$title: ${isActive ? "Aktif" : "Nonaktif"}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isActive ? activeColor : Colors.grey,
),
),
],
),
);
}
}

303
lib/screens/login.dart Normal file
View File

@ -0,0 +1,303 @@
import 'package:flutter/material.dart';
import 'dashboard.dart'; // Pastikan untuk mengimpor halaman dashboard
import 'package:firebase_auth/firebase_auth.dart'; // Import Firebase Auth
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStateMixin {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
bool _isLoading = false;
bool _obscurePassword = true;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
// Initialize Firebase Auth
final FirebaseAuth _auth = FirebaseAuth.instance;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 1500),
);
_fadeAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeIn,
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
// Fungsi login (diperbarui untuk menggunakan Firebase Auth)
void _login() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
// Menggunakan Firebase Auth untuk login
await _auth.signInWithEmailAndPassword(
email: _emailController.text,
password: _passwordController.text,
);
// Pindah ke halaman dashboard setelah login berhasil
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => DashboardPage()),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Login berhasil!'),
backgroundColor: Colors.green,
),
);
} catch (e) {
// Menangani kesalahan login
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Login gagal: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
}
// Fungsi untuk mengirim email reset password
void _resetPassword() async {
String email = _emailController.text;
if (email.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Masukkan email untuk reset password'),
backgroundColor: Colors.orange,
),
);
return;
}
try {
await _auth.sendPasswordResetEmail(email: email);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Email reset password telah dikirim!'),
backgroundColor: Colors.green,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal mengirim email: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Color(0xFFFDEDEE)],
),
),
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: FadeTransition(
opacity: _fadeAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: 'logo',
child: Image.asset('assets/Logo.jpg', width: 180, height: 180),
),
SizedBox(height: 20),
Text(
"JAGO",
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Color(0xFFA82429),
letterSpacing: 2.0,
),
),
SizedBox(height: 10),
Text(
"Selamat Datang",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 8),
Text(
"Silahkan masuk untuk melanjutkan",
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
SizedBox(height: 40),
Card(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Form(
key: _formKey,
child: Column(
children: [
// Input Email
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
labelStyle: TextStyle(color: Color(0xFFA82429)),
hintText: 'Masukkan email anda',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFFA82429)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFFA82429), width: 2),
),
prefixIcon: Icon(Icons.email, color: Color(0xFFA82429)),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email tidak boleh kosong';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Format email tidak valid';
}
return null;
},
),
SizedBox(height: 20),
// Input Password
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: 'Password',
labelStyle: TextStyle(color: Color(0xFFA82429)),
hintText: 'Masukkan password anda',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFFA82429)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFFA82429), width: 2),
),
prefixIcon: Icon(Icons.lock, color: Color(0xFFA82429)),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: Color(0xFFA82429),
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password tidak boleh kosong';
}
if (value.length < 6) {
return 'Password minimal 6 karakter';
}
return null;
},
),
SizedBox(height: 10),
// Tautan Lupa Password
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _resetPassword,
child: Text(
"Lupa password?",
style: TextStyle(
color: Color(0xFFA82429),
fontWeight: FontWeight.w600,
),
),
),
),
SizedBox(height: 24),
// Tombol Login
SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFA82429),
foregroundColor: Colors.white,
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
)
: Text(
'MASUK',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 1.0,
),
),
),
),
],
),
),
),
),
],
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,756 @@
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:intl/intl.dart';
import 'package:jago/services/db_helper.dart';
import 'package:jago/services/notification_service.dart'; // Import layanan notifikasi
import 'package:excel/excel.dart' as excel;
import 'dart:io';
import 'dart:async';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class NotificationPage extends StatefulWidget {
@override
_NotificationPageState createState() => _NotificationPageState();
}
class _NotificationPageState extends State<NotificationPage> {
List<Map<String, dynamic>> notifications = [];
List<Map<String, dynamic>> filteredNotifications = [];
late DatabaseHelper dbHelper;
DateTime? selectedDate;
int ageInWeeks = 2;
late DatabaseReference ageRef;
int selectedAge = 0; // 0 untuk umur saat ini, 1-4 untuk umur spesifik
// Tambahkan variabel untuk row pages
int _rowsPerPage = 10;
List<int> _rowsPerPageOptions = [5, 10, 20, 50, 100];
int _currentPage = 0;
// Inisialisasi notifikasi lokal
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
// Daftar subscription untuk dibersihkan saat dispose
final List<StreamSubscription> _subscriptions = [];
@override
void initState() {
super.initState();
dbHelper = DatabaseHelper();
ageRef = FirebaseDatabase.instance.ref("chicken_age");
// Periksa tabel notifications
dbHelper.checkTables().then((_) {
_fetchAgeFromFirebase();
_loadNotificationsFromDB();
});
// Pastikan NotificationService sudah diinisialisasi
NotificationService().initialize();
}
@override
void dispose() {
// Membersihkan semua subscription untuk mencegah memory leak
for (var subscription in _subscriptions) {
subscription.cancel();
}
super.dispose();
}
void _fetchAgeFromFirebase() {
final subscription = ageRef.onValue.listen((event) {
final data = event.snapshot.value;
if (data != null) {
int newAge = int.parse(data.toString());
if (newAge != ageInWeeks) {
print("🐔 Umur ayam berubah dari $ageInWeeks ke $newAge");
setState(() {
ageInWeeks = newAge;
});
// Muat ulang notifikasi saat umur berubah
_loadNotificationsFromDB();
}
}
});
_subscriptions.add(subscription);
}
Future<void> _loadNotificationsFromDB() async {
List<Map<String, dynamic>> dbData = await dbHelper.getNotifications();
print("🔍 Total notifikasi dari database: ${dbData.length}");
setState(() {
notifications = _filterUniqueNotifications(dbData);
_applyFilter();
});
print("✅ Berhasil memuat ${notifications.length} notifikasi dari database");
}
Future<void> _refreshNotifications() async {
await _loadNotificationsFromDB();
}
List<Map<String, dynamic>> _filterUniqueNotifications(List<Map<String, dynamic>> data) {
List<Map<String, dynamic>> uniqueData = [];
Map<String, dynamic>? lastEntry;
for (var entry in data) {
if (lastEntry == null || lastEntry['event'] != entry['event']) {
uniqueData.add(entry);
lastEntry = entry;
}
}
return uniqueData;
}
void _applyFilter() {
setState(() {
// Filter berdasarkan tanggal jika dipilih
List<Map<String, dynamic>> dateFiltered;
if (selectedDate == null) {
dateFiltered = List.from(notifications);
} else {
String selectedDateString = DateFormat('yyyy-MM-dd').format(selectedDate!);
dateFiltered = notifications.where((notif) {
DateTime dateTime = DateTime.parse(notif['timestamp']);
return DateFormat('yyyy-MM-dd').format(dateTime) == selectedDateString;
}).toList();
}
// Filter berdasarkan umur ayam yang dipilih
filteredNotifications = dateFiltered.where((notif) {
// Ekstrak umur dari pesan notifikasi jika ada
String eventText = notif['event'] ?? '';
// Cek apakah notifikasi berisi informasi umur
if (eventText.contains("[Umur:")) {
// Ekstrak umur dari notifikasi
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
int notifAge = int.parse(match.group(1)!);
// Jika selectedAge adalah 0, gunakan umur ayam saat ini
// Jika tidak, gunakan umur yang dipilih
if (selectedAge == 0) {
return notifAge == ageInWeeks;
} else {
return notifAge == selectedAge;
}
}
return false;
} else {
// Untuk notifikasi lama yang tidak memiliki tag umur, tampilkan di umur 2 minggu
if (selectedAge == 0) {
// Jika memilih "umur saat ini", tampilkan data lama jika umur saat ini adalah 2
return ageInWeeks == 2;
} else {
// Jika memilih umur spesifik, tampilkan data lama hanya jika memilih umur 2
return selectedAge == 2;
}
}
}).toList();
// Reset halaman saat filter berubah
_currentPage = 0;
});
}
Future<void> _pickDate(BuildContext context) async {
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: selectedDate ?? DateTime.now(),
firstDate: DateTime(2024),
lastDate: DateTime(2030),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: Color(0xFFA82429),
onPrimary: Colors.white,
),
),
child: child!,
);
},
);
if (pickedDate != null) {
setState(() {
selectedDate = pickedDate;
_applyFilter();
});
}
}
void _downloadNotificationsAsExcel() async {
// Filter notifikasi sesuai umur yang dipilih
List<Map<String, dynamic>> ageFilteredData = notifications.where((notif) {
String eventText = notif['event'] ?? '';
if (eventText.contains("[Umur:")) {
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
int notifAge = int.parse(match.group(1)!);
// Jika selectedAge adalah 0, gunakan umur ayam saat ini
// Jika tidak, gunakan umur yang dipilih
if (selectedAge == 0) {
return notifAge == ageInWeeks;
} else {
return notifAge == selectedAge;
}
}
return false;
} else {
// Untuk notifikasi lama yang tidak memiliki tag umur, tampilkan di umur 2 minggu
if (selectedAge == 0) {
// Jika memilih "umur saat ini", tampilkan data lama jika umur saat ini adalah 2
return ageInWeeks == 2;
} else {
// Jika memilih umur spesifik, tampilkan data lama hanya jika memilih umur 2
return selectedAge == 2;
}
}
}).toList();
var excelFile = excel.Excel.createExcel();
var sheet = excelFile['Sheet1'];
sheet.appendRow(['Timestamp', 'Event', 'Umur Ayam']);
for (var notif in ageFilteredData) {
String eventText = notif['event'] ?? '';
// Ekstrak umur dari teks event jika ada
int eventAge = 2; // Default umur untuk data lama tanpa tag
if (eventText.contains("[Umur:")) {
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
eventAge = int.parse(match.group(1)!);
}
// Hapus tag umur dari tampilan
eventText = eventText.replaceAll(regExp, "").trim();
}
sheet.appendRow([notif['timestamp'], eventText, eventAge]);
}
// Tentukan nama file berdasarkan mode filter umur
int exportAge = selectedAge == 0 ? ageInWeeks : selectedAge;
final directory = Directory('/storage/emulated/0/Documents/Data Notifikasi');
if (!(await directory.exists())) {
await directory.create(recursive: true);
}
String filePath = '${directory.path}/Notifikasi_${DateFormat('yyyyMMdd').format(DateTime.now())}_Umur${exportAge}minggu.xlsx';
File(filePath)..writeAsBytesSync(excelFile.encode()!);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Data notifikasi berhasil diunduh sebagai Excel'),
backgroundColor: Color(0xFFA82429),
behavior: SnackBarBehavior.floating,
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {},
),
)
);
}
// Tambahkan method untuk paginasi
List<Map<String, dynamic>> _getPaginatedData() {
if (filteredNotifications.isEmpty) return [];
int startIndex = _currentPage * _rowsPerPage;
int endIndex = startIndex + _rowsPerPage;
if (startIndex >= filteredNotifications.length) {
_currentPage = 0;
startIndex = 0;
endIndex = _rowsPerPage;
}
if (endIndex > filteredNotifications.length) {
endIndex = filteredNotifications.length;
}
return filteredNotifications.sublist(startIndex, endIndex);
}
@override
Widget build(BuildContext context) {
// Dapatkan data yang sudah dipaginasi
final paginatedData = _getPaginatedData();
final int totalPages = (filteredNotifications.length / _rowsPerPage).ceil();
// Tentukan judul AppBar berdasarkan mode filter umur
String appBarTitle = selectedAge == 0
? 'Notifikasi (Umur: $ageInWeeks minggu)'
: 'Notifikasi (Umur: $selectedAge minggu)';
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFA82429),
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
appBarTitle,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
)
),
),
body: Column(
children: [
// Header section with curved background
Container(
padding: EdgeInsets.fromLTRB(16, 0, 16, 20),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Row(
children: [
Icon(
Icons.calendar_today,
color: Color(0xFFA82429),
size: 20,
),
SizedBox(width: 8),
Expanded(
child: Text(
selectedDate == null
? 'Semua Tanggal'
: DateFormat('dd/MM/yyyy').format(selectedDate!),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
TextButton(
onPressed: () => _pickDate(context),
child: Text(
'Pilih',
style: TextStyle(
color: Color(0xFFA82429),
fontWeight: FontWeight.bold,
),
),
style: TextButton.styleFrom(
minimumSize: Size(0, 0),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
),
),
],
),
),
),
],
),
SizedBox(height: 12),
Row(
children: [
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: selectedAge,
isExpanded: true,
icon: Icon(Icons.filter_list, color: Color(0xFFA82429)),
items: [
DropdownMenuItem<int>(
value: 0,
child: Text('Umur Saat Ini (${ageInWeeks} minggu)'),
),
DropdownMenuItem<int>(
value: 1,
child: Text('Umur 1 minggu'),
),
DropdownMenuItem<int>(
value: 2,
child: Text('Umur 2 minggu'),
),
DropdownMenuItem<int>(
value: 3,
child: Text('Umur 3 minggu'),
),
DropdownMenuItem<int>(
value: 4,
child: Text('Umur 4 minggu'),
),
],
onChanged: (int? newValue) {
if (newValue != null) {
setState(() {
selectedAge = newValue;
_applyFilter();
});
}
},
),
),
),
),
SizedBox(width: 10),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(Icons.file_download, color: Color(0xFFA82429)),
onPressed: _downloadNotificationsAsExcel,
tooltip: 'Unduh Data Excel',
),
),
],
),
],
),
),
// Paginasi controls
Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'Baris per halaman: ',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _rowsPerPage,
isDense: true,
items: _rowsPerPageOptions.map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(value.toString()),
);
}).toList(),
onChanged: (newValue) {
if (newValue != null) {
setState(() {
_rowsPerPage = newValue;
_currentPage = 0; // Reset ke halaman pertama
});
}
},
),
),
),
],
),
if (totalPages > 0)
Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back_ios, size: 16),
color: _currentPage > 0 ? Color(0xFFA82429) : Colors.grey,
onPressed: _currentPage > 0
? () {
setState(() {
_currentPage--;
});
}
: null,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${_currentPage + 1} / $totalPages',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: Icon(Icons.arrow_forward_ios, size: 16),
color: _currentPage < totalPages - 1 ? Color(0xFFA82429) : Colors.grey,
onPressed: _currentPage < totalPages - 1
? () {
setState(() {
_currentPage++;
});
}
: null,
),
],
),
],
),
),
// List of notifications
Expanded(
child: RefreshIndicator(
onRefresh: _refreshNotifications,
color: Color(0xFFA82429),
child: paginatedData.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.notifications_off,
size: 64,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
selectedAge == 0
? 'Tidak ada notifikasi untuk umur ayam $ageInWeeks minggu'
: 'Tidak ada notifikasi untuk umur ayam $selectedAge minggu',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
textAlign: TextAlign.center,
),
],
),
)
: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: paginatedData.length,
itemBuilder: (context, index) {
final notif = paginatedData[index];
final DateTime dateTime = DateTime.parse(notif['timestamp']);
String eventText = notif['event'] ?? 'Data tidak tersedia';
// Ekstrak umur dari teks event jika ada
int eventAge = 2; // Default untuk data lama tanpa tag
if (eventText.contains("[Umur:")) {
RegExp regExp = RegExp(r"\[Umur: (\d+) minggu\]");
Match? match = regExp.firstMatch(eventText);
if (match != null && match.groupCount >= 1) {
eventAge = int.parse(match.group(1)!);
}
// Hapus tag umur dari tampilan
eventText = eventText.replaceAll(regExp, "").trim();
}
// Tentukan warna berdasarkan jenis notifikasi
Color cardColor;
Color timeColor;
IconData iconData;
if (eventText.contains('Kritis') ||
eventText.contains('Sangat Tinggi') ||
eventText.contains('Sangat Rendah') ||
eventText.contains('darurat')) {
cardColor = Colors.red;
timeColor = Colors.red[200]!;
// Use temperature or humidity specific icons when relevant
if (eventText.contains('Suhu')) {
iconData = Icons.thermostat;
} else if (eventText.contains('Kelembapan')) {
iconData = Icons.water_drop;
} else {
iconData = Icons.warning;
}
} else if (eventText.contains('tinggi') || eventText.contains('rendah')) {
cardColor = Colors.orange;
timeColor = Colors.orange[200]!;
// Use temperature or humidity specific icons when relevant
if (eventText.contains('Suhu')) {
iconData = Icons.thermostat;
} else if (eventText.contains('Kelembapan')) {
iconData = Icons.water_drop;
} else {
iconData = Icons.warning_amber;
}
} else if (eventText.contains('Fuzzy')) {
cardColor = Colors.blue;
timeColor = Colors.blue[200]!;
// Use temperature or humidity specific icons for Fuzzy notifications
if (eventText.contains('Suhu')) {
iconData = Icons.thermostat;
} else if (eventText.contains('Kelembapan')) {
iconData = Icons.water_drop;
} else {
iconData = Icons.auto_graph;
}
} else if (eventText.contains('Status')) {
cardColor = Colors.purple;
timeColor = Colors.purple[200]!;
// Use temperature or humidity specific icons for Status notifications
if (eventText.contains('Suhu')) {
iconData = Icons.thermostat;
} else if (eventText.contains('Kelembapan')) {
iconData = Icons.water_drop;
} else {
iconData = Icons.info;
}
} else if (eventText.contains('normal') ||
eventText.contains('Normal') ||
eventText.contains('Optimal')) {
cardColor = Colors.green;
timeColor = Colors.green[200]!;
// Use temperature or humidity specific icons for normal notifications
if (eventText.contains('Suhu')) {
iconData = Icons.thermostat;
} else if (eventText.contains('Kelembapan')) {
iconData = Icons.water_drop;
} else {
iconData = Icons.check_circle;
}
} else {
cardColor = Colors.blueGrey;
timeColor = Colors.blueGrey[200]!;
iconData = Icons.notifications;
}
return Card(
elevation: 3,
margin: EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Column(
children: [
// Colored header
Container(
color: cardColor,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
iconData,
color: Colors.white,
size: 20,
),
SizedBox(width: 8),
Expanded(
child: Text(
DateFormat('dd/MM/yyyy HH:mm').format(dateTime),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: timeColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Umur: $eventAge minggu',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: cardColor,
),
),
),
],
),
),
// Message body
Container(
width: double.infinity,
padding: EdgeInsets.all(16),
child: Text(
eventText,
style: TextStyle(
fontSize: 16,
color: Colors.black87,
),
),
),
],
),
),
);
},
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
class RangeControlPage extends StatefulWidget {
@override
_RangeControlPageState createState() => _RangeControlPageState();
}
class _RangeControlPageState extends State<RangeControlPage> {
final DatabaseReference _dbRef = FirebaseDatabase.instance.ref("ranges");
int selectedWeek = 1;
Map<String, dynamic> ranges = {};
@override
void initState() {
super.initState();
_fetchRangesFromFirebase();
}
void _fetchRangesFromFirebase() {
_dbRef.onValue.listen((event) {
final data = event.snapshot.value;
if (data != null) {
setState(() {
ranges = Map<String, dynamic>.from(data as Map);
});
}
});
}
Future<void> _updateRange(String type, String value, double newValue) async {
String path = "week$selectedWeek/$type/$value";
await _dbRef.child(path).set(newValue);
}
Widget _buildRangeSlider(String type, String value, double min, double max) {
double currentValue = ranges['week$selectedWeek']?[type]?[value]?.toDouble() ?? min;
return Column(
children: [
Text(
"${type == 'temperature' ? 'Suhu' : 'Kelembapan'} ${value == 'min' ? 'Minimum' : value == 'max' ? 'Maksimum' : 'Target'}",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Slider(
value: currentValue,
min: min,
max: max,
divisions: (max - min).toInt() * 2,
label: currentValue.toStringAsFixed(1),
onChanged: (newValue) {
_updateRange(type, value, newValue);
},
),
Text(
"${currentValue.toStringAsFixed(1)}${type == 'temperature' ? '°C' : '%'}",
style: TextStyle(fontSize: 18),
),
SizedBox(height: 20),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFA82429),
title: Text(
"Kontrol Rentang",
style: TextStyle(color: Colors.white),
),
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(20),
child: Column(
children: [
// Pemilih minggu
DropdownButtonFormField<int>(
value: selectedWeek,
decoration: InputDecoration(
labelText: "Pilih Minggu",
border: OutlineInputBorder(),
),
items: [1, 2, 3, 4].map((week) {
return DropdownMenuItem(
value: week,
child: Text("Minggu $week"),
);
}).toList(),
onChanged: (value) {
setState(() {
selectedWeek = value!;
});
},
),
SizedBox(height: 30),
// Kontrol suhu
Text(
"Rentang Suhu",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
_buildRangeSlider("temperature", "min", 20, 40),
_buildRangeSlider("temperature", "target", 20, 40),
_buildRangeSlider("temperature", "max", 20, 40),
Divider(height: 40),
// Kontrol kelembapan
Text(
"Rentang Kelembapan",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
_buildRangeSlider("humidity", "min", 30, 80),
_buildRangeSlider("humidity", "target", 30, 80),
_buildRangeSlider("humidity", "max", 30, 80),
],
),
),
);
}
}

View File

444
lib/screens/settings.dart Normal file
View File

@ -0,0 +1,444 @@
import 'package:flutter/material.dart';
import 'package:jago/screens/login.dart';
import 'package:jago/services/db_helper.dart'; // Import DatabaseHelper
class SettingsPage extends StatelessWidget {
final DatabaseHelper dbHelper = DatabaseHelper();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFFA82429),
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
Navigator.pop(context);
},
),
title: Text(
'Pengaturan',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
body: Column(
children: [
// Header dengan background curved
Container(
padding: EdgeInsets.fromLTRB(16, 0, 16, 20),
decoration: BoxDecoration(
color: Color(0xFFA82429),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Center(
child: Column(
children: [
SizedBox(height: 10),
CircleAvatar(
backgroundColor: Colors.white,
radius: 40,
child: Icon(
Icons.settings,
size: 40,
color: Color(0xFFA82429),
),
),
SizedBox(height: 10),
Text(
"Pengaturan Aplikasi",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 5),
Text(
"Kelola preferensi dan data aplikasi",
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
),
),
),
// Settings options
Expanded(
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
// Akun Section
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'AKUN',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
// Logout Card
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _confirmLogout(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Color(0xFFA82429).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.logout,
color: Color(0xFFA82429),
size: 24,
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Logout',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
'Keluar dari aplikasi',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.grey,
),
],
),
),
),
),
SizedBox(height: 24),
// Data Section
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'DATA & PENYIMPANAN',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
// Reset Data Card
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _confirmResetData(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.delete_forever,
color: Colors.red,
size: 24,
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Reset Data',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
'Hapus semua data aplikasi',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.grey,
),
],
),
),
),
),
// Info Aplikasi
SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'TENTANG APLIKASI',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Image.asset(
'assets/logo.png',
width: 40,
height: 40,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.eco,
color: Color(0xFFA82429),
size: 40,
);
},
),
SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'JAGO',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFFA82429),
),
),
Text(
'Versi 1.0.0',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
SizedBox(height: 16),
Text(
'Aplikasi otomasi pada kandang anak ayam.',
style: TextStyle(
fontSize: 14,
),
),
SizedBox(height: 8),
Text(
'© 2025 JAGO Developer Mohammad Iqbal Abdillah ',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
),
],
),
),
],
),
);
}
// Method untuk konfirmasi logout
void _confirmLogout(BuildContext context) async {
bool confirm = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
title: Column(
children: [
Icon(
Icons.logout,
color: Color(0xFFA82429),
size: 40,
),
SizedBox(height: 10),
Text(
'Konfirmasi Logout',
style: TextStyle(
color: Color(0xFFA82429),
fontWeight: FontWeight.bold,
),
),
],
),
content: Text('Apakah Anda yakin ingin keluar dari aplikasi?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('Batal'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text('Ya, Keluar', style: TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFA82429),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
);
},
) ?? false;
if (confirm) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => LoginScreen()),
(Route<dynamic> route) => false,
);
}
}
// Method untuk konfirmasi reset data
void _confirmResetData(BuildContext context) async {
bool confirm = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
title: Column(
children: [
Icon(
Icons.warning_amber_rounded,
color: Colors.red,
size: 40,
),
SizedBox(height: 10),
Text(
'Konfirmasi Reset Data',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
'Semua data aplikasi akan dihapus dan tidak dapat dikembalikan. Apakah Anda yakin ingin melanjutkan?',
textAlign: TextAlign.center,
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('Batal'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text('Ya, Reset Data', style: TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
);
},
) ?? false;
if (confirm) {
await dbHelper.resetDatabase();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Data berhasil direset'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {},
),
),
);
}
}
}

106
lib/screens/splash.dart Normal file
View File

@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'login.dart'; // Pastikan file login.dart diimport
class SplashScreen extends StatefulWidget {
@override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_controller.forward();
Future.delayed(Duration(seconds: 3), () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => LoginScreen()),
);
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Color(0xFFFDEDEE)],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo dengan animasi
ScaleTransition(
scale: _animation,
child: Image.asset(
'assets/jago.jpg',
width: 220,
height: 220,
fit: BoxFit.contain,
),
),
SizedBox(height: 30),
// Text dengan fade in
FadeTransition(
opacity: _animation,
child: Text(
"JAGO",
style: TextStyle(
color: Color(0xFFA82429),
fontSize: 36,
fontWeight: FontWeight.bold,
letterSpacing: 2.0,
),
),
),
SizedBox(height: 10),
FadeTransition(
opacity: _animation,
child: Text(
"Aplikasi Otomasi Pada Kandang Anak Ayam",
style: TextStyle(
color: Colors.black87,
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 50),
// Loading indicator
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFA82429)),
),
],
),
),
),
);
}
}

View File

252
lib/services/db_helper.dart Normal file
View File

@ -0,0 +1,252 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
static Database? _database;
DatabaseHelper._internal();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'history.db');
return await openDatabase(
path,
version: 5, // Naikkan versi agar onUpgrade dijalankan
onCreate: (db, version) async {
print("📌 onCreate: Membuat database...");
await db.execute('''
CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
lampu INTEGER NOT NULL,
kipas INTEGER NOT NULL,
event TEXT NOT NULL
)
''');
await db.execute('''
CREATE TABLE notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
event TEXT NOT NULL
)
''');
await db.execute('''
CREATE TABLE angkakematian (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tanggal TEXT NOT NULL,
angka INTEGER NOT NULL,
kloter INTEGER NOT NULL
)
''');
},
onUpgrade: (db, oldVersion, newVersion) async {
print("📌 onUpgrade: Upgrade database dari versi $oldVersion ke $newVersion...");
if (oldVersion < 5) {
await db.execute('DROP TABLE IF EXISTS notifications');
await db.execute('''
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
event TEXT NOT NULL
)
''');
}
if (oldVersion < 5) {
await db.execute('DROP TABLE IF EXISTS angkakematian');
await db.execute('''
CREATE TABLE IF NOT EXISTS angkakematian (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tanggal TEXT NOT NULL,
angka INTEGER NOT NULL,
kloter INTEGER NOT NULL
)
''');
print("✅ Tabel angkakematian berhasil dibuat di upgrade.");
}
if (oldVersion < 5) {
await db.execute('''
ALTER TABLE angkakematian ADD COLUMN kloter INTEGER;
''');
}
},
);
}
Future<void> insertAngkaKematian(Map<String, dynamic> data) async {
final db = await database;
try {
await db.insert('angkakematian', data);
print("✅ Data angka kematian berhasil disimpan: $data");
} catch (e) {
print("❌ Gagal menyimpan angka kematian: $e");
}
}
Future<List<Map<String, dynamic>>> getAngkaKematian() async {
final db = await database;
return await db.query('angkakematian', orderBy: 'id DESC');
}
// 🔹 DEBUG: Cek apakah tabel notifications sudah ada
Future<void> checkTables() async {
final db = await database;
List<Map<String, dynamic>> tables = await db.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='notifications'");
if (tables.isEmpty) {
print("⚠️ Tabel notifications belum ada. Membuat tabel...");
await db.execute('''
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
event TEXT NOT NULL
)
''');
print("✅ Tabel notifications berhasil dibuat.");
} else {
print("✅ Tabel notifications sudah ada.");
// Debug: tampilkan jumlah record
List<Map<String, dynamic>> count = await db.rawQuery("SELECT COUNT(*) as count FROM notifications");
print("📊 Jumlah record di tabel notifications: ${count.first['count']}");
// Debug: tampilkan beberapa record terakhir
List<Map<String, dynamic>> lastRecords = await db.query(
'notifications',
orderBy: 'id DESC',
limit: 5
);
if (lastRecords.isNotEmpty) {
print("📊 5 record terakhir: $lastRecords");
} else {
print("📊 Belum ada record di tabel notifications");
}
}
}
// 🔹 INSERT DATA KE HISTORY
Future<void> insertHistory(Map<String, dynamic> data) async {
final db = await database;
try {
await db.insert('history', data);
print("✅ Data history berhasil disimpan: $data");
} catch (e) {
print("❌ Gagal menyimpan history: $e");
}
}
// 🔹 GET DATA HISTORY
Future<List<Map<String, dynamic>>> getHistory() async {
final db = await database;
return await db.query('history', orderBy: 'id DESC');
}
// 🔹 INSERT DATA KE NOTIFICATIONS (Format sudah sesuai tabel)
Future<void> insertNotification(String event) async {
final db = await database;
String timestamp = DateTime.now().toIso8601String();
try {
// Periksa apakah tabel notifications ada
List<Map<String, dynamic>> tables = await db.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='notifications'");
if (tables.isEmpty) {
print("⚠️ Tabel notifications belum ada saat menyimpan notifikasi. Membuat tabel...");
await db.execute('''
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
event TEXT NOT NULL
)
''');
}
int id = await db.insert('notifications', {
'timestamp': timestamp,
'event': event,
});
print("✅ Notifikasi berhasil disimpan dengan ID: $id, Event: $event");
} catch (e) {
print("❌ Gagal menyimpan notifikasi: $e");
}
}
// 🔹 GET DATA NOTIFICATIONS
Future<List<Map<String, dynamic>>> getNotifications() async {
final db = await database;
try {
// Periksa apakah tabel notifications ada
List<Map<String, dynamic>> tables = await db.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='notifications'");
if (tables.isEmpty) {
print("⚠️ Tabel notifications belum ada. Membuat tabel...");
await db.execute('''
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
event TEXT NOT NULL
)
''');
print("✅ Tabel notifications berhasil dibuat.");
return [];
}
List<Map<String, dynamic>> result = await db.query(
'notifications',
orderBy: 'id DESC',
);
print("✅ Data notifikasi yang diambil: ${result.length} item");
// Debug: tampilkan beberapa data notifikasi terakhir
if (result.isNotEmpty) {
print("📊 Data notifikasi terakhir: ${result.first}");
}
return result;
} catch (e) {
print("❌ Error saat mengambil data notifikasi: $e");
return [];
}
}
// 🔹 DELETE ALL NOTIFICATIONS
Future<void> clearNotifications() async {
final db = await database;
await db.delete('notifications');
print("✅ Semua notifikasi berhasil dihapus");
}
// 🔹 Hapus database (Gunakan ini jika ingin reset database manual)
Future<void> resetDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'history.db');
await deleteDatabase(path);
print("✅ Database berhasil dihapus.");
}
Future<void> deleteAngkaKematian(String tanggal, String kloter) async {
final db = await database;
await db.delete(
'angkakematian',
where: 'tanggal = ? AND kloter = ?',
whereArgs: [tanggal, kloter],
);
print("✅ Data angka kematian dengan tanggal $tanggal dan kloter $kloter berhasil dihapus.");
}
Future<void> deleteAngkaKematianByKloter(String kloter) async {
final db = await database;
await db.delete('angkakematian', where: 'kloter = ?', whereArgs: [kloter]);
}
}

View File

@ -0,0 +1,340 @@
import 'package:firebase_database/firebase_database.dart';
import 'package:jago/services/db_helper.dart';
import 'dart:async';
class HistoryService {
static final HistoryService _instance = HistoryService._internal();
factory HistoryService() => _instance;
final DatabaseHelper dbHelper = DatabaseHelper();
final DatabaseReference _database = FirebaseDatabase.instance.ref();
// Status saat ini
Map<String, dynamic> currentRelay = {'Lampu': false, 'Kipas': false};
Map<String, dynamic> currentControl = {
'fan': {'auto': true, 'status': false},
'light': {'auto': true, 'status': false}
};
int ageInWeeks = 2;
// Daftar subscription untuk dibersihkan saat dispose
final List<StreamSubscription> _subscriptions = [];
// Flag untuk menandai apakah service sudah diinisialisasi
bool _isInitialized = false;
HistoryService._internal();
Future<void> initialize() async {
if (_isInitialized) return;
print("🚀 Menginisialisasi HistoryService...");
// Ambil umur ayam dari Firebase
_fetchAgeFromFirebase();
// Mulai memantau data
_listenForRelayStatus();
_listenForControlStatus();
_listenForStatusUpdates();
_listenForLogs();
_isInitialized = true;
print("✅ HistoryService berhasil diinisialisasi");
}
void dispose() {
for (var subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
_isInitialized = false;
print("🛑 HistoryService dihentikan");
}
void _fetchAgeFromFirebase() {
final subscription = _database.child("chicken_age").onValue.listen((event) {
final data = event.snapshot.value;
if (data != null) {
ageInWeeks = int.parse(data.toString());
print("🐔 Umur ayam diperbarui: $ageInWeeks minggu");
}
});
_subscriptions.add(subscription);
}
void _listenForRelayStatus() {
final subscription = _database.child('relay').onValue.listen((event) {
if (event.snapshot.value != null) {
try {
final Map<dynamic, dynamic> relayData = event.snapshot.value as Map;
bool lampStatus = relayData['Lampu'] is bool ? relayData['Lampu'] : relayData['Lampu'].toString().toLowerCase() == 'true';
bool fanStatus = relayData['Kipas'] is bool ? relayData['Kipas'] : relayData['Kipas'].toString().toLowerCase() == 'true';
// Periksa apakah status berubah
if (lampStatus != currentRelay['Lampu'] || fanStatus != currentRelay['Kipas']) {
String eventDescription = '[RELAY] Lampu ${lampStatus ? "Hidup" : "Mati"}, Kipas ${fanStatus ? "Hidup" : "Mati"}';
// Tambahkan informasi umur ayam
eventDescription += " [Umur: $ageInWeeks minggu]";
Map<String, dynamic> newEntry = {
'timestamp': DateTime.now().toIso8601String(),
'lampu': lampStatus ? 1 : 0,
'kipas': fanStatus ? 1 : 0,
'event': eventDescription
};
// Simpan ke database lokal
dbHelper.insertHistory(newEntry);
print("✅ Data relay berhasil disimpan: $newEntry");
// Update status saat ini
currentRelay['Lampu'] = lampStatus;
currentRelay['Kipas'] = fanStatus;
}
} catch (e) {
print('❌ Error processing relay data: $e');
}
}
}, onError: (error) {
print('❌ Error listening to relay: $error');
});
_subscriptions.add(subscription);
}
void _listenForControlStatus() {
final subscription = _database.child('control').onValue.listen((event) {
if (event.snapshot.value != null) {
try {
final Map<dynamic, dynamic> controlData = event.snapshot.value as Map;
// Ambil status kontrol kipas
bool fanAuto = controlData['fan']['auto'] is bool ?
controlData['fan']['auto'] :
controlData['fan']['auto'].toString().toLowerCase() == 'true';
bool fanStatus = controlData['fan']['status'] is bool ?
controlData['fan']['status'] :
controlData['fan']['status'].toString().toLowerCase() == 'true';
// Ambil status kontrol lampu
bool lightAuto = controlData['light']['auto'] is bool ?
controlData['light']['auto'] :
controlData['light']['auto'].toString().toLowerCase() == 'true';
bool lightStatus = controlData['light']['status'] is bool ?
controlData['light']['status'] :
controlData['light']['status'].toString().toLowerCase() == 'true';
// Periksa apakah status berubah
bool fanAutoChanged = fanAuto != currentControl['fan']['auto'];
bool fanStatusChanged = fanStatus != currentControl['fan']['status'];
bool lightAutoChanged = lightAuto != currentControl['light']['auto'];
bool lightStatusChanged = lightStatus != currentControl['light']['status'];
if (fanAutoChanged || fanStatusChanged || lightAutoChanged || lightStatusChanged) {
String eventDescription = '[CONTROL] ';
if (fanAutoChanged) {
eventDescription += 'Kipas mode ${fanAuto ? "Otomatis" : "Manual"}, ';
}
if (fanStatusChanged && !fanAuto) {
eventDescription += 'Kipas ${fanStatus ? "ON" : "OFF"}, ';
}
if (lightAutoChanged) {
eventDescription += 'Lampu mode ${lightAuto ? "Otomatis" : "Manual"}, ';
}
if (lightStatusChanged && !lightAuto) {
eventDescription += 'Lampu ${lightStatus ? "ON" : "OFF"}, ';
}
// Hapus koma dan spasi di akhir
eventDescription = eventDescription.replaceAll(RegExp(r', $'), '');
// Tambahkan informasi umur ayam
eventDescription += " [Umur: $ageInWeeks minggu]";
Map<String, dynamic> newEntry = {
'timestamp': DateTime.now().toIso8601String(),
'lampu': lightStatus ? 1 : 0,
'kipas': fanStatus ? 1 : 0,
'event': eventDescription
};
// Simpan ke database lokal
dbHelper.insertHistory(newEntry);
print("✅ Data control berhasil disimpan: $newEntry");
// Update status saat ini
currentControl['fan']['auto'] = fanAuto;
currentControl['fan']['status'] = fanStatus;
currentControl['light']['auto'] = lightAuto;
currentControl['light']['status'] = lightStatus;
}
} catch (e) {
print('❌ Error processing control data: $e');
}
}
}, onError: (error) {
print('❌ Error listening to control: $error');
});
_subscriptions.add(subscription);
}
void _listenForStatusUpdates() {
// Pantau status suhu
final subscription = _database.child('status/temperature').onValue.listen((event) {
if (event.snapshot.value != null) {
String status = event.snapshot.value.toString();
String eventDescription = '[STATUS] Suhu: $status';
// Tambahkan informasi umur ayam
eventDescription += " [Umur: $ageInWeeks minggu]";
Map<String, dynamic> newEntry = {
'timestamp': DateTime.now().toIso8601String(),
'lampu': currentRelay['Lampu'] ? 1 : 0,
'kipas': currentRelay['Kipas'] ? 1 : 0,
'event': eventDescription
};
// Simpan ke database lokal
dbHelper.insertHistory(newEntry);
print("✅ Data status suhu berhasil disimpan: $newEntry");
}
});
_subscriptions.add(subscription);
// Pantau status kelembapan
final subscription2 = _database.child('status/humidity').onValue.listen((event) {
if (event.snapshot.value != null) {
String status = event.snapshot.value.toString();
String eventDescription = '[STATUS] Kelembapan: $status';
// Tambahkan informasi umur ayam
eventDescription += " [Umur: $ageInWeeks minggu]";
Map<String, dynamic> newEntry = {
'timestamp': DateTime.now().toIso8601String(),
'lampu': currentRelay['Lampu'] ? 1 : 0,
'kipas': currentRelay['Kipas'] ? 1 : 0,
'event': eventDescription
};
// Simpan ke database lokal
dbHelper.insertHistory(newEntry);
print("✅ Data status kelembapan berhasil disimpan: $newEntry");
}
});
_subscriptions.add(subscription2);
}
void _listenForLogs() {
// Pantau log sistem
final logsRef = _database.child('logs');
// Pantau log WiFi
final subscription = logsRef.child('wifi').onValue.listen((event) {
if (event.snapshot.value != null) {
String logMessage = event.snapshot.value.toString();
String eventDescription = '[SYSTEM] WiFi: $logMessage';
// Tambahkan informasi umur ayam
eventDescription += " [Umur: $ageInWeeks minggu]";
Map<String, dynamic> newEntry = {
'timestamp': DateTime.now().toIso8601String(),
'lampu': currentRelay['Lampu'] ? 1 : 0,
'kipas': currentRelay['Kipas'] ? 1 : 0,
'event': eventDescription
};
// Simpan ke database lokal
dbHelper.insertHistory(newEntry);
print("✅ Data log WiFi berhasil disimpan: $newEntry");
}
});
_subscriptions.add(subscription);
// Pantau log sensor
final subscription2 = logsRef.child('sensor').onValue.listen((event) {
if (event.snapshot.value != null) {
String logMessage = event.snapshot.value.toString();
String eventDescription = '[SENSOR] $logMessage';
// Tambahkan informasi umur ayam
eventDescription += " [Umur: $ageInWeeks minggu]";
Map<String, dynamic> newEntry = {
'timestamp': DateTime.now().toIso8601String(),
'lampu': currentRelay['Lampu'] ? 1 : 0,
'kipas': currentRelay['Kipas'] ? 1 : 0,
'event': eventDescription
};
// Simpan ke database lokal
dbHelper.insertHistory(newEntry);
print("✅ Data log sensor berhasil disimpan: $newEntry");
}
});
_subscriptions.add(subscription2);
// Pantau log LCD
final subscription3 = logsRef.child('lcd').onValue.listen((event) {
if (event.snapshot.value != null) {
String logMessage = event.snapshot.value.toString();
String eventDescription = '[SYSTEM] LCD: $logMessage';
// Tambahkan informasi umur ayam
eventDescription += " [Umur: $ageInWeeks minggu]";
Map<String, dynamic> newEntry = {
'timestamp': DateTime.now().toIso8601String(),
'lampu': currentRelay['Lampu'] ? 1 : 0,
'kipas': currentRelay['Kipas'] ? 1 : 0,
'event': eventDescription
};
// Simpan ke database lokal
dbHelper.insertHistory(newEntry);
print("✅ Data log LCD berhasil disimpan: $newEntry");
}
});
_subscriptions.add(subscription3);
// Pantau log sistem
final subscription4 = logsRef.child('system').onValue.listen((event) {
if (event.snapshot.value != null) {
String logMessage = event.snapshot.value.toString();
String eventDescription = '[SYSTEM] $logMessage';
// Tambahkan informasi umur ayam
eventDescription += " [Umur: $ageInWeeks minggu]";
Map<String, dynamic> newEntry = {
'timestamp': DateTime.now().toIso8601String(),
'lampu': currentRelay['Lampu'] ? 1 : 0,
'kipas': currentRelay['Kipas'] ? 1 : 0,
'event': eventDescription
};
// Simpan ke database lokal
dbHelper.insertHistory(newEntry);
print("✅ Data log sistem berhasil disimpan: $newEntry");
}
});
_subscriptions.add(subscription4);
}
}

View File

@ -0,0 +1,423 @@
//import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:jago/services/db_helper.dart';
import 'dart:async';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
final DatabaseReference _database = FirebaseDatabase.instance.ref();
final DatabaseHelper dbHelper = DatabaseHelper();
int ageInWeeks = 2;
// Inisialisasi notifikasi lokal
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
// Daftar subscription untuk dibersihkan saat dispose
final List<StreamSubscription> _subscriptions = [];
// Flag untuk mencegah duplikasi notifikasi dalam waktu singkat
final Map<String, DateTime> _lastNotificationTimes = {};
// Flag untuk menandai apakah service sudah diinisialisasi
bool _isInitialized = false;
NotificationService._internal();
Future<void> initialize() async {
if (_isInitialized) return;
print("🚀 Menginisialisasi NotificationService...");
// Inisialisasi database
await dbHelper.checkTables();
// Inisialisasi notifikasi lokal
const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
// Setup Firebase Messaging
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
_showNotification(message.notification?.title ?? 'Notifikasi', message.notification?.body ?? '');
});
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Ambil umur ayam dari Firebase
_fetchAgeFromFirebase();
// Mulai memantau data
_startMonitoring();
_isInitialized = true;
print("✅ NotificationService berhasil diinisialisasi");
}
void dispose() {
for (var subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
_isInitialized = false;
print("🛑 NotificationService dihentikan");
}
static Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print("📱 Menerima pesan di background: ${message.notification?.title}");
await NotificationService().initialize();
NotificationService()._showNotification(
message.notification?.title ?? 'Notifikasi',
message.notification?.body ?? ''
);
}
Future<void> _showNotification(String title, String body) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails(
'Jago',
'Notification',
channelDescription: 'Cek Notification!!!',
importance: Importance.max,
priority: Priority.high,
);
const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
title,
body,
platformChannelSpecifics,
);
}
void _fetchAgeFromFirebase() {
print("🐔 Memuat umur ayam dari Firebase...");
final subscription = _database.child("chicken_age").onValue.listen((event) {
final data = event.snapshot.value;
if (data != null) {
ageInWeeks = int.parse(data.toString());
print("🐔 Umur ayam diperbarui: $ageInWeeks minggu");
}
});
_subscriptions.add(subscription);
}
void _startMonitoring() {
print("👀 Mulai memantau data dari Firebase...");
// Pastikan umur ayam diambil terlebih dahulu
_fetchAgeFromFirebase();
// Delay sedikit untuk memastikan umur ayam sudah diambil
Future.delayed(Duration(milliseconds: 500), () {
_loadEmergencyData();
_loadFuzzyData();
_loadStatusData();
_loadSensorData();
});
}
// Memuat data emergency dari Firebase
void _loadEmergencyData() {
try {
print("🔄 Memuat data emergency...");
final subscription = _database.child('emergency').onValue.listen((event) {
if (event.snapshot.value != null) {
try {
print("🔥 Data emergency diterima: ${event.snapshot.value}");
final Map<dynamic, dynamic> emergencyData = event.snapshot.value as Map;
if (emergencyData['low_temperature'] == true) {
print("🔥 Kondisi emergency: Suhu sangat rendah terdeteksi");
_addNotification('Suhu sangat rendah! Kondisi darurat terdeteksi.');
}
if (emergencyData['high_temperature'] == true) {
print("🔥 Kondisi emergency: Suhu sangat tinggi terdeteksi");
_addNotification('Suhu sangat tinggi! Kondisi darurat terdeteksi.');
}
if (emergencyData['low_humidity'] == true) {
print("🔥 Kondisi emergency: Kelembapan sangat rendah terdeteksi");
_addNotification('Kelembapan sangat rendah! Kondisi darurat terdeteksi.');
}
if (emergencyData['high_humidity'] == true) {
print("🔥 Kondisi emergency: Kelembapan sangat tinggi terdeteksi");
_addNotification('Kelembapan sangat tinggi! Kondisi darurat terdeteksi.');
}
} catch (e) {
print("❌ Error saat memproses data emergency: $e");
}
}
});
_subscriptions.add(subscription);
// Juga dengarkan perubahan pada node emergency
final changeSubscription = _database.child('emergency').onChildChanged.listen((event) {
if (event.snapshot.value == true) {
String type = event.snapshot.key ?? "unknown";
String message = "";
print("🔥 Perubahan emergency terdeteksi: ${event.snapshot.key} = ${event.snapshot.value}");
if (type == "low_temperature") {
message = "Suhu sangat rendah! Kondisi darurat terdeteksi.";
} else if (type == "high_temperature") {
message = "Suhu sangat tinggi! Kondisi darurat terdeteksi.";
} else if (type == "low_humidity") {
message = "Kelembapan sangat rendah! Kondisi darurat terdeteksi.";
} else if (type == "high_humidity") {
message = "Kelembapan sangat tinggi! Kondisi darurat terdeteksi.";
}
if (message.isNotEmpty) {
_addNotification(message);
}
}
});
_subscriptions.add(changeSubscription);
} catch (e) {
print("❌ Error saat setup listener emergency: $e");
}
}
// Memuat data fuzzy dari Firebase
void _loadFuzzyData() {
try {
print("🔄 Memuat data fuzzy...");
// Memuat data fuzzy suhu
final tempSubscription = _database.child('fuzzy/temperature').onValue.listen((event) {
if (event.snapshot.value != null) {
try {
print("🧠 Data fuzzy temperature diterima: ${event.snapshot.value}");
final Map<dynamic, dynamic> fuzzyTemp = event.snapshot.value as Map;
if (fuzzyTemp.containsKey('veryHigh') && (fuzzyTemp['veryHigh'] as num).toDouble() > 0.7) {
double value = (fuzzyTemp['veryHigh'] as num).toDouble();
print("🧠 Fuzzy suhu sangat tinggi terdeteksi: $value");
_addNotification('Fuzzy: Suhu sangat tinggi (${value.toStringAsFixed(2)})');
}
if (fuzzyTemp.containsKey('veryLow') && (fuzzyTemp['veryLow'] as num).toDouble() > 0.7) {
double value = (fuzzyTemp['veryLow'] as num).toDouble();
print("🧠 Fuzzy suhu sangat rendah terdeteksi: $value");
_addNotification('Fuzzy: Suhu sangat rendah (${value.toStringAsFixed(2)})');
}
} catch (e) {
print("❌ Error saat memproses data fuzzy temperature: $e");
}
}
});
_subscriptions.add(tempSubscription);
// Memuat data fuzzy kelembapan
final humSubscription = _database.child('fuzzy/humidity').onValue.listen((event) {
if (event.snapshot.value != null) {
try {
print("🧠 Data fuzzy humidity diterima: ${event.snapshot.value}");
final Map<dynamic, dynamic> fuzzyHum = event.snapshot.value as Map;
if (fuzzyHum.containsKey('veryHigh') && (fuzzyHum['veryHigh'] as num).toDouble() > 0.7) {
double value = (fuzzyHum['veryHigh'] as num).toDouble();
print("🧠 Fuzzy kelembapan sangat tinggi terdeteksi: $value");
_addNotification('Fuzzy: Kelembapan sangat tinggi (${value.toStringAsFixed(2)})');
}
if (fuzzyHum.containsKey('veryLow') && (fuzzyHum['veryLow'] as num).toDouble() > 0.7) {
double value = (fuzzyHum['veryLow'] as num).toDouble();
print("🧠 Fuzzy kelembapan sangat rendah terdeteksi: $value");
_addNotification('Fuzzy: Kelembapan sangat rendah (${value.toStringAsFixed(2)})');
}
} catch (e) {
print("❌ Error saat memproses data fuzzy humidity: $e");
}
}
});
_subscriptions.add(humSubscription);
} catch (e) {
print("❌ Error saat setup listener fuzzy: $e");
}
}
// Memuat data status dari Firebase
void _loadStatusData() {
try {
print("🔄 Memuat data status...");
// Memuat status suhu
final tempSubscription = _database.child('status/temperature').onValue.listen((event) {
if (event.snapshot.value != null) {
try {
String tempStatus = event.snapshot.value.toString();
print("📊 Status suhu diterima: $tempStatus");
if (tempStatus.contains("Sangat Tinggi") || tempStatus.contains("Sangat Rendah")) {
print("📊 Status suhu kritis terdeteksi: $tempStatus");
_addNotification('Status: $tempStatus');
}
} catch (e) {
print("❌ Error saat memproses data status temperature: $e");
}
}
});
_subscriptions.add(tempSubscription);
// Memuat status kelembapan
final humSubscription = _database.child('status/humidity').onValue.listen((event) {
if (event.snapshot.value != null) {
try {
String humStatus = event.snapshot.value.toString();
print("📊 Status kelembapan diterima: $humStatus");
if (humStatus.contains("Sangat Tinggi") || humStatus.contains("Sangat Rendah")) {
print("📊 Status kelembapan kritis terdeteksi: $humStatus");
_addNotification('Status: $humStatus');
}
} catch (e) {
print("❌ Error saat memproses data status humidity: $e");
}
}
});
_subscriptions.add(humSubscription);
// Mendengarkan perubahan pada node status
final statusSubscription = _database.child('status').onChildChanged.listen((event) {
String status = event.snapshot.value.toString();
String type = event.snapshot.key ?? "unknown";
print("📊 Perubahan status terdeteksi: $type = $status");
if (status.contains("Sangat Tinggi") || status.contains("Sangat Rendah")) {
print("📊 Status kritis pada perubahan terdeteksi: $type = $status");
_addNotification('Status $type: $status');
}
});
_subscriptions.add(statusSubscription);
} catch (e) {
print("❌ Error saat setup listener status: $e");
}
}
void _loadSensorData() {
print("🔄 Memuat data sensor...");
final subscription = _database.child('sensor').onValue.listen((event) {
if (event.snapshot.value != null) {
try {
final Map<dynamic, dynamic> sensorData = event.snapshot.value as Map;
double temperature = (sensorData['temperature'] as num).toDouble();
double humidity = (sensorData['Humidity'] as num).toDouble();
print("🌡️ Data sensor diterima: Suhu=$temperature°C, Kelembapan=$humidity%");
// Tentukan rentang suhu berdasarkan umur ayam
double tempMin, tempMax, humMin, humMax;
switch (ageInWeeks) {
case 1:
tempMin = 33.0;
tempMax = 35.0;
humMin = 60.0;
humMax = 70.0;
break;
case 2:
tempMin = 30.0;
tempMax = 33.0;
humMin = 60.0;
humMax = 65.0;
break;
case 3:
tempMin = 28.0;
tempMax = 30.0;
humMin = 60.0;
humMax = 65.0;
break;
case 4:
tempMin = 25.0;
tempMax = 28.0;
humMin = 55.0;
humMax = 60.0;
break;
default:
tempMin = 25.0;
tempMax = 28.0;
humMin = 55.0;
humMax = 60.0;
}
// Buat notifikasi berdasarkan kondisi suhu
if (temperature < tempMin - 2) {
print("🌡️ Suhu sangat rendah terdeteksi: $temperature°C");
_addNotification('Suhu sangat rendah: $temperature°C (Kritis)');
} else if (temperature < tempMin) {
print("🌡️ Suhu rendah terdeteksi: $temperature°C");
_addNotification('Suhu rendah: $temperature°C');
} else if (temperature > tempMax + 2) {
print("🌡️ Suhu sangat tinggi terdeteksi: $temperature°C");
_addNotification('Suhu sangat tinggi: $temperature°C (Kritis)');
} else if (temperature > tempMax) {
print("🌡️ Suhu tinggi terdeteksi: $temperature°C");
_addNotification('Suhu tinggi: $temperature°C');
} else if (temperature >= tempMin && temperature <= tempMax) {
print("🌡️ Suhu normal terdeteksi: $temperature°C");
_addNotification('Suhu normal: $temperature°C (Optimal)');
}
// Buat notifikasi berdasarkan kondisi kelembapan
if (humidity < humMin - 5) {
print("💧 Kelembapan sangat rendah terdeteksi: $humidity%");
_addNotification('Kelembapan sangat rendah: $humidity% (Kritis)');
} else if (humidity < humMin) {
print("💧 Kelembapan rendah terdeteksi: $humidity%");
_addNotification('Kelembapan rendah: $humidity%');
} else if (humidity > humMax + 5) {
print("💧 Kelembapan sangat tinggi terdeteksi: $humidity%");
_addNotification('Kelembapan sangat tinggi: $humidity% (Kritis)');
} else if (humidity > humMax) {
print("💧 Kelembapan tinggi terdeteksi: $humidity%");
_addNotification('Kelembapan tinggi: $humidity%');
} else if (humidity >= humMin && humidity <= humMax) {
print("💧 Kelembapan normal terdeteksi: $humidity%");
_addNotification('Kelembapan normal: $humidity% (Optimal)');
}
} catch (e) {
print("❌ Error saat memproses data sensor: $e");
}
}
});
_subscriptions.add(subscription);
}
// Fungsi untuk menambahkan notifikasi baru
void _addNotification(String event) {
print("🔍 Mencoba menambahkan notifikasi: $event");
// Cek apakah notifikasi serupa sudah ditambahkan dalam 5 menit terakhir
if (_lastNotificationTimes.containsKey(event)) {
DateTime lastTime = _lastNotificationTimes[event]!;
Duration difference = DateTime.now().difference(lastTime);
// Jika notifikasi serupa sudah ditambahkan dalam 5 menit terakhir, abaikan
if (difference.inMinutes < 5) {
print("⏱️ Notifikasi serupa sudah ditambahkan ${difference.inSeconds} detik yang lalu, diabaikan: $event");
return;
}
}
// Catat waktu notifikasi ini ditambahkan
_lastNotificationTimes[event] = DateTime.now();
// Tambahkan informasi umur ayam ke event
String eventWithAge = "$event [Umur: $ageInWeeks minggu]";
// Simpan ke database lokal
dbHelper.insertNotification(eventWithAge).then((_) {
print("✅ Berhasil menyimpan notifikasi: $eventWithAge");
// Tampilkan notifikasi lokal
_showNotification('Peringatan Kandang Ayam', event);
}).catchError((error) {
print("❌ Gagal menyimpan notifikasi: $error");
});
}
}

View File

@ -0,0 +1,127 @@
import 'package:firebase_database/firebase_database.dart';
import '../models/sensor_data.dart';
class SensorService {
final DatabaseReference _database = FirebaseDatabase.instance.ref();
// Stream untuk mendapatkan data sensor secara real-time
Stream<List<SensorData>> getSensorDataStream() {
// Membuat broadcast stream yang bisa didengarkan oleh banyak listener
return _database.child('sensor').onValue
.asBroadcastStream()
.map((event) {
print('Raw Firebase Data: ${event.snapshot.value}'); // Debug print
if (event.snapshot.value == null) {
print('Firebase returned null data, using default values');
// Mengembalikan nilai default jika tidak ada data
return [
SensorData(
name: 'Suhu',
value: 0.0,
timestamp: DateTime.now(),
),
SensorData(
name: 'Kelembapan',
value: 0.0,
timestamp: DateTime.now(),
),
];
}
try {
final data = Map<String, dynamic>.from(event.snapshot.value as Map);
print('Parsed Data: $data'); // Debug print
double tempValue = double.tryParse(data['temperature']?.toString() ?? '0.0') ?? 0.0;
double humidValue = double.tryParse(data['Humidity']?.toString() ?? '0.0') ?? 0.0;
print('Parsed Temperature: $tempValue, Humidity: $humidValue');
return [
SensorData(
name: 'Suhu',
value: tempValue,
timestamp: DateTime.now(),
),
SensorData(
name: 'Kelembapan',
value: humidValue,
timestamp: DateTime.now(),
),
];
} catch (e) {
print('Error parsing data: $e');
print('Original data was: ${event.snapshot.value}');
// Mengembalikan nilai default jika parsing gagal
return [
SensorData(
name: 'Suhu',
value: 0.0,
timestamp: DateTime.now(),
),
SensorData(
name: 'Kelembapan',
value: 0.0,
timestamp: DateTime.now(),
),
];
}
});
}
// Fungsi untuk menambah data sensor
Future<void> addSensorData(SensorData sensorData) async {
await _database.push().set({
'name': sensorData.name,
'value': sensorData.value,
'timestamp': sensorData.timestamp.millisecondsSinceEpoch,
});
}
// Fungsi untuk mengambil data sensor terakhir
Future<SensorData?> getLatestSensorData(String sensorName) async {
final DatabaseEvent event = await _database
.orderByChild('name')
.equalTo(sensorName)
.limitToLast(1)
.once();
final Map<dynamic, dynamic>? values =
event.snapshot.value as Map?;
if (values == null || values.isEmpty) return null;
final value = values.values.first;
return SensorData(
name: value['name'],
value: double.parse(value['value'].toString()),
timestamp: DateTime.fromMillisecondsSinceEpoch(value['timestamp']),
);
}
// Tambahkan stream untuk status relay
Stream<Map<String, bool>> getRelayStatusStream() {
return _database.child('relay').onValue
.asBroadcastStream()
.map((event) {
print('Raw Relay Data: ${event.snapshot.value}');
if (event.snapshot.value == null) return <String, bool>{};
try {
final data = Map<String, dynamic>.from(event.snapshot.value as Map);
print('Parsed Relay Data: $data');
return <String, bool>{
'lamp': data['Lampu'] ?? false,
'fan': data['Kipas'] ?? false,
};
} catch (e) {
print('Error parsing relay data: $e');
return <String, bool>{};
}
});
}
}

Some files were not shown because too many files have changed in this diff Show More