├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── iot_center_flutter_mvc │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── icons │ └── launcher_icon.png └── images │ ├── delete-device.png │ ├── device-dashboard-change-dashboard.png │ ├── device-dashboard-editable.png │ ├── device-dashboard-new-dashboard.png │ ├── device-dashboard.png │ ├── device-detail.png │ ├── device-measurements.png │ ├── edit-chart-delete.png │ ├── edit-chart-page.png │ ├── home-page.png │ ├── icons │ ├── add_dark_24dp.svg │ ├── add_white_24dp.svg │ ├── arrow_forward.svg │ ├── arrow_forward_dark.svg │ ├── autorenew_dark_24dp.svg │ ├── autorenew_white_24dp.svg │ ├── dashboard_customize_icon.svg │ ├── dashboard_customize_icon_dark.svg │ ├── delete_dark_24dp.svg │ ├── delete_white_24dp.svg │ ├── done_icon.svg │ ├── done_icon_dark.svg │ ├── edit_icon.svg │ ├── edit_icon_dark.svg │ ├── link_dark_24dp.svg │ ├── link_white_24dp.svg │ ├── lock_dark_24dp.svg │ ├── lock_open_dark_24dp.svg │ ├── lock_open_white_24dp.svg │ ├── lock_white_24dp.svg │ ├── settings_dark_24dp.svg │ ├── settings_white_24dp.svg │ ├── write_data.svg │ └── write_data_dark.svg │ ├── influxdata-icon.svg │ ├── influxdata-logo.png │ ├── new-device.png │ ├── settings-dashboards-add.png │ ├── settings-dashboards.png │ ├── settings-influx.png │ └── settings-sensors.png ├── config ├── data │ └── dynamic │ │ ├── demo.json │ │ ├── factory.svg │ │ └── schema.json ├── mosquitto.conf └── telegraf.conf ├── docker-compose.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── main.dart └── src │ ├── app │ ├── controller │ │ ├── app_controller.dart │ │ └── sensors.dart │ ├── model │ │ ├── device_config.dart │ │ ├── influx_client.dart │ │ └── influx_model.dart │ └── view │ │ ├── common │ │ ├── drop_down_list.dart │ │ ├── form_button.dart │ │ ├── form_row.dart │ │ ├── number_text_field.dart │ │ └── styles.dart │ │ └── my_app.dart │ ├── controller.dart │ ├── device │ ├── controller │ │ ├── chart_detail_controller.dart │ │ ├── dashboard_controller.dart │ │ └── device_detail_controller.dart │ ├── model │ │ ├── chart.dart │ │ ├── chart_data.dart │ │ └── device.dart │ └── view │ │ ├── chart_detail_page.dart │ │ ├── dashboard.dart │ │ ├── device_detail_page.dart │ │ ├── gauge_chart.dart │ │ └── simple_chart.dart │ ├── home │ ├── controller │ │ └── home_page_controller.dart │ └── view │ │ └── home_page.dart │ ├── model.dart │ ├── settings │ ├── controller │ │ └── settings_controller.dart │ └── view │ │ ├── clientId_dialog.dart │ │ ├── settings_page.dart │ │ └── tabs │ │ ├── dashboards_tab.dart │ │ ├── influx_settings_tab.dart │ │ └── sensors_tab.dart │ └── view.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5f105a6ca7a5ac7b8bc9b241f4c2d86f4188cf5c 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "com.example.iot_center_flutter_mvc" 47 | minSdkVersion flutter.minSdkVersion 48 | targetSdkVersion flutter.targetSdkVersion 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | } 69 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/iot_center_flutter_mvc/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.iot_center_flutter_mvc 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/icons/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/icons/launcher_icon.png -------------------------------------------------------------------------------- /assets/images/delete-device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/delete-device.png -------------------------------------------------------------------------------- /assets/images/device-dashboard-change-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/device-dashboard-change-dashboard.png -------------------------------------------------------------------------------- /assets/images/device-dashboard-editable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/device-dashboard-editable.png -------------------------------------------------------------------------------- /assets/images/device-dashboard-new-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/device-dashboard-new-dashboard.png -------------------------------------------------------------------------------- /assets/images/device-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/device-dashboard.png -------------------------------------------------------------------------------- /assets/images/device-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/device-detail.png -------------------------------------------------------------------------------- /assets/images/device-measurements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/device-measurements.png -------------------------------------------------------------------------------- /assets/images/edit-chart-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/edit-chart-delete.png -------------------------------------------------------------------------------- /assets/images/edit-chart-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/edit-chart-page.png -------------------------------------------------------------------------------- /assets/images/home-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/home-page.png -------------------------------------------------------------------------------- /assets/images/icons/add_dark_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/add_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/arrow_forward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/arrow_forward_dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/autorenew_dark_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/autorenew_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/dashboard_customize_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/dashboard_customize_icon_dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/delete_dark_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/delete_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/done_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/done_icon_dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/edit_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/edit_icon_dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/link_dark_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/link_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/lock_dark_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/lock_open_dark_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/lock_open_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/lock_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/settings_dark_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/settings_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/write_data.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/write_data_dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/influxdata-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/influxdata-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/influxdata-logo.png -------------------------------------------------------------------------------- /assets/images/new-device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/new-device.png -------------------------------------------------------------------------------- /assets/images/settings-dashboards-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/settings-dashboards-add.png -------------------------------------------------------------------------------- /assets/images/settings-dashboards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/settings-dashboards.png -------------------------------------------------------------------------------- /assets/images/settings-influx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/settings-influx.png -------------------------------------------------------------------------------- /assets/images/settings-sensors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/assets/images/settings-sensors.png -------------------------------------------------------------------------------- /config/data/dynamic/demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./schema.json", 3 | "cells": [ 4 | { 5 | "type": "svg", 6 | "file": "factory", 7 | "layout": { 8 | "x": 0, 9 | "y": 0, 10 | "w": 12, 11 | "h": 5 12 | }, 13 | "field": [ 14 | "Temperature", 15 | "Pressure", 16 | "TVOC", 17 | "CO2", 18 | "Humidity", 19 | "Lat", 20 | "Lon" 21 | ] 22 | }, 23 | { 24 | "type": "plot", 25 | "plotType": "gauge", 26 | "range": { 27 | "min": -10, 28 | "max": 50 29 | }, 30 | "field": "Temperature", 31 | "label": "Temperature", 32 | "unit": "°C", 33 | "decimalPlaces": 1, 34 | "layout": { 35 | "x": 0, 36 | "y": 5, 37 | "w": 4, 38 | "h": 2 39 | } 40 | }, 41 | { 42 | "type": "plot", 43 | "plotType": "gauge", 44 | "range": { 45 | "min": 0, 46 | "max": 100 47 | }, 48 | "field": "Humidity", 49 | "label": "Humidity", 50 | "unit": "%", 51 | "decimalPlaces": 2, 52 | "layout": { 53 | "x": 4, 54 | "y": 5, 55 | "w": 4, 56 | "h": 2 57 | } 58 | }, 59 | { 60 | "type": "plot", 61 | "plotType": "gauge", 62 | "range": { 63 | "min": 800, 64 | "max": 1100 65 | }, 66 | "field": "Pressure", 67 | "label": "Pressure", 68 | "unit": "hPa", 69 | "decimalPlaces": 2, 70 | "layout": { 71 | "x": 8, 72 | "y": 5, 73 | "w": 4, 74 | "h": 2 75 | } 76 | }, 77 | { 78 | "type": "plot", 79 | "plotType": "gauge", 80 | "range": { 81 | "min": 300, 82 | "max": 3500 83 | }, 84 | "field": "CO2", 85 | "label": "CO2", 86 | "unit": "ppm", 87 | "decimalPlaces": 2, 88 | "layout": { 89 | "x": 0, 90 | "y": 7, 91 | "w": 6, 92 | "h": 3 93 | } 94 | }, 95 | { 96 | "type": "plot", 97 | "plotType": "gauge", 98 | "range": { 99 | "min": 200, 100 | "max": 2200 101 | }, 102 | "field": "TVOC", 103 | "label": "TVOC", 104 | "unit": "", 105 | "decimalPlaces": 2, 106 | "layout": { 107 | "x": 6, 108 | "y": 7, 109 | "w": 6, 110 | "h": 3 111 | } 112 | }, 113 | { 114 | "type": "geo", 115 | "latField": "Lat", 116 | "lonField": "Lon", 117 | "Live": {}, 118 | "Past": {}, 119 | "layout": { 120 | "x": 0, 121 | "y": 10, 122 | "w": 12, 123 | "h": 3 124 | } 125 | }, 126 | { 127 | "type": "plot", 128 | "plotType": "line", 129 | "field": [ 130 | "CO2", 131 | "TVOC" 132 | ], 133 | "label": "CO2 and TVOC", 134 | "layout": { 135 | "x": 0, 136 | "y": 13, 137 | "w": 12, 138 | "h": 3 139 | } 140 | }, 141 | { 142 | "type": "plot", 143 | "plotType": "line", 144 | "field": [ 145 | "Temperature" 146 | ], 147 | "label": "Temperature", 148 | "layout": { 149 | "x": 0, 150 | "y": 16, 151 | "w": 12, 152 | "h": 3 153 | } 154 | }, 155 | { 156 | "type": "plot", 157 | "plotType": "line", 158 | "field": "Humidity", 159 | "label": "Humidity", 160 | "layout": { 161 | "x": 0, 162 | "y": 19, 163 | "w": 12, 164 | "h": 3 165 | } 166 | }, 167 | { 168 | "type": "plot", 169 | "plotType": "line", 170 | "field": "Pressure", 171 | "label": "Pressure", 172 | "layout": { 173 | "x": 0, 174 | "y": 22, 175 | "w": 12, 176 | "h": 3 177 | } 178 | }, 179 | { 180 | "type": "plot", 181 | "plotType": "line", 182 | "field": "CO2", 183 | "label": "CO2", 184 | "layout": { 185 | "x": 0, 186 | "y": 25, 187 | "w": 12, 188 | "h": 3 189 | } 190 | }, 191 | { 192 | "type": "plot", 193 | "plotType": "line", 194 | "field": "TVOC", 195 | "label": "TVOC", 196 | "layout": { 197 | "x": 0, 198 | "y": 28, 199 | "w": 12, 200 | "h": 3 201 | } 202 | } 203 | ] 204 | } -------------------------------------------------------------------------------- /config/data/dynamic/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "title": "Dynamic dashboard definition", 4 | "type": "object", 5 | "properties": { 6 | "cells": { 7 | "type": "array", 8 | "items": { 9 | "allOf": [ 10 | { 11 | "anyOf": [ 12 | { 13 | "type": "object", 14 | "required": [ 15 | "type", 16 | "field", 17 | "file" 18 | ], 19 | "properties": { 20 | "type": { 21 | "const": "svg" 22 | }, 23 | "field": { 24 | "anyOf": [ 25 | { 26 | "type": "array", 27 | "items": { 28 | "type": "string" 29 | } 30 | }, 31 | { 32 | "type": "string" 33 | } 34 | ] 35 | }, 36 | "file": { 37 | "type": "string" 38 | } 39 | } 40 | }, 41 | { 42 | "type": "object", 43 | "required": [ 44 | "type", 45 | "latField", 46 | "lonField" 47 | ], 48 | "properties": { 49 | "type": { 50 | "const": "geo" 51 | }, 52 | "latField": { 53 | "type": "string" 54 | }, 55 | "lonField": { 56 | "type": "string" 57 | } 58 | } 59 | }, 60 | { 61 | "allOf": [ 62 | { 63 | "type": "object", 64 | "required": [ 65 | "type", 66 | "field", 67 | "label" 68 | ], 69 | "properties": { 70 | "type": { 71 | "const": "plot" 72 | }, 73 | "field": { 74 | "anyOf": [ 75 | { 76 | "type": "array", 77 | "items": { 78 | "type": "string" 79 | } 80 | }, 81 | { 82 | "type": "string" 83 | } 84 | ] 85 | }, 86 | "label": { 87 | "type": "string" 88 | } 89 | } 90 | }, 91 | { 92 | "anyOf": [ 93 | { 94 | "type": "object", 95 | "required": [ 96 | "plotType" 97 | ], 98 | "properties": { 99 | "plotType": { 100 | "const": "line" 101 | } 102 | } 103 | }, 104 | { 105 | "type": "object", 106 | "required": [ 107 | "plotType", 108 | "range", 109 | "unit", 110 | "decimalPlaces" 111 | ], 112 | "properties": { 113 | "plotType": { 114 | "const": "gauge" 115 | }, 116 | "range": { 117 | "type": "object", 118 | "properties": { 119 | "min": { 120 | "type": "number" 121 | }, 122 | "max": { 123 | "type": "number" 124 | } 125 | } 126 | }, 127 | "unit": { 128 | "type": "string" 129 | }, 130 | "decimalPlaces": { 131 | "type": "integer" 132 | } 133 | } 134 | } 135 | ] 136 | } 137 | ] 138 | } 139 | ] 140 | }, 141 | { 142 | "type": "object", 143 | "required": [ 144 | "layout" 145 | ], 146 | "properties": { 147 | "layout": { 148 | "type": "object", 149 | "required": [ 150 | "x", 151 | "y", 152 | "w", 153 | "h" 154 | ], 155 | "properties": { 156 | "x": { 157 | "type": "integer", 158 | "minimum": 0 159 | }, 160 | "y": { 161 | "type": "number", 162 | "minimum": 0 163 | }, 164 | "w": { 165 | "type": "number", 166 | "minimum": 0 167 | }, 168 | "h": { 169 | "type": "number", 170 | "minimum": 0 171 | } 172 | } 173 | } 174 | } 175 | } 176 | ] 177 | } 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /config/mosquitto.conf: -------------------------------------------------------------------------------- 1 | allow_anonymous true 2 | listener 1883 3 | persistence false 4 | persistence_location /mosquitto/data/ 5 | socket_domain ipv4 -------------------------------------------------------------------------------- /config/telegraf.conf: -------------------------------------------------------------------------------- 1 | [agent] 2 | interval = "10s" 3 | round_interval = true 4 | metric_batch_size = 1000 5 | metric_buffer_limit = 10000 6 | collection_jitter = "0s" 7 | flush_interval = "10s" 8 | flush_jitter = "0s" 9 | precision = "" 10 | 11 | [[outputs.influxdb_v2]] 12 | urls = ["http://influxdb_v2:8086"] 13 | token = "my-token" 14 | organization = "my-org" 15 | bucket = "iot_center" 16 | 17 | [[inputs.mqtt_consumer]] 18 | servers = ["mqtt://mosquitto:1883"] 19 | topics = [ 20 | "iot_center", 21 | ] 22 | data_format = "influx" -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | influxdb_v2: 4 | image: influxdb:latest 5 | ports: 6 | - "8086:8086" 7 | environment: 8 | - INFLUXD_HTTP_BIND_ADDRESS=:8086 9 | - DOCKER_INFLUXDB_INIT_MODE=setup 10 | - DOCKER_INFLUXDB_INIT_USERNAME=my-user 11 | - DOCKER_INFLUXDB_INIT_PASSWORD=my-password 12 | - DOCKER_INFLUXDB_INIT_ORG=my-org 13 | - DOCKER_INFLUXDB_INIT_BUCKET=iot_center 14 | - DOCKER_INFLUXDB_INIT_RETENTION=30d 15 | - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-token 16 | 17 | command: influxd --reporting-disabled 18 | 19 | mosquitto: 20 | image: eclipse-mosquitto:2.0.10 21 | ports: 22 | - "1883:1883" 23 | volumes: 24 | # - $PWD/mosquitto:/mosquitto/ 25 | - $PWD/config/mosquitto.conf:/mosquitto/config/mosquitto.conf 26 | 27 | telegraf: 28 | image: telegraf:latest 29 | volumes: 30 | - $PWD/config/telegraf.conf:/etc/telegraf/telegraf.conf 31 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - battery_plus (1.0.0): 3 | - Flutter 4 | - environment_sensors (0.0.1): 5 | - Flutter 6 | - Flutter (1.0.0) 7 | - geolocator_apple (1.2.0): 8 | - Flutter 9 | - sensors_plus (0.0.1): 10 | - Flutter 11 | - shared_preferences_ios (0.0.1): 12 | - Flutter 13 | 14 | DEPENDENCIES: 15 | - battery_plus (from `.symlinks/plugins/battery_plus/ios`) 16 | - environment_sensors (from `.symlinks/plugins/environment_sensors/ios`) 17 | - Flutter (from `Flutter`) 18 | - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) 19 | - sensors_plus (from `.symlinks/plugins/sensors_plus/ios`) 20 | - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) 21 | 22 | EXTERNAL SOURCES: 23 | battery_plus: 24 | :path: ".symlinks/plugins/battery_plus/ios" 25 | environment_sensors: 26 | :path: ".symlinks/plugins/environment_sensors/ios" 27 | Flutter: 28 | :path: Flutter 29 | geolocator_apple: 30 | :path: ".symlinks/plugins/geolocator_apple/ios" 31 | sensors_plus: 32 | :path: ".symlinks/plugins/sensors_plus/ios" 33 | shared_preferences_ios: 34 | :path: ".symlinks/plugins/shared_preferences_ios/ios" 35 | 36 | SPEC CHECKSUMS: 37 | battery_plus: 7851ab482c336dd101ae735dc9363f01e1223c7c 38 | environment_sensors: 46802526e2f9e5b84722e0327d6391168b7ad74f 39 | Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a 40 | geolocator_apple: cc556e6844d508c95df1e87e3ea6fa4e58c50401 41 | sensors_plus: 5717760720f7e6acd96fdbd75b7428f5ad755ec2 42 | shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad 43 | 44 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c 45 | 46 | COCOAPODS: 1.11.2 47 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influxdata/iot-center-flutter/d59aedbaf52d13da4d22c98f9ca20ca312ae0257/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Iot Center Flutter Mvc 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | iot_center_flutter_mvc 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | NSLocationWhenInUseUsageDescription 47 | This app allow log device geolocation into influxdb 48 | NSLocationAlwaysUsageDescription 49 | This app allow log device geolocation into influxdb in the background. 50 | CADisableMinimumFrameDurationOnPhone 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:iot_center_flutter_mvc/src/view.dart'; 3 | 4 | 5 | void main() => runApp(const MyApp(key: Key('MyApp'))); 6 | -------------------------------------------------------------------------------- /lib/src/app/controller/app_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:iot_center_flutter_mvc/src/controller.dart'; 3 | import 'package:iot_center_flutter_mvc/src/view.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | const logoTime = const Duration(seconds: 2); 7 | 8 | final String platformStr = defaultTargetPlatform == TargetPlatform.android 9 | ? "android" 10 | : defaultTargetPlatform == TargetPlatform.iOS 11 | ? "ios" 12 | : "flutter"; 13 | 14 | class AppController extends ControllerMVC { 15 | factory AppController() => _this ??= AppController._(); 16 | AppController._(); 17 | static AppController? _this; 18 | 19 | final sensorsSubscriptionManager = SensorsSubscriptionManager(); 20 | late final List sensors; 21 | 22 | String clientId = ""; 23 | Future saveClientId() async { 24 | var prefs = await SharedPreferences.getInstance(); 25 | 26 | prefs.setString("clientId", clientId); 27 | } 28 | 29 | Future loadClientId() async { 30 | var prefs = await SharedPreferences.getInstance(); 31 | 32 | if (prefs.containsKey("clientId")) { 33 | clientId = prefs.getString("clientId")!; 34 | } else { 35 | clientId = "$platformStr-" + 36 | DateTime.now().millisecondsSinceEpoch.toRadixString(36).substring(3); 37 | await saveClientId(); 38 | } 39 | } 40 | 41 | Future initSensors() async { 42 | sensors = await Sensors().sensors; 43 | } 44 | 45 | @override 46 | Future initAsync() async { 47 | try { 48 | await Future.wait([ 49 | Future.delayed(logoTime, () {}), 50 | initSensors(), 51 | loadClientId() 52 | ]).timeout(const Duration(seconds: 5)); 53 | } catch (e) { 54 | // TODO: escalation 55 | } 56 | return true; 57 | } 58 | 59 | @override 60 | bool onAsyncError(FlutterErrorDetails details) { 61 | return false; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/app/controller/sensors.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | 4 | import 'package:battery_plus/battery_plus.dart'; 5 | import 'package:environment_sensors/environment_sensors.dart'; 6 | import 'package:geolocator/geolocator.dart'; 7 | import 'package:sensors_plus/sensors_plus.dart'; 8 | 9 | /// Sensors can return multiple subfields. 10 | /// 11 | /// For example sensor can return {"x": 10, "y": 20 ...} 12 | /// 13 | /// If sensor returns only one value 14 | /// then there will be only one entry with "" key 15 | typedef SensorMeasurement = Map; 16 | 17 | /// Used by SensorInfo to ask user for permission to use sensor 18 | typedef PermissionRequester = Future?> Function(); 19 | 20 | class SensorInfo { 21 | /// sensor or measurement name 22 | String name; 23 | Stream? stream; 24 | 25 | bool get availeble { 26 | return (stream != null); 27 | } 28 | 29 | /// if sensor needs ask user for permission, this function is set 30 | PermissionRequester? _permissionRequester; 31 | 32 | Future Function()? get requestPermission { 33 | if (_permissionRequester == null) return null; 34 | return (() async { 35 | stream = await _permissionRequester!(); 36 | _permissionRequester = null; 37 | }); 38 | } 39 | 40 | SensorInfo(this.name, 41 | {this.stream, PermissionRequester? permissionRequester}) { 42 | _permissionRequester = permissionRequester; 43 | } 44 | } 45 | 46 | class Sensors { 47 | final _es = EnvironmentSensors(); 48 | 49 | Stream get _accelerometer => 50 | SensorsPlatform.instance.accelerometerEvents 51 | .map((event) => {"x": event.x, "y": event.y, "z": event.z}); 52 | 53 | Stream get _userAccelerometer => 54 | SensorsPlatform.instance.userAccelerometerEvents 55 | .map((event) => {"x": event.x, "y": event.y, "z": event.z}); 56 | 57 | Stream get _gyroscope => 58 | SensorsPlatform.instance.gyroscopeEvents 59 | .map((event) => {"x": event.x, "y": event.y, "z": event.z}); 60 | 61 | Stream get _magnetometer => 62 | SensorsPlatform.instance.magnetometerEvents 63 | .map((event) => {"x": event.x, "y": event.y, "z": event.z}); 64 | 65 | Stream get _battery async* { 66 | final battery = Battery(); 67 | final SensorMeasurement batteryLastState = {}; 68 | bool changed = true; 69 | 70 | setField(String name, double value) { 71 | if (batteryLastState[name] != value) { 72 | changed = true; 73 | batteryLastState[name] = value; 74 | } 75 | } 76 | 77 | await for (var _ in Stream.periodic(const Duration(seconds: 1))) { 78 | final level = (await battery.batteryLevel).toDouble(); 79 | setField("level", level); 80 | 81 | final state = (await battery.batteryState); 82 | if (state != BatteryState.unknown) { 83 | setField("charging", state == BatteryState.charging ? 1 : 0); 84 | } 85 | 86 | if (changed) { 87 | changed = false; 88 | yield Map.from(batteryLastState); 89 | } 90 | } 91 | } 92 | 93 | Future?> get _temperature async => 94 | (await _es.getSensorAvailable(SensorType.AmbientTemperature)) 95 | ? EnvironmentSensors().temperature.map((x) => {"": x}) 96 | : null; 97 | 98 | Future?> get _humidity async => 99 | (await _es.getSensorAvailable(SensorType.Humidity)) 100 | ? EnvironmentSensors().humidity.map((x) => {"": x}) 101 | : null; 102 | 103 | Future?> get _light async => 104 | (await _es.getSensorAvailable(SensorType.Light)) 105 | ? EnvironmentSensors().light.map((x) => {"": x}) 106 | : null; 107 | 108 | Future?> get _pressure async => 109 | (await _es.getSensorAvailable(SensorType.Pressure)) 110 | ? EnvironmentSensors().pressure.map((x) => {"": x}) 111 | : null; 112 | 113 | Future?> get _geo async { 114 | if (!await Geolocator.isLocationServiceEnabled()) return null; 115 | final permission = await Geolocator.checkPermission(); 116 | return (permission == LocationPermission.always || 117 | permission == LocationPermission.whileInUse) 118 | ? Geolocator.getPositionStream().map((pos) { 119 | return { 120 | "lat": pos.latitude, 121 | "lon": pos.longitude, 122 | "acc": pos.accuracy 123 | }; 124 | }) 125 | : null; 126 | } 127 | 128 | Future get _geoRequester async { 129 | final permission = await Geolocator.checkPermission(); 130 | if (permission == LocationPermission.deniedForever || 131 | permission == LocationPermission.always || 132 | permission == LocationPermission.whileInUse) return null; 133 | 134 | return () async { 135 | await Geolocator.requestPermission(); 136 | return await _geo; 137 | }; 138 | } 139 | 140 | Future> get sensors async => [ 141 | SensorInfo("Accelerometer", stream: _accelerometer), 142 | SensorInfo("UserAccelerometer", stream: _userAccelerometer), 143 | SensorInfo("Gyroscope", stream: _gyroscope), 144 | SensorInfo("Magnetometer", stream: _magnetometer), 145 | SensorInfo("Battery", stream: _battery.asBroadcastStream()), 146 | SensorInfo("Temperature", stream: await _temperature), 147 | SensorInfo("Humidity", stream: await _humidity), 148 | SensorInfo("Light", stream: await _light), 149 | SensorInfo("Pressure", stream: await _pressure), 150 | SensorInfo("Geo", 151 | stream: await _geo, permissionRequester: await _geoRequester), 152 | ]; 153 | } 154 | 155 | class SensorsSubscriptionManager { 156 | final Map>> subscriptions = 157 | {}; 158 | 159 | final Map _lastValues = {}; 160 | DateTime? _lastDataRead; 161 | 162 | DateTime? get lastDataRead => _lastDataRead; 163 | 164 | /// Returns function that adds sensorname into SensorMeasurement 165 | static SensorMeasurement addNameToMeasure( 166 | SensorInfo sensor, SensorMeasurement measurement) => 167 | measurement.map((key, value) { 168 | final name = sensor.name + (key != "" ? "_$key" : ""); 169 | return MapEntry(name, value); 170 | }); 171 | 172 | SensorMeasurement lastValueOf(SensorInfo sensor) => 173 | _lastValues[sensor.name] ?? {}; 174 | 175 | bool isSubscribed(SensorInfo sensor) => subscriptions.containsKey(sensor); 176 | 177 | void subscribe(SensorInfo sensor, 178 | void Function(SensorMeasurement, SensorInfo) callback) { 179 | if (subscriptions[sensor] != null) unsubscribe(sensor); 180 | final stream = sensor.stream; 181 | if (stream == null) { 182 | log("sensor ${sensor.name} is not subsciable", level: 900); 183 | return; 184 | } 185 | 186 | subscriptions[sensor] = stream.listen((metrics) { 187 | _lastDataRead = DateTime.now(); 188 | _lastValues[sensor.name] = metrics; 189 | callback(metrics, sensor); 190 | }); 191 | } 192 | 193 | Future trySubscribe(SensorInfo sensor, 194 | void Function(SensorMeasurement, SensorInfo) callback) async { 195 | if (!sensor.availeble && sensor.requestPermission != null) { 196 | await sensor.requestPermission!(); 197 | } 198 | if (sensor.availeble) { 199 | subscribe(sensor, callback); 200 | } 201 | return sensor.availeble; 202 | } 203 | 204 | void unsubscribe(SensorInfo sensor) { 205 | final subscriptionHandler = subscriptions[sensor]; 206 | if (subscriptionHandler == null) return; 207 | subscriptionHandler.cancel(); 208 | subscriptions.remove(sensor); 209 | _lastValues.remove(sensor.name); 210 | } 211 | 212 | void unsubscribeAll() { 213 | subscriptions.keys.toList().forEach(unsubscribe); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /lib/src/app/model/device_config.dart: -------------------------------------------------------------------------------- 1 | class DeviceConfig { 2 | String influxUrl = ''; 3 | String influxOrg = ''; 4 | String influxToken = ''; 5 | String influxBucket = ''; 6 | String createdAt = ''; 7 | String id = ''; 8 | String type = ''; 9 | 10 | DeviceConfig(); 11 | 12 | DeviceConfig.withParams(this.id, this.createdAt, this.influxOrg, 13 | this.influxUrl, this.influxBucket, this.influxToken, this.type); 14 | 15 | DeviceConfig.fromJson(Map json) { 16 | influxUrl = json['influx_url']; 17 | influxOrg = json['influx_org']; 18 | influxToken = json['influx_token']; 19 | influxBucket = json['influx_bucket']; 20 | createdAt = json['createdAt']; 21 | id = json['id']; 22 | } 23 | 24 | void fromJson(Map json) { 25 | influxUrl = json['influx_url']; 26 | influxOrg = json['influx_org']; 27 | influxToken = json['influx_token']; 28 | influxBucket = json['influx_bucket']; 29 | createdAt = json['createdAt']; 30 | id = json['id']; 31 | } 32 | 33 | Map toJson() => { 34 | 'influx_url': influxUrl, 35 | 'influx_org': influxOrg, 36 | 'influx_token': influxToken, 37 | 'influx_bucket': influxBucket, 38 | 'createdAt': createdAt, 39 | 'id': id, 40 | }; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /lib/src/app/model/influx_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:influxdb_client/api.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | import 'dart:developer' as developer; 6 | 7 | extension InfluxClient on InfluxDBClient { 8 | InfluxDBClient clone() { 9 | return InfluxDBClient( 10 | url: url, token: token, bucket: bucket, debug: debug, org: org); 11 | } 12 | 13 | void _fromJson(Map json) { 14 | url = json['url']; 15 | org = json['org']; 16 | token = json['token']; 17 | bucket = json['bucket']; 18 | } 19 | 20 | Map toJson() => { 21 | 'url': url, 22 | 'org': org, 23 | 'token': token, 24 | 'bucket': bucket, 25 | }; 26 | 27 | Future loadInfluxClient() async { 28 | try { 29 | var prefs = await SharedPreferences.getInstance(); 30 | if (prefs.containsKey("influxClient")) { 31 | _fromJson(json.decode(prefs.getString("influxClient")!)); 32 | } 33 | return this; 34 | } catch (e) { 35 | developer.log('Failed to load Influx Client' + e.toString()); 36 | throw Exception('Failed to load Influx Client from Shared Preferences.'); 37 | } 38 | } 39 | 40 | Future saveInfluxClient() async { 41 | try { 42 | SharedPreferences prefs = await SharedPreferences.getInstance(); 43 | prefs.setString("influxClient", jsonEncode(this)); 44 | } catch (e) { 45 | developer.log('Failed to save Influx Client' + e.toString()); 46 | throw Exception('Failed to save Influx Client to Shared Preferences.'); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/app/view/common/drop_down_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:iot_center_flutter_mvc/src/view.dart'; 3 | 4 | class DropDownItem { 5 | DropDownItem({required this.label, required this.value}); 6 | 7 | String label; 8 | String value; 9 | } 10 | 11 | class MyDropDown extends StatefulWidget { 12 | const MyDropDown( 13 | {this.padding = EdgeInsets.zero, 14 | this.hint = '', 15 | this.value, 16 | required this.items, 17 | this.onChanged, 18 | this.onSaved, 19 | this.addIfMissing, 20 | Key? key}) 21 | : super(key: key); 22 | 23 | final EdgeInsets padding; 24 | final String hint; 25 | final String? value; 26 | final List items; 27 | final Function(String?)? onChanged; 28 | final Function(String?)? onSaved; 29 | 30 | /// If current value is missing in items, then it's added so it won't fail 31 | final bool? addIfMissing; 32 | 33 | @override 34 | State createState() { 35 | return _MyDropDown(); 36 | } 37 | } 38 | 39 | class _MyDropDown extends State { 40 | @override 41 | void initState() { 42 | super.initState(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | var val = widget.value ?? ""; 48 | if (widget.items.isNotEmpty && val.isEmpty) { 49 | val = widget.items.first.value.toString(); 50 | } 51 | 52 | final List> items = 53 | widget.items 54 | .toSet() 55 | .map((DropDownItem map) { 56 | return DropdownMenuItem( 57 | value: map.value.toString(), 58 | child: Text( 59 | map.label, 60 | style: const TextStyle(fontSize: 16), 61 | )); 62 | }).toList(); 63 | 64 | if (widget.addIfMissing == true && 65 | items.where((element) => element.value == val).isEmpty) { 66 | items.add(DropdownMenuItem( 67 | value: val, 68 | child: Text( 69 | val, 70 | style: const TextStyle(fontSize: 16), 71 | ))); 72 | } else if (items.where((element) => element.value == val).isEmpty && 73 | widget.items.isNotEmpty) { 74 | val = widget.items.first.value.toString(); 75 | } 76 | 77 | var dropDown = DropdownButtonFormField( 78 | isExpanded: true, 79 | hint: Text(widget.hint), 80 | decoration: InputDecoration( 81 | border: OutlineInputBorder( 82 | borderSide: BorderSide.none, 83 | borderRadius: BorderRadius.circular(5), 84 | gapPadding: 0, 85 | ), 86 | focusedBorder: OutlineInputBorder( 87 | borderSide: BorderSide.none, 88 | borderRadius: BorderRadius.circular(5), 89 | gapPadding: 0, 90 | ), 91 | filled: true, 92 | fillColor: Colors.white, 93 | ), 94 | value: val, 95 | items: items, 96 | onChanged: widget.onChanged, 97 | onSaved: widget.onSaved, 98 | ); 99 | 100 | return widget.padding == EdgeInsets.zero 101 | ? dropDown 102 | : Padding( 103 | padding: widget.padding, 104 | child: dropDown, 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/app/view/common/form_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/view.dart'; 2 | 3 | class FormButton extends StatelessWidget { 4 | const FormButton({required this.label, this.onPressed, Key? key}) : super(key: key); 5 | 6 | final void Function()? onPressed; 7 | final String? label; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Container( 12 | decoration: BoxDecoration( 13 | boxShadow: const [ 14 | BoxShadow(color: Color.fromRGBO(201, 201, 201, 1.0), blurRadius: 4.0) 15 | ], 16 | gradient: pinkPurpleGradient, 17 | borderRadius: BorderRadius.circular(6), 18 | ), 19 | child: ElevatedButton( 20 | style: ButtonStyle( 21 | padding: MaterialStateProperty.all(const EdgeInsets.all(20)), 22 | backgroundColor: MaterialStateProperty.all(Colors.transparent), 23 | shadowColor: MaterialStateProperty.all(Colors.transparent), 24 | textStyle: MaterialStateProperty.all( 25 | const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), 26 | ), 27 | onPressed: onPressed, 28 | child: Text(label!), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/app/view/common/form_row.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:iot_center_flutter_mvc/src/view.dart'; 3 | 4 | class TextBoxRow extends FormRow { 5 | TextBoxRow( 6 | {Key? key, 7 | padding = const EdgeInsets.all(5), 8 | label, 9 | this.hint = '', 10 | this.inputFormatters = const [], 11 | this.readOnly = false, 12 | this.controller, 13 | this.validator, 14 | this.onChanged, 15 | this.onSaved}) 16 | : super( 17 | key: key, 18 | label: label, 19 | inputWidget: Container( 20 | decoration: boxDecor, 21 | child: TextFormField( 22 | style: TextStyle( 23 | color: readOnly! ? Colors.black54 : Colors.black, 24 | ), 25 | readOnly: readOnly, 26 | inputFormatters: inputFormatters, 27 | decoration: InputDecoration( 28 | border: OutlineInputBorder( 29 | borderSide: BorderSide.none, 30 | borderRadius: BorderRadius.circular(5), 31 | ), 32 | fillColor: readOnly ? lightGrey : Colors.white, 33 | filled: true, 34 | hintText: hint, 35 | ), 36 | controller: controller ?? TextEditingController(), 37 | validator: validator, 38 | onChanged: onChanged, 39 | onSaved: onSaved, 40 | )), 41 | ); 42 | 43 | final String? hint; 44 | final TextEditingController? controller; 45 | final List inputFormatters; 46 | final Function(String?)? onChanged; 47 | final Function(String?)? onSaved; 48 | final String? Function(String?)? validator; 49 | final bool? readOnly; 50 | } 51 | 52 | class NumberBoxRow extends FormRow { 53 | NumberBoxRow( 54 | {Key? key, 55 | padding = const EdgeInsets.all(5), 56 | label, 57 | this.hint = '', 58 | this.min = -99999, 59 | this.max = 99999, 60 | this.controller, 61 | this.onChanged, 62 | this.onSaved}) 63 | : super( 64 | key: key, 65 | label: label, 66 | inputWidget: Container( 67 | decoration: boxDecor, 68 | child: NumberTextField( 69 | onSaved: onSaved, 70 | min: min, 71 | max: max, 72 | controller: controller ?? TextEditingController(), 73 | ), 74 | ), 75 | ); 76 | 77 | final String? hint; 78 | final TextEditingController? controller; 79 | final int min; 80 | final int max; 81 | 82 | final Function(String?)? onChanged; 83 | final Function(String?)? onSaved; 84 | } 85 | 86 | class DropDownListRow extends FormRow { 87 | DropDownListRow( 88 | {Key? key, 89 | padding = const EdgeInsets.all(5), 90 | label, 91 | this.hint = '', 92 | required this.items, 93 | this.value, 94 | this.onChanged, 95 | this.onSaved, 96 | bool? addIfMissing}) 97 | : super( 98 | key: key, 99 | label: label, 100 | inputWidget: Container( 101 | decoration: boxDecor, 102 | child: MyDropDown( 103 | value: value, 104 | items: items!, 105 | onChanged: onChanged, 106 | onSaved: onSaved, 107 | addIfMissing: addIfMissing, 108 | )), 109 | ); 110 | 111 | final String? hint; 112 | final List? items; 113 | final String? value; 114 | final Function(String?)? onChanged; 115 | final Function(String?)? onSaved; 116 | } 117 | 118 | class DoubleNumberBoxRow extends FormRow { 119 | DoubleNumberBoxRow({ 120 | Key? key, 121 | padding = const EdgeInsets.all(5), 122 | label, 123 | this.firstHint = '', 124 | this.firstController, 125 | this.firstMin = -99999, 126 | this.firstMax = 99999, 127 | this.firstOnChanged, 128 | this.firstOnSaved, 129 | this.secondHint = '', 130 | this.secondController, 131 | this.secondMin = -99999, 132 | this.secondMax = 99999, 133 | this.secondOnChanged, 134 | this.secondOnSaved, 135 | }) : super( 136 | key: key, 137 | label: label, 138 | inputWidget: Row( 139 | children: [ 140 | Expanded( 141 | child: Padding( 142 | padding: const EdgeInsets.only(right: 4), 143 | child: Container( 144 | decoration: boxDecor, 145 | child: NumberTextField( 146 | onSaved: firstOnSaved, 147 | controller: firstController ?? TextEditingController(), 148 | min: firstMin, 149 | max: firstMax, 150 | ), 151 | )), 152 | ), 153 | Expanded( 154 | child: Padding( 155 | padding: const EdgeInsets.only(left: 4), 156 | child: Container( 157 | decoration: boxDecor, 158 | child: NumberTextField( 159 | onSaved: secondOnSaved, 160 | controller: secondController ?? TextEditingController(), 161 | min: secondMin, 162 | max: secondMax, 163 | ), 164 | )), 165 | ) 166 | ], 167 | ), 168 | ); 169 | 170 | final String? firstHint; 171 | final TextEditingController? firstController; 172 | final int firstMin; 173 | final int firstMax; 174 | final Function(String?)? firstOnChanged; 175 | final Function(String?)? firstOnSaved; 176 | 177 | final String? secondHint; 178 | final TextEditingController? secondController; 179 | final int secondMin; 180 | final int secondMax; 181 | final Function(String?)? secondOnChanged; 182 | final Function(String?)? secondOnSaved; 183 | } 184 | 185 | class FormRow extends StatefulWidget { 186 | const FormRow( 187 | {this.padding = const EdgeInsets.all(5), 188 | required this.label, 189 | required this.inputWidget, 190 | Key? key}) 191 | : super(key: key); 192 | 193 | final EdgeInsets padding; 194 | final String? label; 195 | final Widget inputWidget; 196 | 197 | @override 198 | State createState() { 199 | return _FormRowState(); 200 | } 201 | } 202 | 203 | class _FormRowState extends State { 204 | @override 205 | void initState() { 206 | super.initState(); 207 | } 208 | 209 | @override 210 | Widget build(BuildContext context) { 211 | return widget.label != null && widget.label!.isNotEmpty 212 | ? Padding( 213 | padding: widget.padding, 214 | child: Row( 215 | children: [ 216 | SizedBox( 217 | width: 110, 218 | child: Text( 219 | widget.label!, 220 | style: const TextStyle( 221 | fontSize: 15, 222 | fontWeight: FontWeight.w600, 223 | color: darkBlue, 224 | ), 225 | )), 226 | Expanded( 227 | child: SizedBox(child: widget.inputWidget), 228 | ), 229 | ], 230 | ), 231 | ) 232 | : Padding( 233 | padding: widget.padding, 234 | child: Row( 235 | children: [ 236 | Expanded( 237 | child: widget.inputWidget, 238 | ), 239 | ], 240 | ), 241 | ); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /lib/src/app/view/common/number_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:iot_center_flutter_mvc/src/view.dart'; 3 | 4 | class NumberTextField extends StatefulWidget { 5 | final TextEditingController? controller; 6 | final FocusNode? focusNode; 7 | final int min; 8 | final int max; 9 | final int step; 10 | final ValueChanged? onChanged; 11 | final Function(String?)? onSaved; 12 | 13 | const NumberTextField({ 14 | Key? key, 15 | this.controller, 16 | this.focusNode, 17 | this.min = -9999, 18 | this.max = 9999, 19 | this.step = 1, 20 | this.onChanged, 21 | this.onSaved, 22 | }) : super(key: key); 23 | 24 | @override 25 | State createState() => _NumberTextFieldState(); 26 | } 27 | 28 | class _NumberTextFieldState extends State { 29 | late TextEditingController _controller; 30 | late FocusNode _focusNode; 31 | bool _notMaxValue = false; 32 | bool _notMinValue = false; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | _controller = widget.controller ?? TextEditingController(); 38 | _focusNode = widget.focusNode ?? FocusNode(); 39 | _updateArrows(int.tryParse(_controller.text)); 40 | } 41 | 42 | @override 43 | void didUpdateWidget(covariant NumberTextField oldWidget) { 44 | super.didUpdateWidget(oldWidget); 45 | _controller = widget.controller ?? _controller; 46 | _focusNode = widget.focusNode ?? _focusNode; 47 | _updateArrows(int.tryParse(_controller.text)); 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) => TextFormField( 52 | onSaved: widget.onSaved, 53 | controller: _controller, 54 | focusNode: _focusNode, 55 | textInputAction: TextInputAction.done, 56 | keyboardType: TextInputType.number, 57 | maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0), 58 | decoration: InputDecoration( 59 | counterText: '', 60 | border: OutlineInputBorder( 61 | borderSide: BorderSide.none, 62 | borderRadius: BorderRadius.circular(5), 63 | ), 64 | fillColor: Colors.white, 65 | filled: true, 66 | suffixIconConstraints: const BoxConstraints( 67 | maxHeight: 45, maxWidth: kMinInteractiveDimension), 68 | suffixIcon: 69 | Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ 70 | Expanded( 71 | child: Material( 72 | type: MaterialType.transparency, 73 | child: InkWell( 74 | child: Opacity( 75 | opacity: _notMaxValue ? 1 : .4, 76 | child: const Icon( 77 | Icons.arrow_drop_up, 78 | color: darkGrey, 79 | )), 80 | onTap: _notMaxValue ? () => _update(true) : null))), 81 | Expanded( 82 | child: Material( 83 | type: MaterialType.transparency, 84 | child: InkWell( 85 | child: Opacity( 86 | opacity: _notMinValue ? 1 : .4, 87 | child: const Icon( 88 | Icons.arrow_drop_down, 89 | color: darkGrey, 90 | )), 91 | onTap: _notMinValue ? () => _update(false) : null))), 92 | ])), 93 | onChanged: (value) { 94 | final intValue = int.tryParse(value); 95 | widget.onChanged?.call(intValue); 96 | _updateArrows(intValue); 97 | }, 98 | inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)]); 99 | 100 | void _update(bool up) { 101 | var intValue = int.tryParse(_controller.text); 102 | intValue == null 103 | ? intValue = 0 104 | : intValue += up ? widget.step : -widget.step; 105 | _controller.text = intValue.toString(); 106 | _updateArrows(intValue); 107 | _focusNode.requestFocus(); 108 | } 109 | 110 | void _updateArrows(int? value) { 111 | final notMaxValue = value == null || value < widget.max; 112 | final notMinValue = value == null || value > widget.min; 113 | if (_notMaxValue != notMaxValue || _notMinValue != notMinValue) { 114 | setState(() { 115 | _notMaxValue = notMaxValue; 116 | _notMinValue = notMinValue; 117 | }); 118 | } 119 | } 120 | } 121 | 122 | class _NumberTextInputFormatter extends TextInputFormatter { 123 | final int min; 124 | final int max; 125 | 126 | _NumberTextInputFormatter(this.min, this.max); 127 | 128 | @override 129 | TextEditingValue formatEditUpdate( 130 | TextEditingValue oldValue, TextEditingValue newValue) { 131 | if (const ['-', ''].contains(newValue.text)) return newValue; 132 | final intValue = int.tryParse(newValue.text); 133 | if (intValue == null) return oldValue; 134 | if (intValue < min) return newValue.copyWith(text: min.toString()); 135 | if (intValue > max) return newValue.copyWith(text: max.toString()); 136 | return newValue.copyWith(text: intValue.toString()); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/src/app/view/common/styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/view.dart'; 2 | 3 | const Color darkBlue = Color.fromRGBO(2, 10, 71, 1); 4 | const Color turquoise = Color.fromRGBO(94, 228, 228, 1); 5 | const Color pink = Color.fromRGBO(211, 9, 113, 1); 6 | const Color purple = Color.fromRGBO(155, 42, 255, 1); 7 | const Color blue = Color.fromRGBO(147, 148, 255, 1); 8 | const Color lightGrey = Color.fromRGBO(250, 250, 250, 1); 9 | const Color darkGrey = Color.fromRGBO(86, 86, 86, 1.0); 10 | 11 | const LinearGradient pinkPurpleGradient = LinearGradient( 12 | begin: Alignment.topLeft, 13 | end: Alignment.bottomRight, 14 | stops: [ 15 | 0, 16 | 0.6, 17 | ], 18 | colors: [ 19 | pink, 20 | purple, 21 | ], 22 | ); 23 | 24 | const LinearGradient buttonGradient = LinearGradient( 25 | begin: Alignment.topCenter, 26 | end: Alignment.bottomCenter, 27 | colors: [ 28 | pink, 29 | purple, 30 | ], 31 | ); 32 | 33 | BoxDecoration boxDecor = BoxDecoration( 34 | color: Colors.white, 35 | borderRadius: BorderRadius.circular(5), 36 | boxShadow: const [ 37 | BoxShadow(color: Color.fromRGBO(201, 201, 201, 1.0), blurRadius: 4.0) 38 | ]); 39 | -------------------------------------------------------------------------------- /lib/src/app/view/my_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/home/view/home_page.dart'; 2 | import 'package:iot_center_flutter_mvc/src/view.dart'; 3 | import 'package:iot_center_flutter_mvc/src/controller.dart'; 4 | 5 | class MyApp extends AppStatefulWidgetMVC { 6 | const MyApp({Key? key}) : super(key: key); 7 | 8 | @override 9 | AppStateMVC createState() => _MyAppState(); 10 | } 11 | 12 | class _MyAppState extends AppStateMVC { 13 | factory _MyAppState() => _this ??= _MyAppState._(); 14 | 15 | _MyAppState._() 16 | : super( 17 | controller: AppController(), 18 | controllers: [ 19 | HomePageController(), 20 | ], 21 | ); 22 | static _MyAppState? _this; 23 | 24 | @override 25 | Widget buildApp(BuildContext context) => MaterialApp( 26 | theme: ThemeData(backgroundColor: const Color(0xFFF5F5F5)), 27 | home: FutureBuilder( 28 | future: initAsync(), 29 | builder: (context, snapshot) { 30 | if (snapshot.hasData) { 31 | if (snapshot.data!) { 32 | return HomePage(key: UniqueKey()); 33 | } else { 34 | return const Text('Failed to startup'); 35 | } 36 | } else if (snapshot.hasError) { 37 | return Text('${snapshot.error}'); 38 | } 39 | return Container( 40 | padding: const EdgeInsets.all(50), 41 | decoration: const BoxDecoration(gradient: pinkPurpleGradient), 42 | child: Container( 43 | decoration: const BoxDecoration( 44 | image: DecorationImage( 45 | image: AssetImage("assets/images/influxdata-logo.png"), 46 | fit: BoxFit.contain), 47 | ), 48 | // child: const Center( 49 | // child: CircularProgressIndicator( 50 | // color: Colors.white, 51 | // )) 52 | ), 53 | ); 54 | }), 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/controller.dart: -------------------------------------------------------------------------------- 1 | export 'package:iot_center_flutter_mvc/src/app/controller/app_controller.dart'; 2 | export 'package:iot_center_flutter_mvc/src/app/controller/sensors.dart'; 3 | 4 | export 'package:iot_center_flutter_mvc/src/home/controller/home_page_controller.dart'; 5 | 6 | export 'package:iot_center_flutter_mvc/src/device/controller/device_detail_controller.dart'; 7 | 8 | export 'package:iot_center_flutter_mvc/src/settings/controller/settings_controller.dart'; 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/device/controller/chart_detail_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/view.dart'; 2 | import 'package:iot_center_flutter_mvc/src/model.dart'; 3 | 4 | class ChartDetailController extends ControllerMVC { 5 | factory ChartDetailController([StateMVC? state]) => 6 | _this ??= ChartDetailController._(state); 7 | ChartDetailController._(StateMVC? state) 8 | : _dashboardController = DashboardController(), 9 | super(state); 10 | static ChartDetailController? _this; 11 | final DashboardController _dashboardController; 12 | 13 | bool isGauge = true; 14 | var chartType = ''; 15 | 16 | List chartTypeList = [ 17 | DropDownItem(label: 'Gauge chart', value: ChartType.gauge.toString()), 18 | DropDownItem(label: 'Simple chart', value: ChartType.simple.toString()), 19 | ]; 20 | get fieldNames => _dashboardController.fieldNames; 21 | 22 | Chart? _chart; 23 | Chart get chart => _chart!; 24 | 25 | set chart(Chart value) { 26 | setState(() { 27 | isGauge = value.data.chartType == ChartType.gauge; 28 | 29 | label.text = value.data.label; 30 | unit.text = value.data.unit; 31 | startValue.text = value.data.startValue.toStringAsFixed(0); 32 | endValue.text = value.data.endValue.toStringAsFixed(0); 33 | decimalPlaces.text = isGauge && value.data.decimalPlaces != null 34 | ? value.data.decimalPlaces!.toStringAsFixed(0) 35 | : '0'; 36 | 37 | _chart = value; 38 | }); 39 | } 40 | 41 | TextEditingController label = TextEditingController(); 42 | TextEditingController startValue = TextEditingController(); 43 | TextEditingController endValue = TextEditingController(); 44 | TextEditingController decimalPlaces = TextEditingController(); 45 | TextEditingController unit = TextEditingController(); 46 | 47 | showAlertDialog(BuildContext context, Chart chart) { 48 | // set up the buttons 49 | Widget cancelButton = TextButton( 50 | child: const Text("Cancel"), 51 | onPressed: () { 52 | Navigator.of(context).pop(); 53 | }, 54 | ); 55 | Widget continueButton = TextButton( 56 | child: const Text("Delete"), 57 | onPressed: () { 58 | _dashboardController.deleteChart(chart.row, chart.column); 59 | Navigator.of(context).pop(); 60 | Navigator.of(context).pop(); 61 | }, 62 | ); 63 | // set up the AlertDialog 64 | AlertDialog alert = AlertDialog( 65 | title: const Text("Delete chart"), 66 | actions: [ 67 | cancelButton, 68 | continueButton, 69 | ], 70 | ); 71 | // show the dialog 72 | showDialog( 73 | context: context, 74 | builder: (BuildContext context) { 75 | return alert; 76 | }, 77 | ); 78 | } 79 | 80 | void saveChart(bool newChart) { 81 | chart.data.label = label.text; 82 | chart.data.startValue = 83 | _dashboardController.isGauge ? double.parse(startValue.text) : 0; 84 | chart.data.endValue = 85 | _dashboardController.isGauge ? double.parse(endValue.text) : 0; 86 | 87 | chart.data.decimalPlaces = isGauge ? int.parse(decimalPlaces.text) : 0; 88 | chart.data.unit = unit.text; 89 | 90 | chart.data.chartType = 91 | chartType == 'ChartType.gauge' ? ChartType.gauge : ChartType.simple; 92 | 93 | _dashboardController.saveChart(chart, newChart); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/device/controller/dashboard_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:influxdb_client/api.dart'; 4 | import 'package:iot_center_flutter_mvc/src/view.dart'; 5 | import 'package:iot_center_flutter_mvc/src/model.dart'; 6 | 7 | class DashboardController extends ControllerMVC { 8 | factory DashboardController([StateMVC? state]) => 9 | _this ??= DashboardController._(state); 10 | DashboardController._(StateMVC? state) 11 | : _model = InfluxModel(), 12 | super(state); 13 | static DashboardController? _this; 14 | final InfluxModel _model; 15 | 16 | Device? selectedDevice; 17 | String selectedTimeOption = "-1h"; 18 | 19 | Future>? fieldNames; 20 | var isGauge = true; 21 | 22 | List timeOptionList = [ 23 | DropDownItem(label: 'Past 5m', value: '-5m'), 24 | DropDownItem(label: 'Past 15m', value: '-15m'), 25 | DropDownItem(label: 'Past 1h', value: '-1h'), 26 | DropDownItem(label: 'Past 6h', value: '-6h'), 27 | DropDownItem(label: 'Past 1d', value: '-1d'), 28 | DropDownItem(label: 'Past 3d', value: '-3d'), 29 | DropDownItem(label: 'Past 7d', value: '-7d'), 30 | DropDownItem(label: 'Past 30d', value: '-30d'), 31 | ]; 32 | 33 | @override 34 | void initState() { 35 | fieldNames = _model.fetchFieldNames(selectedDevice!.id); 36 | super.initState(); 37 | } 38 | 39 | bool _editable = false; 40 | bool get editable => _editable; 41 | set editable(bool value) { 42 | _editable = value; 43 | setState(() { 44 | editable; 45 | }); 46 | } 47 | 48 | int _rowCount = 0; 49 | int get rowCount => _rowCount; 50 | void setRowCount() { 51 | _rowCount = selectedDevice!.dashboard != null && 52 | selectedDevice!.dashboard!.isNotEmpty 53 | ? selectedDevice!.dashboard! 54 | .reduce((currentChart, nextChart) => 55 | currentChart.row > nextChart.row ? currentChart : nextChart) 56 | .row + 57 | 1 58 | : 0; 59 | setState(() { 60 | rowCount; 61 | }); 62 | } 63 | 64 | void refreshCharts() { 65 | for (var chart in selectedDevice!.dashboard!) { 66 | if (chart.data.reloadData != null) { 67 | chart.data.reloadData!(); 68 | setState(() { 69 | chart.data; 70 | }); 71 | } 72 | } 73 | } 74 | 75 | Widget buildChartListViewRow(index, BuildContext context) { 76 | var chartRow = 77 | selectedDevice!.dashboard!.where((e) => e.row == index).toList(); 78 | List chartWidgets = []; 79 | 80 | if (chartRow.isNotEmpty) { 81 | chartRow.sort(((a, b) => a.column.compareTo(b.column))); 82 | for (var chart in chartRow) { 83 | chartWidgets.add(getChartWidget(chart, context)); 84 | } 85 | } 86 | return Row( 87 | children: chartWidgets, 88 | ); 89 | } 90 | 91 | Widget getChartWidget(Chart chart, BuildContext context) { 92 | Widget chartWidget = chart.data.chartType == ChartType.gauge 93 | ? GaugeChart( 94 | chartData: chart.data, 95 | con: this, 96 | ) 97 | : SimpleChart( 98 | chartData: chart.data, 99 | con: this, 100 | ); 101 | 102 | return Expanded( 103 | child: Padding( 104 | padding: const EdgeInsets.all(5.0), 105 | child: Container( 106 | decoration: boxDecor, 107 | child: Padding( 108 | padding: const EdgeInsets.all(10), 109 | child: Column( 110 | children: [ 111 | Padding( 112 | padding: editable 113 | ? const EdgeInsets.only(bottom: 0) 114 | : const EdgeInsets.only(bottom: 15, top: 15), 115 | child: Row( 116 | children: [ 117 | Expanded( 118 | child: Text( 119 | chart.data.label, 120 | style: const TextStyle( 121 | fontSize: 15, 122 | fontWeight: FontWeight.w700, 123 | color: darkBlue, 124 | ), 125 | ), 126 | ), 127 | Visibility( 128 | visible: editable, 129 | child: IconButton( 130 | onPressed: () { 131 | Navigator.push( 132 | context, 133 | MaterialPageRoute( 134 | builder: (c) => ChartDetailPage( 135 | chart: chart, 136 | newChart: false, 137 | ), 138 | )); 139 | }, 140 | icon: const Icon(Icons.settings), 141 | iconSize: 17, 142 | color: darkBlue, 143 | ), 144 | ), 145 | ], 146 | ), 147 | ), 148 | chartWidget, 149 | ], 150 | ))))); 151 | } 152 | 153 | Future> getDataFromInflux( 154 | String measurement, bool median) async { 155 | return _model.fetchDeviceDataField( 156 | measurement, median, selectedDevice!, selectedTimeOption); 157 | } 158 | 159 | double getDouble(dynamic value) => 160 | value is String ? double.parse(value) : value.toDouble(); 161 | 162 | void deleteChart(int row, int column) { 163 | selectedDevice!.dashboard!.removeWhere( 164 | (element) => element.row == row && element.column == column); 165 | 166 | refreshCharts(); 167 | setRowCount(); 168 | } 169 | 170 | Chart? getLastChart() { 171 | return selectedDevice!.dashboard != null && 172 | selectedDevice!.dashboard!.isNotEmpty 173 | ? selectedDevice!.dashboard!.reduce((currentChart, nextChart) => 174 | currentChart.row > nextChart.row || 175 | (currentChart.row == nextChart.row && 176 | currentChart.column > nextChart.column) 177 | ? currentChart 178 | : nextChart) 179 | : null; 180 | } 181 | 182 | void saveChart(Chart chart, bool newChart) { 183 | if (newChart) { 184 | var lastChart = getLastChart(); 185 | 186 | if (lastChart != null && 187 | chart.data.chartType == ChartType.gauge && 188 | lastChart.data.chartType == ChartType.gauge && 189 | lastChart.column == 1) { 190 | chart.row = lastChart.row; 191 | chart.column = 2; 192 | } else if (lastChart != null) { 193 | chart.row = lastChart.row + 1; 194 | chart.column = 1; 195 | } else { 196 | chart.row = 1; 197 | chart.column = 1; 198 | } 199 | 200 | selectedDevice!.dashboard!.add(chart); 201 | } else { 202 | chart.data.reloadData!(); 203 | setState(() { 204 | chart.data; 205 | }); 206 | } 207 | 208 | setRowCount(); 209 | } 210 | 211 | Widget changeTimeRange(BuildContext context) { 212 | final _formKey = GlobalKey(); 213 | 214 | return AlertDialog( 215 | title: const Text("Change time range"), 216 | content: Form( 217 | key: _formKey, 218 | child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ 219 | Expanded( 220 | child: DropDownListRow( 221 | items: timeOptionList, 222 | value: selectedTimeOption, 223 | onChanged: (value) {}, 224 | onSaved: (value) { 225 | setState(() { 226 | selectedTimeOption = value!; 227 | }); 228 | refreshCharts(); 229 | Navigator.of(context).pop(); 230 | }, 231 | ), 232 | ), 233 | ]), 234 | ), 235 | actions: [ 236 | TextButton( 237 | child: const Text("Cancel"), 238 | onPressed: () { 239 | Navigator.of(context).pop(); 240 | }, 241 | ), 242 | TextButton( 243 | child: const Text("Save", style: TextStyle(color: pink)), 244 | onPressed: (() async { 245 | if (_formKey.currentState!.validate()) { 246 | _formKey.currentState!.save(); 247 | } 248 | })), 249 | ], 250 | ); 251 | } 252 | 253 | Widget changeDashboard(BuildContext context, String? dashboardKey) { 254 | final _formKey = GlobalKey(); 255 | 256 | return AlertDialog( 257 | title: const Text("Change dashboard"), 258 | content: Form( 259 | key: _formKey, 260 | child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ 261 | FutureBuilder( 262 | future: _model.fetchDashboardsByType(selectedDevice!.type), 263 | builder: (context, AsyncSnapshot snapshot) { 264 | if (snapshot.hasData && 265 | snapshot.connectionState == ConnectionState.done) { 266 | final data = snapshot.data; 267 | late List dashboardList = List.empty(); 268 | if (data is List) { 269 | dashboardList = data 270 | .map((d) => 271 | DropDownItem(label: d['key'], value: d['key'])) 272 | .toList(); 273 | } 274 | var selectedDashboard = 275 | dashboardKey == null || dashboardKey.isEmpty 276 | ? selectedDevice!.dashboardKey 277 | : dashboardKey; 278 | 279 | return Expanded( 280 | child: DropDownListRow( 281 | items: dashboardList, 282 | value: selectedDashboard, 283 | onChanged: (value) { 284 | selectedDevice!.dashboardKey = value.toString(); 285 | }, 286 | onSaved: (value) { 287 | selectedDevice!.dashboardKey = value.toString(); 288 | }, 289 | ), 290 | ); 291 | } else { 292 | return const SizedBox( 293 | width: 20, 294 | height: 20, 295 | child: CircularProgressIndicator( 296 | color: pink, 297 | strokeWidth: 3, 298 | ), 299 | ); 300 | } 301 | }), 302 | ]), 303 | ), 304 | actions: [ 305 | TextButton( 306 | child: const Text("Cancel"), 307 | onPressed: () { 308 | Navigator.of(context).pop(); 309 | }, 310 | ), 311 | TextButton( 312 | child: const Text("New"), 313 | onPressed: () { 314 | Navigator.of(context).pop(); 315 | showDialog( 316 | context: context, 317 | builder: (BuildContext context) { 318 | return newDashboard(context); 319 | }, 320 | ); 321 | }, 322 | ), 323 | TextButton( 324 | child: const Text("Save", style: TextStyle(color: pink)), 325 | onPressed: (() async { 326 | _formKey.currentState!.save(); 327 | _model.pairDeviceDashboard( 328 | selectedDevice!.id, selectedDevice!.dashboardKey); 329 | 330 | selectedDevice!.dashboard = await _model.fetchDashboard( 331 | selectedDevice!.dashboardKey, selectedDevice!.type); 332 | 333 | Navigator.of(context).pop(); 334 | 335 | setRowCount(); 336 | refreshCharts(); 337 | })), 338 | ], 339 | ); 340 | } 341 | 342 | Widget newDashboard(BuildContext context) { 343 | late TextEditingController newDashboardController = TextEditingController(); 344 | final _formKey = GlobalKey(); 345 | 346 | return AlertDialog( 347 | title: const Text("New dashboard"), 348 | content: Form( 349 | key: _formKey, 350 | child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ 351 | Expanded( 352 | child: TextBoxRow( 353 | hint: 'Dashboard key', 354 | label: '', 355 | controller: newDashboardController, 356 | padding: const EdgeInsets.fromLTRB(10, 10, 0, 20), 357 | validator: (value) { 358 | if (value == null || value.isEmpty) { 359 | return 'Dashboard ID cannot be empty'; 360 | } 361 | return null; 362 | }, 363 | )), 364 | ]), 365 | ), 366 | actions: [ 367 | TextButton( 368 | child: const Text("Cancel"), 369 | onPressed: () { 370 | Navigator.of(context).pop(); 371 | }, 372 | ), 373 | TextButton( 374 | child: const Text("Create", style: TextStyle(color: pink)), 375 | onPressed: (() async { 376 | await _model.createDashboard( 377 | newDashboardController.text, selectedDevice!.type, null); 378 | 379 | Navigator.of(context).pop(); 380 | 381 | showDialog( 382 | context: context, 383 | builder: (BuildContext context) { 384 | return changeDashboard(context, newDashboardController.text); 385 | }, 386 | ); 387 | })), 388 | ], 389 | ); 390 | } 391 | 392 | Future saveDashboard() async{ 393 | var pairDeviceDashboard = selectedDevice!.dashboardKey.isEmpty; 394 | var dashboardKey = await _model.createDashboard(selectedDevice!.dashboardKey, selectedDevice!.type, 395 | selectedDevice!.dashboard); 396 | 397 | if (pairDeviceDashboard) { 398 | selectedDevice!.dashboardKey = dashboardKey; 399 | _model.pairDeviceDashboard( 400 | selectedDevice!.id, selectedDevice!.dashboardKey); 401 | } 402 | 403 | return pairDeviceDashboard; 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /lib/src/device/controller/device_detail_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:influxdb_client/api.dart'; 4 | import 'package:iot_center_flutter_mvc/src/device/view/dashboard.dart'; 5 | import 'package:iot_center_flutter_mvc/src/view.dart'; 6 | import 'package:iot_center_flutter_mvc/src/model.dart'; 7 | import 'dart:developer' as developer; 8 | import 'package:intl/intl.dart'; 9 | 10 | class DeviceDetailController extends ControllerMVC { 11 | factory DeviceDetailController([StateMVC? state]) => 12 | _this ??= DeviceDetailController._(state); 13 | DeviceDetailController._(StateMVC? state) 14 | : _model = InfluxModel(), 15 | _dashboardController = DashboardController(), 16 | super(state); 17 | static DeviceDetailController? _this; 18 | final InfluxModel _model; 19 | DashboardController _dashboardController; 20 | 21 | Future? selectedDevice; 22 | 23 | InfluxDBClient get client => _model.client; 24 | bool editable = false; 25 | String get selectedTimeOption => _dashboardController.selectedTimeOption; 26 | 27 | get dashboardList => _model.fetchDashboards(); 28 | 29 | Future writeEmulatedData(Function onProgress, String deviceId) async => 30 | _model.writeEmulatedData(deviceId, onProgress); 31 | 32 | Future> getMeasurements(String deviceId) async => 33 | _model.fetchMeasurements(deviceId); 34 | 35 | int rowCount = 0; 36 | 37 | int getRowCount(Device device) { 38 | return device.dashboard != null 39 | ? device.dashboard! 40 | .reduce((currentChart, nextChart) => 41 | currentChart.row > nextChart.row ? currentChart : nextChart) 42 | .row + 43 | 1 44 | : 0; 45 | } 46 | 47 | void updateRowCount(Device device) { 48 | setState(() { 49 | rowCount = getRowCount(device); 50 | }); 51 | } 52 | 53 | void initDevice(String deviceId) { 54 | selectedIndex = 0; 55 | selectedDevice = _model.fetchDeviceWithDashboard(deviceId).then((value) => initTabs(value)); 56 | } 57 | 58 | Device initTabs(Device device) { 59 | measurements = getMeasurements(device.id); 60 | 61 | dashboardTab = getDashboardTab(device); 62 | deviceDetailTab = getDeviceDetailTab(device); 63 | measurementsTab = getMeasurementsTab(); 64 | 65 | _dashboardController = DashboardController(); 66 | 67 | actualTab = dashboardTab; 68 | 69 | return device; 70 | } 71 | 72 | Widget? deviceDetailTab; 73 | Widget? measurementsTab; 74 | Widget? dashboardTab; 75 | 76 | int selectedIndex = 0; 77 | Widget? actualTab; 78 | 79 | void bottomMenuOnTap(int index) { 80 | setState(() { 81 | selectedIndex = index; 82 | switch (index) { 83 | case 0: 84 | actualTab = dashboardTab; 85 | break; 86 | case 1: 87 | actualTab = deviceDetailTab; 88 | break; 89 | case 2: 90 | actualTab = measurementsTab; 91 | break; 92 | } 93 | }); 94 | } 95 | 96 | Widget getDeviceDetailTab(Device device) { 97 | return ListView( 98 | children: [ 99 | tile(device.id, 'Device Id', Icons.device_thermostat), 100 | tile(device.dashboardKey, 'Dashboard Key', 101 | Icons.dashboard), 102 | tile(device.createdAt, 'Registration Time', 103 | Icons.lock_clock), 104 | tile(device.key, 'Device key', Icons.key), 105 | tile(device.influxBucket, 'InfluxDB Bucket', 106 | Icons.shopping_basket_rounded), 107 | ], 108 | ); 109 | } 110 | 111 | Widget getMeasurementsTab() { 112 | return ListView( 113 | children: [ 114 | FutureBuilder( 115 | future: measurements, 116 | builder: (context, AsyncSnapshot snapshot) { 117 | if (snapshot.hasError) { 118 | return Text(snapshot.error.toString()); 119 | } 120 | if (snapshot.hasData && 121 | snapshot.connectionState == ConnectionState.done) { 122 | // return _buildMeasurementList(snapshot.data); 123 | List rows = []; 124 | for (var record in snapshot.data) { 125 | rows.add(measurementContainer(record)); 126 | } 127 | 128 | return Column( 129 | children: rows, 130 | ); 131 | } else { 132 | return const Text("loading..."); 133 | } 134 | }), 135 | ], 136 | ); 137 | } 138 | 139 | Widget getDashboardTab(Device device) { 140 | return DashboardTab(selectedDevice: device); 141 | } 142 | 143 | ListTile tile(String title, String subtitle, IconData icon) => ListTile( 144 | title: Text(title), 145 | subtitle: Text(subtitle), 146 | leading: Icon( 147 | icon, 148 | ), 149 | ); 150 | 151 | Widget measurementContainer(FluxRecord record) { 152 | var format = NumberFormat.decimalPattern(); 153 | var textStyle = const TextStyle( 154 | fontWeight: FontWeight.w600, 155 | ); 156 | 157 | return Padding( 158 | padding: const EdgeInsets.all(5.0), 159 | child: Container( 160 | decoration: boxDecor, 161 | child: Padding( 162 | padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20), 163 | child: Row( 164 | children: [ 165 | SizedBox( 166 | width: 130, 167 | child: Text( 168 | record["_field"], 169 | style: textStyle, 170 | )), 171 | Expanded( 172 | flex: 2, 173 | child: Column( 174 | children: [ 175 | Row( 176 | children: [ 177 | Expanded( 178 | child: Text("Count", 179 | style: textStyle, textScaleFactor: 0.8)), 180 | Text(record["count"].toString(), textScaleFactor: 0.8), 181 | ], 182 | ), 183 | Row( 184 | children: [ 185 | Expanded( 186 | child: Text("Max value", 187 | style: textStyle, textScaleFactor: 0.8)), 188 | Text(format.format(record["maxValue"]), 189 | textScaleFactor: 0.8), 190 | ], 191 | ), 192 | Row( 193 | children: [ 194 | Expanded( 195 | child: Text("Min value", 196 | style: textStyle, textScaleFactor: 0.8)), 197 | Text(format.format(record["minValue"]), 198 | textScaleFactor: 0.8), 199 | ], 200 | ), 201 | Row( 202 | children: [ 203 | Expanded( 204 | child: Text("Max time", 205 | style: textStyle, textScaleFactor: 0.8)), 206 | Text(record["maxTime"], textScaleFactor: 0.8), 207 | ], 208 | ), 209 | ], 210 | ), 211 | ), 212 | ], 213 | ), 214 | ), 215 | ), 216 | ); 217 | } 218 | 219 | bool writeInProgress = false; 220 | Future>? measurements; 221 | 222 | Future writeSampleData(String deviceId) async { 223 | writeEmulatedData((progressPercent, writtenPoints, totalPoints) { 224 | developer.log( 225 | "$progressPercent%, $writtenPoints of $totalPoints points written"); 226 | }, deviceId) 227 | .then((value) { 228 | developer.log("Write completed. $value points written."); 229 | setState(() { 230 | writeInProgress = false; 231 | }); 232 | refreshMeasurements(deviceId); 233 | refreshData(); 234 | return value; 235 | }); 236 | return null; 237 | } 238 | 239 | void refreshMeasurements(String deviceId) { 240 | setState(() { 241 | measurements = getMeasurements(deviceId); 242 | measurementsTab = getMeasurementsTab(); 243 | }); 244 | bottomMenuOnTap(selectedIndex); 245 | } 246 | 247 | void writeStart(String deviceId) async { 248 | if (writeInProgress) { 249 | return; 250 | } 251 | 252 | developer.log("write data.... $deviceId"); 253 | setState(() { 254 | writeInProgress = true; 255 | }); 256 | 257 | var x = await writeSampleData(deviceId); 258 | developer.log("Points written $x"); 259 | } 260 | 261 | void editableOnChange(Device device) async { 262 | if (editable) { 263 | if (await _dashboardController.saveDashboard()){ 264 | setState(() { 265 | deviceDetailTab = getDeviceDetailTab(device); 266 | }); 267 | } 268 | } 269 | setState(() { 270 | editable = !editable; 271 | }); 272 | _dashboardController.editable = editable; 273 | } 274 | 275 | void refreshData() { 276 | _dashboardController.refreshCharts(); 277 | } 278 | 279 | void newChartPage(BuildContext context) { 280 | var chart = Chart( 281 | row: 0, 282 | column: 0, 283 | data: ChartData.gauge( 284 | measurement: '', 285 | endValue: 100, 286 | label: 'label', 287 | unit: 'unit', 288 | startValue: 0, 289 | decimalPlaces: 0, 290 | )); 291 | 292 | Navigator.push( 293 | context, 294 | MaterialPageRoute( 295 | builder: (c) => ChartDetailPage( 296 | chart: chart, 297 | newChart: true, 298 | ), 299 | )); 300 | } 301 | 302 | void timeRangeOnChange(BuildContext context) { 303 | showDialog( 304 | context: context, 305 | builder: (BuildContext context) { 306 | return _dashboardController.changeTimeRange(context); 307 | }, 308 | ).whenComplete(() => setState(() { 309 | _dashboardController.selectedTimeOption; 310 | })); 311 | } 312 | 313 | void changeDashboard(BuildContext context, Device device) { 314 | showDialog( 315 | context: context, 316 | builder: (BuildContext context) { 317 | return _dashboardController.changeDashboard(context, ''); 318 | }, 319 | ); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /lib/src/device/model/chart.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/model.dart'; 2 | 3 | class Chart { 4 | Chart({required this.data, required this.row, required this.column}); 5 | 6 | Chart.empty(); 7 | 8 | Chart.fromJson(Map json) { 9 | var chartType = json['chartType']; 10 | 11 | if (chartType == 'ChartType.gauge') { 12 | data = ChartData.gauge( 13 | measurement: json['measurement'], 14 | endValue: json['endValue'], 15 | label: json['label'], 16 | unit: json['unit'], 17 | startValue: json['startValue'], 18 | decimalPlaces: json['decimalPlaces']); 19 | row = json['row']; 20 | column = json['column']; 21 | } else { 22 | data = ChartData.simple( 23 | measurement: json['measurement'], 24 | label: json['label'], 25 | unit: json['unit'], 26 | ); 27 | row = json['row']; 28 | column = json['column']; 29 | } 30 | } 31 | 32 | Map toJson() => { 33 | 'measurement': data.measurement, 34 | 'label': data.label, 35 | 'unit': data.unit, 36 | 'startValue': data.startValue, 37 | 'endValue': data.endValue, 38 | 'decimalPlaces': data.decimalPlaces, 39 | 'chartType': data.chartType.toString(), 40 | 'row': row, 41 | 'column': column, 42 | }; 43 | 44 | late ChartData data; 45 | 46 | int row = 0; 47 | int column = 0; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/device/model/chart_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:influxdb_client/api.dart'; 2 | 3 | enum ChartType { 4 | gauge, 5 | simple, 6 | } 7 | 8 | class ChartData { 9 | ChartData.gauge({ 10 | required this.measurement, 11 | this.label = '', 12 | this.startValue = 0, 13 | this.endValue = 100, 14 | this.unit = '', 15 | this.size = 130, 16 | this.decimalPlaces = 0, 17 | }) { 18 | chartType = ChartType.gauge; 19 | } 20 | 21 | ChartData.simple({ 22 | required this.measurement, 23 | this.label = '', 24 | this.unit = '', 25 | }) { 26 | chartType = ChartType.simple; 27 | } 28 | 29 | List data = []; 30 | String measurement = ''; 31 | String label = ''; 32 | String unit = ''; 33 | double startValue = 0; 34 | double endValue = 100; 35 | double size = 120; 36 | int? decimalPlaces; 37 | 38 | Function()? reloadData; 39 | 40 | ChartType chartType = ChartType.simple; 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/device/model/device.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/model.dart'; 2 | 3 | class Device { 4 | String influxUrl = ''; 5 | String influxOrg = ''; 6 | String influxToken = ''; 7 | String influxBucket = ''; 8 | String createdAt = ''; 9 | String id = ''; 10 | String key = ''; 11 | String dashboardKey = ''; 12 | String type = ''; 13 | 14 | List? _dashboard; 15 | List? get dashboard => _dashboard; 16 | set dashboard(value) => _dashboard = value; 17 | 18 | String get tokenSubstring => influxToken.toString().substring(0, 3) + "..."; 19 | 20 | Device(this.id, this.createdAt, this.key, this.influxOrg, this.influxUrl, 21 | this.influxBucket, this.influxToken, this.dashboardKey, this.type); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/device/view/chart_detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:influxdb_client/api.dart'; 2 | import 'package:iot_center_flutter_mvc/src/device/controller/chart_detail_controller.dart'; 3 | import 'package:iot_center_flutter_mvc/src/model.dart'; 4 | import 'package:iot_center_flutter_mvc/src/view.dart'; 5 | 6 | class ChartDetailPage extends StatefulWidget { 7 | const ChartDetailPage({Key? key, required this.chart, required this.newChart}) 8 | : super(key: key); 9 | 10 | final Chart chart; 11 | final bool newChart; 12 | 13 | @override 14 | _ChartDetailPageState createState() { 15 | return _ChartDetailPageState(); 16 | } 17 | } 18 | 19 | class _ChartDetailPageState extends StateMVC { 20 | final _formKey = GlobalKey(); 21 | 22 | late ChartDetailController con; 23 | 24 | _ChartDetailPageState() : super(ChartDetailController()) { 25 | con = controller as ChartDetailController; 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Scaffold( 31 | appBar: AppBar( 32 | backgroundColor: darkBlue, 33 | title: widget.newChart 34 | ? const Text("New chart") 35 | : const Text("Edit chart"), 36 | actions: [ 37 | Visibility( 38 | visible: !widget.newChart, 39 | child: IconButton( 40 | icon: const Icon(Icons.delete), 41 | color: Colors.white, 42 | onPressed: () { 43 | con.showAlertDialog(context, widget.chart); 44 | }, 45 | ), 46 | ), 47 | ]), 48 | backgroundColor: lightGrey, 49 | body: Padding( 50 | padding: 51 | const EdgeInsets.only(top: 30, left: 15, right: 15, bottom: 10), 52 | child: Form( 53 | key: _formKey, 54 | child: ListView( 55 | children: [ 56 | DropDownListRow( 57 | label: "Type:", 58 | items: con.chartTypeList, 59 | value: widget.chart.data.chartType.toString(), 60 | onChanged: (value) { 61 | setState(() { 62 | con.isGauge = value == 'ChartType.gauge'; 63 | }); 64 | }, 65 | onSaved: (value) { 66 | con.chartType = value!; 67 | }, 68 | ), 69 | FutureBuilder>( 70 | future: con.fieldNames, 71 | builder: 72 | (context, AsyncSnapshot> snapshot) { 73 | if (snapshot.hasError) { 74 | return Text(snapshot.error.toString()); 75 | } 76 | if (snapshot.hasData) { 77 | final List data = snapshot.data!; 78 | final items = data 79 | .map((x) => DropDownItem( 80 | label: x["_value"], value: x["_value"])) 81 | .toList(); 82 | 83 | return DropDownListRow( 84 | label: "Field:", 85 | items: items, 86 | value: widget.chart.data.measurement, 87 | onChanged: (value) {}, 88 | onSaved: (value) { 89 | widget.chart.data.measurement = value!; 90 | }, 91 | addIfMissing: true, 92 | ); 93 | } else { 94 | return const Text("loading..."); 95 | } 96 | }), 97 | TextBoxRow( 98 | label: "Label:", 99 | controller: con.label, 100 | ), 101 | Visibility( 102 | visible: con.isGauge, 103 | child: DoubleNumberBoxRow( 104 | label: "Range:", 105 | firstController: con.startValue, 106 | secondController: con.endValue, 107 | ), 108 | ), 109 | Visibility( 110 | visible: con.isGauge, 111 | child: NumberBoxRow( 112 | label: "Rounded:", 113 | controller: con.decimalPlaces, 114 | ), 115 | ), 116 | TextBoxRow( 117 | label: "Unit:", 118 | controller: con.unit, 119 | ), 120 | Padding( 121 | padding: 122 | const EdgeInsets.symmetric(vertical: 35, horizontal: 3), 123 | child: FormButton( 124 | label: widget.newChart ? 'Create' : 'Update', 125 | onPressed: () { 126 | if (_formKey.currentState!.validate()) { 127 | _formKey.currentState!.save(); 128 | con.saveChart(widget.newChart); 129 | Navigator.pop(context); 130 | } 131 | }), 132 | ), 133 | ], 134 | ), 135 | ), 136 | )); 137 | } 138 | 139 | @override 140 | void initState() { 141 | super.initState(); 142 | add(con); 143 | con.chart = widget.chart; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/src/device/view/dashboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/model.dart'; 2 | import 'package:iot_center_flutter_mvc/src/view.dart'; 3 | 4 | class DashboardTab extends StatefulWidget { 5 | const DashboardTab({required this.selectedDevice, Key? key}) : super(key: key); 6 | 7 | final Device selectedDevice; 8 | 9 | @override 10 | _DashboardTabState createState() { 11 | return _DashboardTabState(); 12 | } 13 | } 14 | 15 | class _DashboardTabState extends StateMVC { 16 | late DashboardController con; 17 | 18 | _DashboardTabState() : super(DashboardController()) { 19 | con = controller as DashboardController; 20 | } 21 | 22 | @override 23 | void initState() { 24 | con.selectedDevice = widget.selectedDevice; 25 | 26 | super.initState(); 27 | add(con); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | con.setRowCount(); 33 | return ListView.builder( 34 | itemCount: con.rowCount, 35 | itemBuilder: (context, index) { 36 | return con.buildChartListViewRow(index, context); 37 | }, 38 | ); 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/device/view/device_detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/device/controller/device_detail_controller.dart'; 2 | import 'package:iot_center_flutter_mvc/src/view.dart'; 3 | 4 | import '../../model.dart'; 5 | 6 | class DeviceDetailPage extends StatefulWidget { 7 | const DeviceDetailPage({required this.deviceId, Key? key}) : super(key: key); 8 | 9 | final String deviceId; 10 | 11 | @override 12 | _DeviceDetailPageState createState() { 13 | return _DeviceDetailPageState(); 14 | } 15 | } 16 | 17 | class _DeviceDetailPageState extends StateMVC { 18 | late DeviceDetailController con; 19 | 20 | _DeviceDetailPageState() : super(DeviceDetailController()) { 21 | con = controller as DeviceDetailController; 22 | } 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | add(con); 28 | con.initDevice(widget.deviceId); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return FutureBuilder( 34 | future: con.selectedDevice, 35 | builder: (context, AsyncSnapshot device) { 36 | if (device.hasData && 37 | device.connectionState == ConnectionState.done) { 38 | return Scaffold( 39 | extendBodyBehindAppBar: false, 40 | backgroundColor: lightGrey, 41 | appBar: AppBar( 42 | title: Text(widget.deviceId), 43 | backgroundColor: darkBlue, 44 | actions: [ 45 | Visibility( 46 | visible: con.selectedIndex == 0 && !con.editable, 47 | child: TextButton( 48 | style: TextButton.styleFrom( 49 | textStyle: const TextStyle( 50 | fontSize: 19, fontWeight: FontWeight.w500), 51 | primary: Colors.white, 52 | ), 53 | onPressed: () { 54 | con.timeRangeOnChange(context); 55 | }, 56 | child: Text(con.selectedTimeOption), 57 | ), 58 | ), 59 | Visibility( 60 | visible: con.selectedIndex == 0 && con.editable, 61 | child: IconButton( 62 | icon: const Icon(Icons.dashboard_customize), 63 | color: Colors.white, 64 | onPressed: () { 65 | con.changeDashboard(context, device.data!); 66 | }, 67 | ), 68 | ), 69 | Visibility( 70 | visible: con.selectedIndex == 0 && !con.editable, 71 | child: IconButton( 72 | icon: const Icon(Icons.refresh), 73 | color: Colors.white, 74 | onPressed: () { 75 | con.refreshData(); 76 | }, 77 | ), 78 | ), 79 | Visibility( 80 | visible: con.selectedIndex == 0, 81 | child: IconButton( 82 | icon: Icon(con.editable ? Icons.done : Icons.edit), 83 | color: Colors.white, 84 | onPressed: () { 85 | con.editableOnChange(device.data!); 86 | }, 87 | ), 88 | ), 89 | Visibility( 90 | visible: con.selectedIndex == 1 && 91 | device.data!.type == "virtual", 92 | child: IconButton( 93 | icon: Icon(con.writeInProgress 94 | ? Icons.lock_outline 95 | : Icons.app_registration), 96 | color: Colors.white, 97 | onPressed: () async { 98 | con.writeStart(widget.deviceId); 99 | }, 100 | ), 101 | ), 102 | Visibility( 103 | visible: con.selectedIndex == 2, 104 | child: IconButton( 105 | icon: const Icon(Icons.refresh), 106 | color: Colors.white, 107 | onPressed: () { 108 | con.refreshMeasurements(widget.deviceId); 109 | }, 110 | ), 111 | ), 112 | ], 113 | ), 114 | floatingActionButton: Visibility( 115 | visible: con.editable, 116 | child: FloatingActionButton( 117 | backgroundColor: darkBlue, 118 | child: const Icon(Icons.add), 119 | onPressed: () { 120 | con.newChartPage(context); 121 | }, 122 | ), 123 | ), 124 | bottomNavigationBar: Visibility( 125 | visible: !con.editable, 126 | child: BottomNavigationBar( 127 | currentIndex: con.selectedIndex, 128 | backgroundColor: Colors.white, 129 | selectedItemColor: pink, 130 | unselectedItemColor: darkBlue, 131 | selectedFontSize: 12, 132 | unselectedFontSize: 12, 133 | items: const [ 134 | BottomNavigationBarItem( 135 | icon: Icon(Icons.dashboard_outlined), 136 | label: 'Dashboard', 137 | ), 138 | BottomNavigationBarItem( 139 | icon: Icon(Icons.info_outlined), 140 | label: 'Device detail', 141 | ), 142 | BottomNavigationBarItem( 143 | icon: Icon(Icons.analytics_outlined), 144 | label: 'Measurements', 145 | ), 146 | ], 147 | onTap: con.bottomMenuOnTap, 148 | ), 149 | ), 150 | body: Padding( 151 | padding: const EdgeInsets.all(10), child: con.actualTab)); 152 | } 153 | return const SizedBox( 154 | width: 20, 155 | height: 20, 156 | child: CircularProgressIndicator( 157 | color: pink, 158 | strokeWidth: 3, 159 | ), 160 | ); 161 | }); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /lib/src/device/view/gauge_chart.dart: -------------------------------------------------------------------------------- 1 | import 'package:influxdb_client/api.dart'; 2 | import 'package:iot_center_flutter_mvc/src/model.dart'; 3 | import 'package:iot_center_flutter_mvc/src/view.dart'; 4 | 5 | import 'package:vector_math/vector_math.dart' show radians; 6 | import 'dart:math' show cos, pi, sin; 7 | 8 | class GaugeChart extends StatefulWidget { 9 | const GaugeChart({ 10 | Key? key, 11 | required this.chartData, required this.con, 12 | }) : super(key: key); 13 | 14 | final ChartData chartData; 15 | final DashboardController con; 16 | 17 | @override 18 | StateMVC createState() { 19 | return _GaugeChart(); 20 | } 21 | } 22 | 23 | class _GaugeChart extends StateMVC { 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | _data = widget.con.getDataFromInflux(widget.chartData.measurement, true); 29 | 30 | widget.chartData.reloadData = () { 31 | _data = widget.con.getDataFromInflux(widget.chartData.measurement, true); 32 | refresh(); 33 | }; 34 | } 35 | 36 | Future>? _data; 37 | 38 | @override 39 | Widget buildWidget(BuildContext context) { 40 | return Column( 41 | children: [ 42 | Row( 43 | children: [ 44 | Expanded( 45 | child: Center( 46 | child: SizedBox( 47 | width: widget.chartData.size, 48 | height: widget.chartData.size, 49 | child: FutureBuilder( 50 | future: _data, 51 | builder: (context, AsyncSnapshot snapshot) { 52 | if (snapshot.hasError) { 53 | return Text(snapshot.error.toString()); 54 | } 55 | if (snapshot.hasData && 56 | snapshot.connectionState == 57 | ConnectionState.done) { 58 | widget.chartData.data = snapshot.data; 59 | final value = widget.chartData.data.isNotEmpty 60 | ? widget.con.getDouble( 61 | widget.chartData.data.last["_value"]) 62 | : widget.chartData.startValue; 63 | 64 | var calcValue = 65 | (value - widget.chartData.startValue) / 66 | (widget.chartData.endValue - 67 | widget.chartData.startValue); 68 | 69 | calcValue = calcValue > 1 ? 1 : calcValue; 70 | calcValue = calcValue < 0 ? 0 : calcValue; 71 | 72 | final label = widget.chartData.data.isNotEmpty 73 | ? value.toStringAsFixed( 74 | widget.chartData.decimalPlaces!) 75 | : "no data"; 76 | return CustomPaint( 77 | painter: GaugeChartPainter( 78 | calcValue: calcValue, 79 | radius: widget.chartData.size / 2, 80 | ), 81 | child: Padding( 82 | padding: EdgeInsets.only( 83 | top: widget.chartData.size / 3 + 10), 84 | child: Column( 85 | children: [ 86 | Text( 87 | label, 88 | style: TextStyle( 89 | fontSize: widget.chartData.size / 9, 90 | fontWeight: FontWeight.w700, 91 | ), 92 | ), 93 | Text( 94 | widget.chartData.unit, 95 | style: TextStyle( 96 | fontSize: 97 | (widget.chartData.size / 8) - 5, 98 | fontWeight: FontWeight.w800, 99 | ), 100 | ), 101 | ], 102 | ), 103 | )); 104 | } else { 105 | return CustomPaint( 106 | painter: GaugeChartPainter( 107 | calcValue: 0, 108 | radius: widget.chartData.size / 2, 109 | ), 110 | child: Padding( 111 | padding: EdgeInsets.only( 112 | top: widget.chartData.size / 3 + 10), 113 | child: Column( 114 | children: const [ 115 | SizedBox( 116 | width: 20, 117 | height: 20, 118 | child: CircularProgressIndicator( 119 | color: pink, 120 | strokeWidth: 3, 121 | ), 122 | ), 123 | ], 124 | ), 125 | )); 126 | } 127 | }) 128 | // 129 | ), 130 | ), 131 | ) 132 | ], 133 | ), 134 | SizedBox( 135 | width: widget.chartData.size, 136 | child: Row( 137 | children: [ 138 | Expanded( 139 | child: Center( 140 | child: Text( 141 | widget.chartData.startValue.toStringAsFixed(0), 142 | style: const TextStyle( 143 | fontSize: 12, 144 | fontWeight: FontWeight.w600, 145 | ), 146 | ), 147 | ), 148 | ), 149 | Expanded( 150 | child: Center( 151 | child: Text( 152 | widget.chartData.endValue.toStringAsFixed(0), 153 | style: const TextStyle( 154 | fontSize: 12, 155 | fontWeight: FontWeight.w600, 156 | ), 157 | ), 158 | ), 159 | ) 160 | ], 161 | ), 162 | ) 163 | ], 164 | ); 165 | } 166 | } 167 | 168 | class GaugeChartPainter extends CustomPainter { 169 | const GaugeChartPainter({ 170 | required this.calcValue, 171 | required this.radius, 172 | }); 173 | 174 | final double calcValue; 175 | final double radius; 176 | 177 | static const double startAngle = 130; 178 | static const double endAngle = 280; 179 | static const double levelCount = 51; 180 | 181 | @override 182 | void paint(Canvas canvas, Size size) { 183 | final space = radius / 2; 184 | const itemAngle = endAngle / levelCount; 185 | final width = radius / 6 + 5; 186 | 187 | var paint = Paint() 188 | ..strokeWidth = radius / 100 189 | ..strokeCap = StrokeCap.round 190 | ..style = PaintingStyle.stroke; 191 | 192 | for (var index = 0; index < levelCount; index++) { 193 | final angle = itemAngle * index + startAngle + (itemAngle / 2); 194 | canvas.save(); 195 | 196 | final offset = Offset( 197 | (radius - space) * cos(pi * angle / 180) + radius, 198 | (radius - space) * sin(pi * angle / 180) + radius, 199 | ); 200 | 201 | canvas.translate(offset.dx, offset.dy); 202 | canvas.rotate(radians(angle)); 203 | 204 | canvas.drawLine( 205 | Offset.zero, 206 | Offset(index % 10 == 0 ? space / 3 : space / 5, 0), 207 | paint, 208 | ); 209 | canvas.restore(); 210 | } 211 | 212 | final outerRect = Rect.fromLTWH( 213 | width / 2, 214 | width / 2, 215 | size.height - width, 216 | size.height - width, 217 | ); 218 | 219 | paint 220 | ..color = Colors.black26 221 | ..strokeWidth = width; 222 | 223 | canvas.drawArc( 224 | outerRect, 225 | radians(startAngle), 226 | radians(endAngle), 227 | false, 228 | paint, 229 | ); 230 | 231 | canvas.save(); 232 | 233 | paint 234 | ..color = Colors.white 235 | ..shader = SweepGradient( 236 | stops: const [0, 0.4], 237 | colors: const [ 238 | pink, 239 | purple, 240 | ], 241 | startAngle: radians(0), 242 | endAngle: radians(360), 243 | transform: GradientRotation(radians(startAngle - width)), 244 | ).createShader(outerRect); 245 | 246 | canvas.drawArc( 247 | outerRect, 248 | radians(startAngle), 249 | radians(endAngle * calcValue), 250 | false, 251 | paint, 252 | ); 253 | 254 | canvas.restore(); 255 | } 256 | 257 | @override 258 | bool shouldRepaint(covariant CustomPainter oldDelegate) { 259 | return this != oldDelegate; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /lib/src/device/view/simple_chart.dart: -------------------------------------------------------------------------------- 1 | import 'package:charts_flutter/flutter.dart' as charts; 2 | import 'package:influxdb_client/api.dart'; 3 | import 'package:iot_center_flutter_mvc/src/model.dart'; 4 | import 'package:iot_center_flutter_mvc/src/view.dart'; 5 | 6 | class SimpleChart extends StatefulWidget { 7 | const SimpleChart({Key? key, required this.chartData, required this.con}) 8 | : super(key: key); 9 | 10 | final ChartData chartData; 11 | final DashboardController con; 12 | 13 | @override 14 | State createState() { 15 | return _SimpleChart(); 16 | } 17 | } 18 | 19 | class _SimpleChart extends StateMVC { 20 | Future>? _data; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _data = widget.con.getDataFromInflux(widget.chartData.measurement, false); 26 | 27 | widget.chartData.reloadData = () { 28 | _data = widget.con.getDataFromInflux(widget.chartData.measurement, false); 29 | refresh(); 30 | }; 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return FutureBuilder( 36 | future: _data, 37 | builder: (context, AsyncSnapshot snapshot) { 38 | if (snapshot.hasError) { 39 | return Text(snapshot.error.toString()); 40 | } 41 | if (snapshot.hasData && 42 | snapshot.connectionState == ConnectionState.done) { 43 | var series = [ 44 | charts.Series( 45 | id: widget.chartData.measurement, 46 | data: snapshot.data!, 47 | seriesColor: charts.ColorUtil.fromDartColor(pink), 48 | domainFn: (r, _) => DateTime.parse(r['_time']), 49 | measureFn: (r, _) => r["_value"], 50 | ) 51 | ]; 52 | 53 | return Stack(children: [ 54 | SizedBox( 55 | height: 130, 56 | child: charts.TimeSeriesChart( 57 | series, 58 | animate: true, 59 | ), 60 | ), 61 | Center( 62 | child: Text( 63 | widget.chartData.unit, 64 | style: const TextStyle( 65 | fontSize: 12, 66 | fontWeight: FontWeight.w700, 67 | ), 68 | ), 69 | ) 70 | ]); 71 | } else { 72 | var series = [ 73 | charts.Series( 74 | id: widget.chartData.measurement, 75 | data: [], 76 | seriesColor: charts.ColorUtil.fromDartColor(pink), 77 | domainFn: (r, _) => DateTime.parse(r['_time']), 78 | measureFn: (r, _) => r["_value"], 79 | ) 80 | ]; 81 | return Stack(children: [ 82 | SizedBox( 83 | height: 130, 84 | child: charts.TimeSeriesChart( 85 | series, 86 | animate: true, 87 | ), 88 | ), 89 | Center( 90 | child: Column( 91 | children: [ 92 | Text( 93 | widget.chartData.unit, 94 | style: const TextStyle( 95 | fontSize: 12, 96 | fontWeight: FontWeight.w700, 97 | ), 98 | ), 99 | const Padding( 100 | padding: EdgeInsets.all(20.0), 101 | child: SizedBox( 102 | width: 20, 103 | height: 20, 104 | child: CircularProgressIndicator( 105 | color: pink, 106 | strokeWidth: 3, 107 | ), 108 | ), 109 | ), 110 | ], 111 | ), 112 | ) 113 | ]); 114 | } 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/src/home/controller/home_page_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:influxdb_client/api.dart'; 4 | import 'package:iot_center_flutter_mvc/src/view.dart'; 5 | import 'package:iot_center_flutter_mvc/src/model.dart'; 6 | 7 | class HomePageController extends ControllerMVC { 8 | factory HomePageController([StateMVC? state]) => 9 | _this ??= HomePageController._(state); 10 | HomePageController._(StateMVC? state) 11 | : _model = InfluxModel(), 12 | super(state); 13 | static HomePageController? _this; 14 | final InfluxModel _model; 15 | 16 | InfluxDBClient get client => _model.client; 17 | Future> get deviceList => _model.fetchDevices(); 18 | 19 | 20 | Future checkClient(InfluxDBClient client) => _model.checkClient(client); 21 | 22 | DashboardsTab? devicesListView; 23 | late SensorsTab sensorsView; 24 | InfluxSettingsTab? influxSettings; 25 | 26 | int selectedIndex = 0; 27 | Widget? actualTab; 28 | 29 | bool deleteWithData = false; 30 | bool settingsReadonly = true; 31 | 32 | void refreshDevices() { 33 | setState(() { 34 | deviceList; 35 | }); 36 | } 37 | 38 | /// Load client settings for a InfluxDB from Shared Preferences. 39 | void loadSavedInfluxClient() { 40 | client.loadInfluxClient(); 41 | } 42 | 43 | /// Save client settings for a InfluxDB from Shared Preferences. 44 | void saveInfluxClient() { 45 | client.saveInfluxClient(); 46 | } 47 | 48 | Widget newDeviceDialog(BuildContext context) { 49 | late var newDeviceController = TextEditingController(); 50 | final _formKey = GlobalKey(); 51 | 52 | var selectedDeviceType = _model.deviceTypeList.first.value; 53 | return AlertDialog( 54 | title: const Text("New Device"), 55 | content: Form( 56 | key: _formKey, 57 | child: Column( 58 | mainAxisSize: MainAxisSize.min, 59 | children: [ 60 | Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ 61 | Expanded( 62 | child: TextBoxRow( 63 | hint: 'Device ID', 64 | label: '', 65 | controller: newDeviceController, 66 | padding: const EdgeInsets.fromLTRB(10, 10, 0, 20), 67 | validator: (value) { 68 | if (value == null || value.isEmpty) { 69 | return 'Device ID cannot be empty'; 70 | } 71 | return null; 72 | }, 73 | )), 74 | ]), 75 | Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ 76 | Expanded( 77 | child: DropDownListRow( 78 | items: _model.deviceTypeList, 79 | value: selectedDeviceType, 80 | onChanged: (value) { 81 | selectedDeviceType = value!; 82 | }, 83 | ), 84 | ), 85 | ]), 86 | ], 87 | ), 88 | ), 89 | actions: [ 90 | TextButton( 91 | child: const Text("Cancel"), 92 | onPressed: () { 93 | Navigator.of(context).pop(); 94 | }, 95 | ), 96 | TextButton( 97 | child: const Text("Save", style: TextStyle(color: pink)), 98 | onPressed: (() async { 99 | if (_formKey.currentState!.validate()) { 100 | _formKey.currentState!.save(); 101 | 102 | await _model.createDevice( 103 | newDeviceController.text, selectedDeviceType); 104 | refreshDevices(); 105 | 106 | Navigator.of(context).pop(); 107 | } 108 | })), 109 | ], 110 | ); 111 | } 112 | 113 | Color getColor(Set states) { 114 | const Set interactiveStates = { 115 | MaterialState.pressed, 116 | MaterialState.hovered, 117 | MaterialState.focused, 118 | }; 119 | if (states.any(interactiveStates.contains)) { 120 | return Colors.blue; 121 | } 122 | return pink; 123 | } 124 | 125 | Widget removeDeviceDialog(BuildContext context, deviceId) { 126 | deleteWithData = false; 127 | return AlertDialog( 128 | title: Text("Confirm delete device $deviceId ?"), 129 | content: StatefulBuilder(builder: (context, setState) { 130 | return Row(children: [ 131 | Checkbox( 132 | checkColor: Colors.white, 133 | fillColor: MaterialStateProperty.resolveWith(getColor), 134 | value: deleteWithData, 135 | onChanged: (bool? value) { 136 | setState(() { 137 | deleteWithData = value!; 138 | }); 139 | }, 140 | ), 141 | const Text("Delete device with data?"), 142 | ]); 143 | }), 144 | actions: [ 145 | TextButton( 146 | child: const Text("Cancel"), 147 | onPressed: () { 148 | Navigator.of(context).pop(); 149 | }, 150 | ), 151 | TextButton( 152 | child: const Text("Delete", style: TextStyle(color: pink)), 153 | onPressed: (() async { 154 | await _model.deleteDevice(deviceId!, deleteWithData); 155 | Navigator.of(context).pop(); 156 | 157 | refreshDevices(); 158 | })), 159 | ], 160 | ); 161 | } 162 | 163 | void deviceDetail(BuildContext context, String deviceId) async { 164 | Navigator.push( 165 | context, 166 | MaterialPageRoute( 167 | builder: (context) => 168 | DeviceDetailPage( 169 | deviceId: deviceId))) 170 | .whenComplete( 171 | () => refreshDevices()); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/src/home/view/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/view.dart'; 2 | import 'package:iot_center_flutter_mvc/src/controller.dart'; 3 | 4 | class HomePage extends StatefulWidget { 5 | const HomePage({Key? key, this.title = 'IoT Center Demo'}) : super(key: key); 6 | final String title; 7 | 8 | @override 9 | State createState() => _HomePageState(); 10 | } 11 | 12 | class _HomePageState extends StateMVC { 13 | _HomePageState() : super(HomePageController()) { 14 | con = controller as HomePageController; 15 | } 16 | 17 | late HomePageController con; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | add(con); 23 | } 24 | 25 | @override 26 | Widget buildWidget(BuildContext context) { 27 | return Scaffold( 28 | extendBodyBehindAppBar: false, 29 | backgroundColor: lightGrey, 30 | appBar: AppBar( 31 | title: const Text('IoT Center Demo'), 32 | backgroundColor: darkBlue, 33 | actions: [ 34 | IconButton( 35 | icon: const Icon(Icons.add), 36 | color: Colors.white, 37 | onPressed: (() { 38 | showDialog( 39 | context: context, 40 | builder: (BuildContext context) { 41 | return con.newDeviceDialog(context); 42 | }, 43 | ); 44 | })), 45 | IconButton( 46 | icon: const Icon(Icons.refresh), 47 | color: Colors.white, 48 | onPressed: () { 49 | con.refreshDevices(); 50 | }, 51 | ), 52 | IconButton( 53 | icon: const Icon(Icons.settings), 54 | color: Colors.white, 55 | onPressed: () async { 56 | await Navigator.push(context, 57 | MaterialPageRoute(builder: (c) => const SettingsPage())); 58 | refresh(); 59 | }, 60 | ), 61 | ], 62 | ), 63 | body: Padding( 64 | padding: const EdgeInsets.all(8.0), 65 | child: FutureBuilder( 66 | future: con.deviceList, 67 | builder: (context, AsyncSnapshot snapshot) { 68 | if (snapshot.hasData && 69 | snapshot.connectionState == ConnectionState.done) { 70 | return ListView.builder( 71 | itemCount: snapshot.data.length, 72 | itemBuilder: (_, index) { 73 | return Padding( 74 | padding: const EdgeInsets.all(5), 75 | child: Container( 76 | decoration: boxDecor, 77 | child: Row( 78 | children: [ 79 | const Padding( 80 | padding: EdgeInsets.symmetric( 81 | vertical: 30, horizontal: 20), 82 | child: Icon( 83 | Icons.thermostat_outlined, 84 | color: Colors.grey, 85 | ), 86 | ), 87 | Expanded( 88 | child: Column( 89 | crossAxisAlignment: 90 | CrossAxisAlignment.start, 91 | children: [ 92 | Text( 93 | '${snapshot.data[index]['deviceId']}', 94 | style: const TextStyle( 95 | color: darkBlue, 96 | fontWeight: FontWeight.w500), 97 | ), 98 | // Text( 99 | // '${snapshot.data[index]['dashboardKey']}', 100 | // style: const TextStyle( 101 | // color: Colors.grey, 102 | // fontWeight: FontWeight.w400), 103 | // ), 104 | ], 105 | ), 106 | ), 107 | IconButton( 108 | icon: const Icon( 109 | Icons.delete_outline, 110 | color: pink, 111 | ), 112 | onPressed: () { 113 | showDialog( 114 | context: context, 115 | builder: (BuildContext context) { 116 | return con.removeDeviceDialog(context, 117 | snapshot.data[index]['deviceId']); 118 | }, 119 | ); 120 | }), 121 | IconButton( 122 | icon: const Icon( 123 | Icons.arrow_forward, 124 | color: darkBlue, 125 | ), 126 | onPressed: () { 127 | con.deviceDetail(context, 128 | snapshot.data[index]['deviceId']); 129 | }), 130 | ], 131 | ), 132 | ), 133 | ); 134 | }); 135 | } else { 136 | return const SizedBox( 137 | width: 20, 138 | height: 20, 139 | child: CircularProgressIndicator( 140 | color: pink, 141 | strokeWidth: 3, 142 | ), 143 | ); 144 | } 145 | }), 146 | )); 147 | } 148 | 149 | @override 150 | void onError(FlutterErrorDetails details) { 151 | super.onError(details); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/src/model.dart: -------------------------------------------------------------------------------- 1 | export 'package:iot_center_flutter_mvc/src/app/model/influx_client.dart'; 2 | 3 | export 'package:iot_center_flutter_mvc/src/app/model/influx_model.dart'; 4 | 5 | export 'package:iot_center_flutter_mvc/src/app/model/device_config.dart'; 6 | 7 | 8 | 9 | export 'package:iot_center_flutter_mvc/src/device/model/device.dart'; 10 | 11 | export 'package:iot_center_flutter_mvc/src/device/controller/dashboard_controller.dart'; 12 | 13 | 14 | 15 | export 'package:iot_center_flutter_mvc/src/device/model/chart.dart'; 16 | 17 | export 'package:iot_center_flutter_mvc/src/device/model/chart_data.dart'; 18 | -------------------------------------------------------------------------------- /lib/src/settings/controller/settings_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:influxdb_client/api.dart'; 4 | import 'package:iot_center_flutter_mvc/src/view.dart'; 5 | import 'package:iot_center_flutter_mvc/src/model.dart'; 6 | 7 | class SettingsPageController extends ControllerMVC { 8 | factory SettingsPageController([StateMVC? state]) => 9 | _this ??= SettingsPageController._(state); 10 | SettingsPageController._(StateMVC? state) 11 | : _model = InfluxModel(), 12 | super(state); 13 | static SettingsPageController? _this; 14 | final InfluxModel _model; 15 | 16 | InfluxDBClient get client => _model.client; 17 | Future> get dashboardList => _model.fetchDashboards(); 18 | 19 | Future checkClient(InfluxDBClient client) => _model.checkClient(client); 20 | 21 | DashboardsTab? dashboardsListView; 22 | late SensorsTab sensorsView; 23 | InfluxSettingsTab? influxSettings; 24 | 25 | int selectedIndex = 0; 26 | Widget? actualTab; 27 | 28 | bool deleteWithData = false; 29 | bool settingsReadonly = true; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | 35 | dashboardsListView = DashboardsTab(con: this); 36 | sensorsView = SensorsTab(con: this); 37 | influxSettings = InfluxSettingsTab(con: this); 38 | 39 | if (actualTab != null) { 40 | bottomMenuOnTap(selectedIndex); 41 | } else { 42 | actualTab = dashboardsListView; 43 | } 44 | } 45 | 46 | void bottomMenuOnTap(int index) { 47 | setState(() { 48 | selectedIndex = index; 49 | switch (index) { 50 | case 0: 51 | actualTab = dashboardsListView; 52 | break; 53 | case 1: 54 | actualTab = sensorsView; 55 | break; 56 | case 2: 57 | actualTab = influxSettings; 58 | break; 59 | } 60 | }); 61 | } 62 | 63 | void refreshDashboards() { 64 | setState(() { 65 | dashboardList; 66 | dashboardsListView = DashboardsTab(con: this); 67 | }); 68 | 69 | bottomMenuOnTap(selectedIndex); 70 | } 71 | 72 | /// Load client settings for a InfluxDB from Shared Preferences. 73 | void loadSavedInfluxClient() { 74 | client.loadInfluxClient(); 75 | } 76 | 77 | /// Save client settings for a InfluxDB from Shared Preferences. 78 | void saveInfluxClient() { 79 | client.saveInfluxClient(); 80 | } 81 | 82 | Widget newDashboardDialog(BuildContext context) { 83 | late var newDashboardController = TextEditingController(); 84 | final _formKey = GlobalKey(); 85 | 86 | var selectedDeviceType = _model.deviceTypeList.first.value; 87 | return AlertDialog( 88 | title: const Text("New Dashboard"), 89 | content: Form( 90 | key: _formKey, 91 | child: Column( 92 | mainAxisSize: MainAxisSize.min, 93 | children: [ 94 | Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ 95 | Expanded( 96 | child: TextBoxRow( 97 | hint: 'Dashboard key', 98 | label: '', 99 | controller: newDashboardController, 100 | padding: const EdgeInsets.fromLTRB(10, 10, 0, 20), 101 | validator: (value) { 102 | if (value == null || value.isEmpty) { 103 | return 'Dashboard key cannot be empty'; 104 | } 105 | return null; 106 | }, 107 | )), 108 | ]), 109 | Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ 110 | Expanded( 111 | child: DropDownListRow( 112 | items: _model.deviceTypeList, 113 | value: selectedDeviceType, 114 | onChanged: (value) { 115 | selectedDeviceType = value!; 116 | }, 117 | ), 118 | ), 119 | ]), 120 | ], 121 | ), 122 | ), 123 | actions: [ 124 | TextButton( 125 | child: const Text("Cancel"), 126 | onPressed: () { 127 | Navigator.of(context).pop(); 128 | }, 129 | ), 130 | TextButton( 131 | child: const Text("Save", style: TextStyle(color: pink)), 132 | onPressed: (() async { 133 | if (_formKey.currentState!.validate()) { 134 | _formKey.currentState!.save(); 135 | 136 | await _model.createDashboard(newDashboardController.text, 137 | selectedDeviceType, List.empty(growable: true)); 138 | refreshDashboards(); 139 | 140 | Navigator.of(context).pop(); 141 | } 142 | })), 143 | ], 144 | ); 145 | } 146 | 147 | Color getColor(Set states) { 148 | const Set interactiveStates = { 149 | MaterialState.pressed, 150 | MaterialState.hovered, 151 | MaterialState.focused, 152 | }; 153 | if (states.any(interactiveStates.contains)) { 154 | return Colors.blue; 155 | } 156 | return pink; 157 | } 158 | 159 | Widget removeDashboardDialog(BuildContext context, dashboardKey) { 160 | return AlertDialog( 161 | title: Text("Confirm delete dashboard $dashboardKey ?"), 162 | actions: [ 163 | TextButton( 164 | child: const Text("Cancel"), 165 | onPressed: () { 166 | Navigator.of(context).pop(); 167 | }, 168 | ), 169 | TextButton( 170 | child: const Text("Delete", style: TextStyle(color: pink)), 171 | onPressed: (() async { 172 | _model.deleteDashboard(dashboardKey); 173 | Navigator.of(context).pop(); 174 | 175 | refreshDashboards(); 176 | })), 177 | ], 178 | ); 179 | } 180 | 181 | void changeReadonly() { 182 | setState(() { 183 | settingsReadonly = !settingsReadonly; 184 | influxSettings = InfluxSettingsTab(con: this); 185 | }); 186 | bottomMenuOnTap(selectedIndex); 187 | } 188 | 189 | Future> deviceList([String? dashboardKey]) { 190 | return dashboardKey != null 191 | ? _model.fetchDashboardDevices(dashboardKey) 192 | : _model.fetchDevices(); 193 | } 194 | 195 | void writeSensor(Map fieldValueMap) { 196 | _model.writePoint(fieldValueMap); 197 | } 198 | 199 | Future createDevice(String deviceId, String deviceType) { 200 | return _model.createDevice(deviceId, deviceType); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /lib/src/settings/view/clientId_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/controller.dart'; 2 | import 'package:iot_center_flutter_mvc/src/view.dart'; 3 | 4 | class ClientIdDialog extends StatelessWidget { 5 | const ClientIdDialog({ 6 | Key? key, 7 | required this.con, 8 | required this.currentClientId, 9 | required this.onClientRegistered, 10 | }) : super(key: key); 11 | 12 | final SettingsPageController con; 13 | final String currentClientId; 14 | final void Function(String clientId) onClientRegistered; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | late var newDeviceController = TextEditingController(); 19 | newDeviceController.text = currentClientId; 20 | final _formKey = GlobalKey(); 21 | 22 | return AlertDialog( 23 | title: const Text("New Device"), 24 | content: Form( 25 | key: _formKey, 26 | child: Column( 27 | mainAxisSize: MainAxisSize.min, 28 | children: [ 29 | Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ 30 | Expanded( 31 | child: TextBoxRow( 32 | hint: 'Device ID', 33 | label: '', 34 | controller: newDeviceController, 35 | padding: const EdgeInsets.fromLTRB(10, 10, 0, 20), 36 | validator: (value) { 37 | if (value == null || value.isEmpty) { 38 | return 'Device ID cannot be empty'; 39 | } 40 | return null; 41 | }, 42 | )), 43 | ]), 44 | ], 45 | ), 46 | ), 47 | actions: [ 48 | TextButton( 49 | child: const Text("Cancel"), 50 | onPressed: () { 51 | Navigator.of(context).pop(); 52 | }, 53 | ), 54 | TextButton( 55 | child: const Text("Save", style: TextStyle(color: pink)), 56 | onPressed: (() async { 57 | if (_formKey.currentState!.validate()) { 58 | _formKey.currentState!.save(); 59 | final clientId = newDeviceController.text; 60 | 61 | await con.createDevice(clientId, "mobile"); 62 | onClientRegistered(clientId); 63 | 64 | Navigator.of(context).pop(); 65 | } 66 | })), 67 | ], 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/settings/view/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/view.dart'; 2 | import 'package:iot_center_flutter_mvc/src/controller.dart'; 3 | 4 | class SettingsPage extends StatefulWidget { 5 | const SettingsPage({Key? key}) : super(key: key); 6 | 7 | @override 8 | _SettingsPageState createState() { 9 | return _SettingsPageState(); 10 | } 11 | } 12 | 13 | class _SettingsPageState extends StateMVC { 14 | late SettingsPageController con; 15 | 16 | _SettingsPageState() : super(SettingsPageController()) { 17 | con = controller as SettingsPageController; 18 | } 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | add(con); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | extendBodyBehindAppBar: false, 30 | backgroundColor: lightGrey, 31 | appBar: AppBar( 32 | title: const Text('Settings'), 33 | backgroundColor: darkBlue, 34 | actions: [ 35 | Visibility( 36 | visible: con.selectedIndex == 0, 37 | child: IconButton( 38 | icon: const Icon(Icons.add), 39 | color: Colors.white, 40 | onPressed: (() { 41 | showDialog( 42 | context: context, 43 | builder: (BuildContext context) { 44 | return con.newDashboardDialog(context); 45 | }, 46 | ); 47 | })), 48 | ), 49 | Visibility( 50 | visible: con.selectedIndex == 0, 51 | child: IconButton( 52 | icon: const Icon(Icons.refresh), 53 | color: Colors.white, 54 | onPressed: () { 55 | con.refreshDashboards(); 56 | }, 57 | ), 58 | ), 59 | Visibility( 60 | visible: con.selectedIndex == 2, 61 | child: IconButton( 62 | icon: Icon(con.settingsReadonly 63 | ? Icons.lock_outline_rounded 64 | : Icons.lock_open), 65 | color: Colors.white, 66 | onPressed: () { 67 | con.changeReadonly(); 68 | }, 69 | ), 70 | ), 71 | ], 72 | ), 73 | bottomNavigationBar: BottomNavigationBar( 74 | currentIndex: con.selectedIndex, 75 | backgroundColor: Colors.white, 76 | selectedItemColor: pink, 77 | unselectedItemColor: darkBlue, 78 | selectedFontSize: 12, 79 | unselectedFontSize: 12, 80 | items: const [ 81 | BottomNavigationBarItem( 82 | icon: Icon(Icons.dashboard_outlined), 83 | label: 'Dashboards', 84 | ), 85 | BottomNavigationBarItem( 86 | icon: Icon(Icons.sensors), 87 | label: 'Sensors', 88 | ), 89 | BottomNavigationBarItem( 90 | icon: Icon(Icons.cloud_outlined), 91 | label: 'Influx settings', 92 | ), 93 | ], 94 | onTap: con.bottomMenuOnTap, 95 | ), 96 | body: 97 | Padding(padding: const EdgeInsets.all(10), child: con.actualTab!)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/settings/view/tabs/dashboards_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:iot_center_flutter_mvc/src/controller.dart'; 2 | import 'package:iot_center_flutter_mvc/src/view.dart'; 3 | 4 | class DashboardsTab extends StatefulWidget { 5 | const DashboardsTab({ 6 | required this.con, 7 | Key? key, 8 | }) : super(key: key); 9 | 10 | final SettingsPageController con; 11 | 12 | @override 13 | State createState() { 14 | return _DashboardsTabState(); 15 | } 16 | } 17 | 18 | class _DashboardsTabState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | return FutureBuilder( 22 | future: widget.con.dashboardList, 23 | builder: (context, AsyncSnapshot dashboardRecords) { 24 | if (dashboardRecords.hasData && 25 | dashboardRecords.connectionState == ConnectionState.done) { 26 | return ListView.builder( 27 | itemCount: dashboardRecords.data.length, 28 | itemBuilder: (_, index) { 29 | var devicesList = widget.con 30 | .deviceList(dashboardRecords.data[index]['dashboardKey'] ?? ""); 31 | 32 | return Padding( 33 | padding: const EdgeInsets.all(5.0), 34 | child: Container( 35 | decoration: boxDecor, 36 | child: Padding( 37 | padding: const EdgeInsets.symmetric( 38 | vertical: 10.0, horizontal: 10), 39 | child: Column( 40 | children: [ 41 | Row( 42 | children: [ 43 | const Padding( 44 | padding: EdgeInsets.only(right: 10.0), 45 | child: Icon( 46 | Icons.dashboard_outlined, 47 | color: Colors.grey, 48 | ), 49 | ), 50 | Expanded( 51 | child: Column( 52 | crossAxisAlignment: 53 | CrossAxisAlignment.start, 54 | children: [ 55 | Text( 56 | 'Dashboard key: ${dashboardRecords.data[index]['dashboardKey']}', 57 | style: const TextStyle( 58 | color: darkBlue, 59 | fontWeight: FontWeight.w500), 60 | ), 61 | ], 62 | ), 63 | ), 64 | IconButton( 65 | icon: const Icon( 66 | Icons.delete_outline, 67 | color: pink, 68 | ), 69 | onPressed: () { 70 | showDialog( 71 | context: context, 72 | builder: (BuildContext context) { 73 | return widget.con 74 | .removeDashboardDialog( 75 | context, 76 | dashboardRecords.data[index] 77 | ['dashboardKey']); 78 | }, 79 | ); 80 | }), 81 | ], 82 | ), 83 | const Padding( 84 | padding: EdgeInsets.symmetric(horizontal: 8.0), 85 | child: Divider( 86 | color: Colors.black, 87 | height: 36, 88 | ), 89 | ), 90 | Row( 91 | children: [ 92 | Expanded( 93 | flex: 2, 94 | child: Padding( 95 | padding: 96 | const EdgeInsets.only(bottom: 10.0), 97 | child: FutureBuilder( 98 | future: devicesList, 99 | builder: (context, 100 | AsyncSnapshot 101 | devicesRecord) { 102 | if (devicesRecord.hasData && 103 | devicesRecord.connectionState == 104 | ConnectionState.done) { 105 | List deviceRows = []; 106 | for (var device 107 | in devicesRecord.data) { 108 | deviceRows.add(Row( 109 | children: [ 110 | const Padding( 111 | padding: EdgeInsets.only( 112 | right: 10.0), 113 | child: Icon( 114 | Icons.thermostat_outlined, 115 | color: Colors.grey, 116 | ), 117 | ), 118 | Text( 119 | device['deviceId'], 120 | style: const TextStyle( 121 | color: darkBlue, 122 | fontWeight: 123 | FontWeight.w500), 124 | ), 125 | ], 126 | )); 127 | } 128 | 129 | if (deviceRows.isEmpty) { 130 | deviceRows.add(Row( 131 | children: const [ 132 | Padding( 133 | padding: EdgeInsets.only( 134 | right: 10.0), 135 | child: Icon( 136 | Icons.cancel_outlined, 137 | color: Colors.grey, 138 | ), 139 | ), 140 | Text( 141 | 'No devices', 142 | style: TextStyle( 143 | color: darkBlue, 144 | fontWeight: 145 | FontWeight.w500), 146 | ), 147 | ], 148 | )); 149 | } 150 | 151 | return Column( 152 | children: deviceRows, 153 | ); 154 | } 155 | return const SizedBox( 156 | width: 20, 157 | height: 20, 158 | child: CircularProgressIndicator( 159 | color: pink, 160 | strokeWidth: 3, 161 | ), 162 | ); 163 | }), 164 | ), 165 | ) 166 | ], 167 | ) 168 | ], 169 | ), 170 | ), 171 | ), 172 | ); 173 | }); 174 | } else { 175 | return const SizedBox( 176 | width: 20, 177 | height: 20, 178 | child: CircularProgressIndicator( 179 | color: pink, 180 | strokeWidth: 3, 181 | ), 182 | ); 183 | } 184 | }); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /lib/src/settings/view/tabs/influx_settings_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:influxdb_client/api.dart'; 2 | import 'package:iot_center_flutter_mvc/src/controller.dart'; 3 | 4 | import 'package:iot_center_flutter_mvc/src/view.dart'; 5 | 6 | class InfluxSettingsTab extends StatefulWidget { 7 | const InfluxSettingsTab({ required this.con, Key? key}) : super(key: key); 8 | 9 | final SettingsPageController con; 10 | 11 | @override 12 | _InfluxSettingsTabState createState() { 13 | return _InfluxSettingsTabState(); 14 | } 15 | } 16 | 17 | class _InfluxSettingsTabState extends StateMVC { 18 | final _formKey = GlobalKey(); 19 | 20 | late TextEditingController urlController; 21 | late TextEditingController tokenController; 22 | late TextEditingController orgController; 23 | late TextEditingController bucketController; 24 | 25 | late InfluxDBClient client; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | client = widget.con.client; 31 | 32 | urlController = TextEditingController(text: client.url); 33 | tokenController = TextEditingController(text: client.token); 34 | orgController = TextEditingController(text: client.org); 35 | bucketController = TextEditingController(text: client.bucket); 36 | } 37 | 38 | @override 39 | Widget buildWidget(BuildContext context) { 40 | return Form( 41 | key: _formKey, 42 | child: ListView( 43 | children: [ 44 | TextBoxRow( 45 | readOnly: widget.con.settingsReadonly, 46 | label: "Url:", 47 | controller: urlController, 48 | onSaved: (value) { 49 | client.url = value!; 50 | }, 51 | ), 52 | TextBoxRow( 53 | readOnly: widget.con.settingsReadonly, 54 | label: "Token:", 55 | controller: tokenController, 56 | onSaved: (value) { 57 | client.token = value!; 58 | }, 59 | ), 60 | TextBoxRow( 61 | readOnly: widget.con.settingsReadonly, 62 | label: "Org:", 63 | controller: orgController, 64 | onSaved: (value) { 65 | client.org = value!; 66 | }, 67 | ), 68 | TextBoxRow( 69 | readOnly: widget.con.settingsReadonly, 70 | label: "Bucket:", 71 | controller: bucketController, 72 | onSaved: (value) { 73 | client.bucket = value!; 74 | }, 75 | ), 76 | 77 | 78 | Visibility( 79 | visible: !widget.con.settingsReadonly, 80 | child: Padding( 81 | padding: 82 | const EdgeInsets.symmetric(vertical: 35, horizontal: 3), 83 | child: FormButton( 84 | label: 'Save', 85 | onPressed: () { 86 | // Validate returns true if the form is valid, or false otherwise. 87 | if (_formKey.currentState!.validate()) { 88 | _formKey.currentState!.save(); 89 | 90 | widget.con.checkClient(client); 91 | 92 | } 93 | }), 94 | ), 95 | ), 96 | ], 97 | ), 98 | ); 99 | } 100 | } -------------------------------------------------------------------------------- /lib/src/settings/view/tabs/sensors_tab.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:iot_center_flutter_mvc/src/settings/view/clientId_dialog.dart'; 4 | import 'package:iot_center_flutter_mvc/src/view.dart'; 5 | import 'package:iot_center_flutter_mvc/src/controller.dart'; 6 | 7 | class SensorsTab extends StatefulWidget { 8 | const SensorsTab({Key? key, required this.con}) : super(key: key); 9 | 10 | final SettingsPageController con; 11 | 12 | @override 13 | _SensorsTabState createState() { 14 | return _SensorsTabState(); 15 | } 16 | } 17 | 18 | class _SensorsTabState extends StateMVC { 19 | final AppController appController = AppController(); 20 | late final SensorsSubscriptionManager subscriptionManager; 21 | late final List sensors; 22 | bool clientRegistered = false; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | add(appController); 28 | add(widget.con); 29 | subscriptionManager = appController.sensorsSubscriptionManager; 30 | sensors = appController.sensors; 31 | } 32 | 33 | void onData(SensorMeasurement measure, SensorInfo sensor) { 34 | widget.con.writeSensor( 35 | SensorsSubscriptionManager.addNameToMeasure(sensor, measure)); 36 | setState(() {}); 37 | } 38 | 39 | void Function(bool value) onSensorSwitchChanged(SensorInfo sensor) => 40 | (bool value) async { 41 | if (!clientRegistered) { 42 | final list = await widget.con.deviceList(); 43 | if (list.any( 44 | (element) => element['deviceId'] == appController.clientId)) { 45 | clientRegistered = true; 46 | } else { 47 | showDialog( 48 | context: context, 49 | builder: (BuildContext context) { 50 | return ClientIdDialog( 51 | currentClientId: appController.clientId, 52 | con: widget.con, 53 | onClientRegistered: (clientId) { 54 | appController.clientId = clientId; 55 | clientRegistered = true; 56 | }, 57 | ); 58 | }, 59 | ); 60 | return; 61 | } 62 | } 63 | 64 | if (value) { 65 | await subscriptionManager.trySubscribe(sensor, onData); 66 | } else { 67 | subscriptionManager.unsubscribe(sensor); 68 | } 69 | setState(() {}); 70 | }; 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | createSensorSwitchListTile(SensorInfo sensor) => SwitchListTile( 75 | title: Column( 76 | crossAxisAlignment: CrossAxisAlignment.start, 77 | children: [ 78 | Text(sensor.name), 79 | Text( 80 | subscriptionManager 81 | .lastValueOf(sensor) 82 | .entries 83 | .map((entry) => 84 | "${entry.key}=${entry.value.toStringAsFixed(2)}") 85 | .join(" "), 86 | style: const TextStyle( 87 | fontFeatures: [FontFeature.tabularFigures()], 88 | )), 89 | ], 90 | ), 91 | value: subscriptionManager.isSubscribed(sensor), 92 | onChanged: (sensor.availeble || sensor.requestPermission != null) 93 | ? onSensorSwitchChanged(sensor) 94 | : null, 95 | ); 96 | 97 | final sensorsListView = Scrollbar( 98 | isAlwaysShown: true, 99 | child: ListView( 100 | children: sensors.map(createSensorSwitchListTile).toList(), 101 | ), 102 | ); 103 | 104 | return Column( 105 | children: [ 106 | Container( 107 | padding: const EdgeInsets.all(8), 108 | child: Text("clientId: " + appController.clientId), 109 | ), 110 | Expanded(child: sensorsListView) 111 | ], 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/src/view.dart: -------------------------------------------------------------------------------- 1 | export 'package:flutter/material.dart' hide StateSetter; 2 | 3 | export 'package:mvc_pattern/mvc_pattern.dart'; 4 | 5 | export 'package:iot_center_flutter_mvc/src/app/view/my_app.dart'; 6 | 7 | export 'package:iot_center_flutter_mvc/src/settings/view/settings_page.dart'; 8 | 9 | export 'package:iot_center_flutter_mvc/src/device/view/chart_detail_page.dart'; 10 | 11 | 12 | 13 | 14 | export 'package:iot_center_flutter_mvc/src/device/view/gauge_chart.dart'; 15 | 16 | export 'package:iot_center_flutter_mvc/src/device/view/simple_chart.dart'; 17 | 18 | 19 | 20 | 21 | export 'package:iot_center_flutter_mvc/src/app/view/common/drop_down_list.dart'; 22 | 23 | export 'package:iot_center_flutter_mvc/src/app/view/common/number_text_field.dart'; 24 | 25 | export 'package:iot_center_flutter_mvc/src/app/view/common/form_row.dart'; 26 | 27 | export 'package:iot_center_flutter_mvc/src/app/view/common/form_button.dart'; 28 | 29 | export 'package:iot_center_flutter_mvc/src/app/view/common/styles.dart'; 30 | 31 | 32 | 33 | 34 | export 'package:iot_center_flutter_mvc/src/device/view/device_detail_page.dart'; 35 | 36 | export 'package:iot_center_flutter_mvc/src/settings/view/tabs/dashboards_tab.dart'; 37 | 38 | export 'package:iot_center_flutter_mvc/src/settings/view/tabs/influx_settings_tab.dart'; 39 | 40 | export 'package:iot_center_flutter_mvc/src/settings/view/tabs/sensors_tab.dart'; 41 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: iot_center_flutter_mvc 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.16.1 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | mvc_pattern: ^8.0.0 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.2 37 | influxdb_client: 38 | git: 39 | url: https://github.com/influxdata/influxdb-client-dart 40 | ref: main 41 | shared_preferences: ^2.0.8 42 | charts_flutter: ^0.12.0 43 | sensors_plus: ^1.3.2 44 | battery_plus: ^2.1.4 45 | environment_sensors: ^0.2.0 46 | geolocator: ^8.2.1 47 | flutter_launcher_icons: ^0.9.2 48 | 49 | flutter_icons: 50 | android: "launcher_icon" 51 | ios: true 52 | image_path: "assets/icons/launcher_icon.png" 53 | 54 | dev_dependencies: 55 | flutter_test: 56 | sdk: flutter 57 | 58 | # The "flutter_lints" package below contains a set of recommended lints to 59 | # encourage good coding practices. The lint set provided by the package is 60 | # activated in the `analysis_options.yaml` file located at the root of your 61 | # package. See that file for information about deactivating specific lint 62 | # rules and activating additional ones. 63 | flutter_lints: ^1.0.0 64 | 65 | # For information on the generic Dart part of this file, see the 66 | # following page: https://dart.dev/tools/pub/pubspec 67 | 68 | # The following section is specific to Flutter. 69 | flutter: 70 | 71 | # The following line ensures that the Material Icons font is 72 | # included with your application, so that you can use the icons in 73 | # the material Icons class. 74 | uses-material-design: true 75 | 76 | # To add assets to your application, add an assets section, like this: 77 | # assets: 78 | # - images/a_dot_burr.jpeg 79 | # - images/a_dot_ham.jpeg 80 | 81 | # An image asset can refer to one or more resolution-specific "variants", see 82 | # https://flutter.dev/assets-and-images/#resolution-aware. 83 | 84 | # For details regarding adding assets from package dependencies, see 85 | # https://flutter.dev/assets-and-images/#from-packages 86 | 87 | # To add custom fonts to your application, add a fonts section here, 88 | # in this "flutter" section. Each entry in this list should have a 89 | # "family" key with the font family name, and a "fonts" key with a 90 | # list giving the asset and other descriptors for the font. For 91 | # example: 92 | # fonts: 93 | # - family: Schyler 94 | # fonts: 95 | # - asset: fonts/Schyler-Regular.ttf 96 | # - asset: fonts/Schyler-Italic.ttf 97 | # style: italic 98 | # - family: Trajan Pro 99 | # fonts: 100 | # - asset: fonts/TrajanPro.ttf 101 | # - asset: fonts/TrajanPro_Bold.ttf 102 | # weight: 700 103 | # 104 | # For details regarding fonts from package dependencies, 105 | # see https://flutter.dev/custom-fonts/#from-packages 106 | 107 | assets: 108 | - assets/images/ -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // // This is a basic Flutter widget test. 2 | // // 3 | // // To perform an interaction with a widget in your test, use the WidgetTester 4 | // // utility that Flutter provides. For example, you can send tap and scroll 5 | // // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // // tree, read text, and verify that the values of widget properties are correct. 7 | // 8 | // import 'package:flutter/material.dart'; 9 | // import 'package:flutter_test/flutter_test.dart'; 10 | // 11 | // import 'package:iot_center_flutter_mvc/main.dart'; 12 | // 13 | // void main() { 14 | // testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // // Build our app and trigger a frame. 16 | // await tester.pumpWidget(const MyApp()); 17 | // 18 | // // Verify that our counter starts at 0. 19 | // expect(find.text('0'), findsOneWidget); 20 | // expect(find.text('1'), findsNothing); 21 | // 22 | // // Tap the '+' icon and trigger a frame. 23 | // await tester.tap(find.byIcon(Icons.add)); 24 | // await tester.pump(); 25 | // 26 | // // Verify that our counter has incremented. 27 | // expect(find.text('0'), findsNothing); 28 | // expect(find.text('1'), findsOneWidget); 29 | // }); 30 | // } 31 | --------------------------------------------------------------------------------