├── .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 |
--------------------------------------------------------------------------------