Compare commits
157 Commits
Author | SHA1 | Date | |
---|---|---|---|
56a333a852 | |||
c5922368de | |||
8c2316a51a | |||
e2e6c015de | |||
0a6ff4586d | |||
fc228d85ae | |||
61823cb43b | |||
127e0b8182 | |||
38c37fa212 | |||
dfaf2a2924 | |||
c90c40c046 | |||
d2049b726a | |||
6508f109f7 | |||
37e63637a7 | |||
6650c5c145 | |||
9160dbf7f2 | |||
243fcd7c49 | |||
c114bcfb35 | |||
83defb08f1 | |||
57ebdbbe85 | |||
c6aceed623 | |||
ba4c88ec5d | |||
ee1685e981 | |||
996fbf7bba | |||
56cd8963d7 | |||
5759aad0cb | |||
02717332f7 | |||
8d1b159f56 | |||
fb335e1100 | |||
5f0bc83d67 | |||
6a8cee2cc2 | |||
0d2f1cf9aa | |||
8efeb3da8a | |||
620aa3b8d8 | |||
ab5bf3b807 | |||
6663bcad72 | |||
113cd29f74 | |||
f2fdfb0a32 | |||
691e48a36b | |||
2036cc117f | |||
389d28a1e1 | |||
27e6198d83 | |||
de762a4878 | |||
e8efefe25d | |||
21f3e8985a | |||
622543d405 | |||
abdc0fc1c8 | |||
1ecb839042 | |||
cece4d1e16 | |||
623634cb6e | |||
f9c37f5084 | |||
3e12f4f8a4 | |||
b07ff6fe71 | |||
5a3b57c28e | |||
e858eee83b | |||
73f00d3bd7 | |||
eea59cf11b | |||
61b459ed8a | |||
dca8c309aa | |||
be53500104 | |||
bc1a791608 | |||
b112ff980a | |||
7beab9ae93 | |||
8c0d1f90a3 | |||
05c05ba768 | |||
67e885e76a | |||
594bce0b8d | |||
7f6569e0db | |||
1c829c8364 | |||
7ca4b02e6d | |||
fadfefd836 | |||
37155901ef | |||
fbbb96409d | |||
5126c54914 | |||
916d0b7e3c | |||
0815840a9c | |||
bc237796b2 | |||
7f44800f64 | |||
85ac746e9d | |||
8215175098 | |||
39ee8b1799 | |||
c76d3d68c8 | |||
cde257922b | |||
be0c9d3372 | |||
66cd7ea307 | |||
b704ce6984 | |||
247c856a41 | |||
9afaebfa12 | |||
929abea5d3 | |||
13102a6b04 | |||
57c3083f9f | |||
5c31ddd00f | |||
8f55be187d | |||
1fe82d8b0d | |||
cbc56a8105 | |||
b63cddfa46 | |||
91db82f730 | |||
0c4d1b78ff | |||
5af2fd0562 | |||
2375543ebf | |||
de187f3ed5 | |||
9266ffacf3 | |||
3c0ca5d16d | |||
caabf25260 | |||
0af2afbb80 | |||
12d226509d | |||
3417c38426 | |||
c7fc5afbb8 | |||
11f565a9dc | |||
53240faac3 | |||
95d4878785 | |||
ef15026203 | |||
ad6355503b | |||
491c2b0dc0 | |||
5b99ade088 | |||
e1d9d9f304 | |||
209ccd4f7f | |||
5a8a207f2e | |||
19c85d9c16 | |||
a916ddfa50 | |||
8c1ad9c7f9 | |||
93af1eca7e | |||
cabf836fa3 | |||
15b3d31a6f | |||
9b98689012 | |||
84ebd0c33c | |||
ccd7774931 | |||
b2773635f5 | |||
8b046b7313 | |||
885a516676 | |||
921b0e09b0 | |||
277c67fc6f | |||
2a01ff8a03 | |||
b246b7bc1d | |||
e1868b9a14 | |||
125f3ac16c | |||
be502b5668 | |||
6f33fdca9f | |||
a7cda2a35e | |||
102b10ade0 | |||
4e96b9adbb | |||
b9581d3762 | |||
7c010359c3 | |||
4a75243994 | |||
d29d7e5b3b | |||
5ebd25e0d1 | |||
b7d5a53e86 | |||
20d3498bfd | |||
67d7bb45f5 | |||
6a03105d01 | |||
5ae580ecf1 | |||
0efef33e53 | |||
ccb88884a7 | |||
d70ba0a55a | |||
5140840d3a | |||
14759fd3c9 | |||
fed35be517 |
2
.gitignore
vendored
@ -11,3 +11,5 @@ build/
|
||||
.idea/
|
||||
|
||||
key.properties
|
||||
premium_features_manager.class.dart
|
||||
pubspec.lock
|
@ -1,12 +1,9 @@
|
||||
[](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart)
|
||||
# HA Client
|
||||
## Native Android client for Home Assistant
|
||||
### With Lovelace UI support
|
||||
|
||||
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
||||
|
||||
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
|
||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||
|
||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
|
||||
|
||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)
|
||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn)
|
||||
|
@ -70,7 +70,10 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-core:16.0.8'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
42
android/app/google-services.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "441874387819",
|
||||
"firebase_url": "https://ha-client-c73c4.firebaseio.com",
|
||||
"project_id": "ha-client-c73c4",
|
||||
"storage_bucket": "ha-client-c73c4.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45",
|
||||
"android_client_info": {
|
||||
"package_name": "com.keyboardcrumbs.haclient"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
@ -6,6 +6,11 @@
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
@ -13,9 +18,15 @@
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:name=".Application"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="ha_notify" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
@ -26,14 +37,33 @@
|
||||
<!-- This keeps the window background of the activity showing
|
||||
until Flutter renders its first frame. It can be removed if
|
||||
there is no splash screen (such as the default splash screen
|
||||
defined in @style/LaunchTheme). -->
|
||||
defined in @style/LaunchTheme).
|
||||
<meta-data
|
||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||
android:value="true" />
|
||||
android:value="true" />-->
|
||||
<intent-filter>
|
||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="false"/>
|
||||
<receiver
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
|
||||
android:exported="false"/>
|
||||
<receiver
|
||||
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
@ -0,0 +1,18 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import io.flutter.app.FlutterApplication;
|
||||
import io.flutter.plugin.common.PluginRegistry;
|
||||
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
public class Application extends FlutterApplication implements PluginRegistrantCallback {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWith(PluginRegistry registry) {
|
||||
GeneratedPluginRegistrant.registerWith(registry);
|
||||
}
|
||||
}
|
@ -3,8 +3,9 @@ package com.keyboardcrumbs.hassclient;
|
||||
import android.os.Bundle;
|
||||
import io.flutter.app.FlutterActivity;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
import io.flutter.plugins.share.FlutterShareReceiverActivity;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
public class MainActivity extends FlutterShareReceiverActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
BIN
android/app/src/main/res/drawable/mini_icon.png
Normal file
After Width: | Height: | Size: 612 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 11 KiB |
@ -5,7 +5,8 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.1.2'
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx2g
|
||||
org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=false
|
||||
android.enableJetifier=true
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||
|
1232
android/hs_err_pid766.log
Normal file
16
assets/js/externalAuth.js
Normal file
@ -0,0 +1,16 @@
|
||||
window.externalApp = {};
|
||||
window.externalApp.getExternalAuth = function(options) {
|
||||
console.log("Starting external auth");
|
||||
var options = JSON.parse(options);
|
||||
if (options && options.callback) {
|
||||
var responseData = {
|
||||
access_token: "[token]",
|
||||
expires_in: 1800
|
||||
};
|
||||
console.log("Waiting for callback to be added");
|
||||
setTimeout(function(){
|
||||
console.log("Calling a callback");
|
||||
window[options.callback](true, responseData);
|
||||
}, 500);
|
||||
}
|
||||
};
|
41
flutter_01.log
Normal file
@ -0,0 +1,41 @@
|
||||
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||
|
||||
## command
|
||||
|
||||
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||
|
||||
## exception
|
||||
|
||||
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/.packages, isolateId: isolates/68989666}}, details: Isolate must be runnable before this request is made.}}
|
||||
|
||||
```
|
||||
null```
|
||||
|
||||
## flutter doctor
|
||||
|
||||
```
|
||||
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||
• Engine revision fee001c93f
|
||||
• Dart version 2.4.0
|
||||
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||
• Android SDK at /home/estevez/Android/Sdk
|
||||
• Android NDK location not configured (optional; useful for native profiling support)
|
||||
• Platform android-29, build-tools 29.0.2
|
||||
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
• All Android licenses accepted.
|
||||
|
||||
[✓] Android Studio (version 3.5)
|
||||
• Android Studio at /home/estevez/bin/android-studio
|
||||
• Flutter plugin version 38.2.3
|
||||
• Dart plugin version 191.8423
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
|
||||
[✓] Connected device (1 available)
|
||||
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||
|
||||
• No issues found!
|
||||
```
|
41
flutter_02.log
Normal file
@ -0,0 +1,41 @@
|
||||
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||
|
||||
## command
|
||||
|
||||
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||
|
||||
## exception
|
||||
|
||||
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/.packages, isolateId: isolates/289688365}}, details: Isolate must be runnable before this request is made.}}
|
||||
|
||||
```
|
||||
null```
|
||||
|
||||
## flutter doctor
|
||||
|
||||
```
|
||||
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||
• Engine revision fee001c93f
|
||||
• Dart version 2.4.0
|
||||
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||
• Android SDK at /home/estevez/Android/Sdk
|
||||
• Android NDK location not configured (optional; useful for native profiling support)
|
||||
• Platform android-29, build-tools 29.0.2
|
||||
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
• All Android licenses accepted.
|
||||
|
||||
[✓] Android Studio (version 3.5)
|
||||
• Android Studio at /home/estevez/bin/android-studio
|
||||
• Flutter plugin version 38.2.3
|
||||
• Dart plugin version 191.8423
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
|
||||
[✓] Connected device (1 available)
|
||||
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||
|
||||
• No issues found!
|
||||
```
|
41
flutter_03.log
Normal file
@ -0,0 +1,41 @@
|
||||
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||
|
||||
## command
|
||||
|
||||
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||
|
||||
## exception
|
||||
|
||||
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/.packages, isolateId: isolates/866521062}}, details: Isolate must be runnable before this request is made.}}
|
||||
|
||||
```
|
||||
null```
|
||||
|
||||
## flutter doctor
|
||||
|
||||
```
|
||||
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||
• Engine revision fee001c93f
|
||||
• Dart version 2.4.0
|
||||
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||
• Android SDK at /home/estevez/Android/Sdk
|
||||
• Android NDK location not configured (optional; useful for native profiling support)
|
||||
• Platform android-29, build-tools 29.0.2
|
||||
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
• All Android licenses accepted.
|
||||
|
||||
[✓] Android Studio (version 3.5)
|
||||
• Android Studio at /home/estevez/bin/android-studio
|
||||
• Flutter plugin version 38.2.3
|
||||
• Dart plugin version 191.8423
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
|
||||
[✓] Connected device (1 available)
|
||||
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||
|
||||
• No issues found!
|
||||
```
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 24 KiB |
@ -13,7 +13,12 @@ class HACard {
|
||||
int columnsCount;
|
||||
List stateFilter;
|
||||
List states;
|
||||
List conditions;
|
||||
String content;
|
||||
String unit;
|
||||
int min;
|
||||
int max;
|
||||
Map severity;
|
||||
|
||||
HACard({
|
||||
this.name,
|
||||
@ -26,8 +31,17 @@ class HACard {
|
||||
this.showEmpty: true,
|
||||
this.content,
|
||||
this.states,
|
||||
this.conditions: const [],
|
||||
this.unit,
|
||||
this.min,
|
||||
this.max,
|
||||
this.severity,
|
||||
@required this.type
|
||||
});
|
||||
}) {
|
||||
if (this.columnsCount <= 0) {
|
||||
this.columnsCount = 4;
|
||||
}
|
||||
}
|
||||
|
||||
List<EntityWrapper> getEntitiesToShow() {
|
||||
return entities.where((entityWrapper) {
|
@ -24,33 +24,58 @@ class CardWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
if (card.conditions.isNotEmpty) {
|
||||
bool showCardByConditions = true;
|
||||
for (var condition in card.conditions) {
|
||||
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
||||
if (conditionEntity != null &&
|
||||
((condition['state'] != null && conditionEntity.state != condition['state']) ||
|
||||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
|
||||
) {
|
||||
showCardByConditions = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!showCardByConditions) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
switch (card.type) {
|
||||
|
||||
case CardType.entities: {
|
||||
case CardType.ENTITIES: {
|
||||
return _buildEntitiesCard(context);
|
||||
}
|
||||
|
||||
case CardType.glance: {
|
||||
case CardType.GLANCE: {
|
||||
return _buildGlanceCard(context);
|
||||
}
|
||||
|
||||
case CardType.mediaControl: {
|
||||
case CardType.MEDIA_CONTROL: {
|
||||
return _buildMediaControlsCard(context);
|
||||
}
|
||||
|
||||
case CardType.entityButton: {
|
||||
case CardType.ENTITY_BUTTON: {
|
||||
return _buildEntityButtonCard(context);
|
||||
}
|
||||
|
||||
case CardType.markdown: {
|
||||
case CardType.GAUGE: {
|
||||
return _buildGaugeCard(context);
|
||||
}
|
||||
|
||||
/* case CardType.LIGHT: {
|
||||
return _buildLightCard(context);
|
||||
}*/
|
||||
|
||||
case CardType.MARKDOWN: {
|
||||
return _buildMarkdownCard(context);
|
||||
}
|
||||
|
||||
case CardType.alarmPanel: {
|
||||
case CardType.ALARM_PANEL: {
|
||||
return _buildAlarmPanelCard(context);
|
||||
}
|
||||
|
||||
case CardType.horizontalStack: {
|
||||
case CardType.HORIZONTAL_STACK: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
@ -73,7 +98,7 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
case CardType.verticalStack: {
|
||||
case CardType.VERTICAL_STACK: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
@ -107,12 +132,12 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name));
|
||||
body.add(CardHeader(name: card.name));
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
if (!entity.entity.isHidden) {
|
||||
body.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(10.0, 4.0, 0.0, 4.0),
|
||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
handleTap: true,
|
||||
@ -122,7 +147,10 @@ class CardWidget extends StatelessWidget {
|
||||
}
|
||||
});
|
||||
return Card(
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -131,7 +159,7 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name));
|
||||
body.add(CardHeader(name: card.name));
|
||||
body.add(MarkdownBody(data: card.content));
|
||||
return Card(
|
||||
child: Padding(
|
||||
@ -143,7 +171,7 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(
|
||||
body.add(CardHeader(
|
||||
name: card.name ?? "",
|
||||
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||
style: TextStyle(
|
||||
@ -195,18 +223,26 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> rows = [];
|
||||
rows.add(CardHeaderWidget(name: card.name));
|
||||
rows.add(CardHeader(name: card.name));
|
||||
|
||||
List<Widget> result = [];
|
||||
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||
|
||||
rows.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
List<Widget> buttons = [];
|
||||
double buttonWidth = constraints.maxWidth / columnsCount;
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
result.add(
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1/columnsCount,
|
||||
buttons.add(
|
||||
SizedBox(
|
||||
width: buttonWidth,
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: GlanceEntityContainer(
|
||||
child: GlanceCardEntityContainer(
|
||||
showName: card.showName,
|
||||
showState: card.showState,
|
||||
),
|
||||
@ -215,19 +251,23 @@ class CardWidget extends StatelessWidget {
|
||||
)
|
||||
);
|
||||
});
|
||||
rows.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
|
||||
child: Wrap(
|
||||
//alignment: WrapAlignment.spaceAround,
|
||||
runSpacing: Sizes.rowPadding*2,
|
||||
children: result,
|
||||
return Wrap(
|
||||
//spacing: 5.0,
|
||||
//alignment: WrapAlignment.spaceEvenly,
|
||||
runSpacing: Sizes.doubleRowPadding,
|
||||
children: buttons,
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return Card(
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: rows
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -247,7 +287,41 @@ class CardWidget extends StatelessWidget {
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: ButtonEntityContainer(),
|
||||
child: EntityButtonCardBody(),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGaugeCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
|
||||
card.linkedEntityWrapper.unitOfMeasurement;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: GaugeCardBody(
|
||||
min: card.min,
|
||||
max: card.max,
|
||||
severity: card.severity,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLightCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: LightCardBody(
|
||||
min: card.min,
|
||||
max: card.max,
|
||||
severity: card.severity,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
@ -255,7 +329,7 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildUnsupportedCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name ?? ""));
|
||||
body.add(CardHeader(name: card.name ?? ""));
|
||||
List<Widget> result = [];
|
||||
if (card.linkedEntityWrapper != null) {
|
||||
result.addAll(<Widget>[
|
@ -1,12 +1,12 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class CardHeaderWidget extends StatelessWidget {
|
||||
class CardHeader extends StatelessWidget {
|
||||
|
||||
final String name;
|
||||
final Widget trailing;
|
||||
final Widget subtitle;
|
||||
|
||||
const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
@ -1,8 +1,8 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class ButtonEntityContainer extends StatelessWidget {
|
||||
class EntityButtonCardBody extends StatelessWidget {
|
||||
|
||||
ButtonEntityContainer({
|
||||
EntityButtonCardBody({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -15,25 +15,26 @@ class ButtonEntityContainer extends StatelessWidget {
|
||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitHeight,
|
||||
child: EntityIcon(
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
size: Sizes.iconSize,
|
||||
)
|
||||
),
|
||||
size: constraints.maxWidth / 2.5,
|
||||
);
|
||||
}
|
||||
),
|
||||
_buildName()
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
153
lib/cards/widgets/gauge_card_body.dart
Normal file
@ -0,0 +1,153 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class GaugeCardBody extends StatefulWidget {
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final Map severity;
|
||||
|
||||
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||
|
||||
@override
|
||||
_GaugeCardBodyState createState() => _GaugeCardBodyState();
|
||||
}
|
||||
|
||||
class _GaugeCardBodyState extends State<GaugeCardBody> {
|
||||
|
||||
List<charts.Series> seriesList;
|
||||
|
||||
List<charts.Series<GaugeSegment, String>> _createData(double value) {
|
||||
double fixedValue;
|
||||
if (value > widget.max) {
|
||||
fixedValue = widget.max.toDouble();
|
||||
} else if (value < widget.min) {
|
||||
fixedValue = widget.min.toDouble();
|
||||
} else {
|
||||
fixedValue = value;
|
||||
}
|
||||
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
|
||||
Color mainColor;
|
||||
if (widget.severity != null) {
|
||||
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
|
||||
mainColor = Colors.red;
|
||||
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
|
||||
mainColor = Colors.amber;
|
||||
} else {
|
||||
mainColor = Colors.green;
|
||||
}
|
||||
} else {
|
||||
mainColor = Colors.green;
|
||||
}
|
||||
final data = [
|
||||
GaugeSegment('Main', toShow, mainColor),
|
||||
GaugeSegment('Rest', 100 - toShow, Colors.black45),
|
||||
];
|
||||
|
||||
return [
|
||||
charts.Series<GaugeSegment, String>(
|
||||
id: 'Segments',
|
||||
domainFn: (GaugeSegment segment, _) => segment.segment,
|
||||
measureFn: (GaugeSegment segment, _) => segment.value,
|
||||
colorFn: (GaugeSegment segment, _) => segment.color,
|
||||
// Set a label accessor to control the text of the arc label.
|
||||
labelAccessorFn: (GaugeSegment segment, _) =>
|
||||
segment.segment == 'Main' ? '${segment.value}' : null,
|
||||
data: data,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
overflow: Overflow.clip,
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double verticalOffset;
|
||||
if(constraints.maxWidth > 150.0) {
|
||||
verticalOffset = 0.2;
|
||||
} else if (constraints.maxWidth > 100.0) {
|
||||
verticalOffset = 0.3;
|
||||
} else {
|
||||
verticalOffset = 0.3;
|
||||
}
|
||||
return FractionallySizedBox(
|
||||
heightFactor: 2,
|
||||
widthFactor: 1,
|
||||
alignment: FractionalOffset(0,verticalOffset),
|
||||
child: charts.PieChart(
|
||||
_createData(entityWrapper.entity.doubleState),
|
||||
animate: false,
|
||||
defaultRenderer: charts.ArcRendererConfig(
|
||||
arcRatio: 0.4,
|
||||
startAngle: pi,
|
||||
arcLength: pi,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||
child: SimpleEntityState(
|
||||
//textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
bold: true,
|
||||
textAlign: TextAlign.center,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
fontSize: fontSize,
|
||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: fontSize),
|
||||
child: EntityName(
|
||||
fontSize: fontSize,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
textAlign: TextAlign.center,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GaugeSegment {
|
||||
final String segment;
|
||||
final double value;
|
||||
final charts.Color color;
|
||||
|
||||
GaugeSegment(this.segment, this.value, Color color)
|
||||
: this.color = charts.Color(
|
||||
r: color.red, g: color.green, b: color.blue, a: color.alpha);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class GlanceEntityContainer extends StatelessWidget {
|
||||
class GlanceCardEntityContainer extends StatelessWidget {
|
||||
|
||||
final bool showName;
|
||||
final bool showState;
|
||||
@ -9,7 +9,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
final double nameFontSize;
|
||||
final bool wordsWrapInName;
|
||||
|
||||
GlanceEntityContainer({
|
||||
GlanceCardEntityContainer({
|
||||
Key key,
|
||||
@required this.showName,
|
||||
@required this.showState,
|
||||
@ -54,15 +54,10 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
|
||||
return Center(
|
||||
child: InkResponse(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
//mainAxisAlignment: MainAxisAlignment.start,
|
||||
//crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: result,
|
||||
),
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
),
|
90
lib/cards/widgets/light_card_body.dart
Normal file
@ -0,0 +1,90 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightCardBody extends StatefulWidget {
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final Map severity;
|
||||
|
||||
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LightCardBodyState createState() => _LightCardBodyState();
|
||||
}
|
||||
|
||||
class _LightCardBodyState extends State<LightCardBody> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
LightEntity entity = entityWrapper.entity;
|
||||
Logger.d("Light brightness: ${entity.brightness}");
|
||||
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: Container(
|
||||
//color: Colors.redAccent,
|
||||
child: SingleCircularSlider(
|
||||
255,
|
||||
entity.brightness ?? 0,
|
||||
baseColor: Colors.white,
|
||||
handlerColor: Colors.blue[200],
|
||||
selectionColor: Colors.blue[100],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
overflow: Overflow.clip,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||
child: SimpleEntityState(
|
||||
//textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
bold: true,
|
||||
textAlign: TextAlign.center,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
fontSize: fontSize,
|
||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: fontSize),
|
||||
child: EntityName(
|
||||
fontSize: fontSize,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
textAlign: TextAlign.center,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of 'main.dart';
|
||||
|
||||
class EntityState {
|
||||
static const on = 'on';
|
||||
@ -77,23 +77,40 @@ class EntityUIAction {
|
||||
}
|
||||
|
||||
class CardType {
|
||||
static const horizontalStack = "horizontal-stack";
|
||||
static const verticalStack = "vertical-stack";
|
||||
static const entities = "entities";
|
||||
static const glance = "glance";
|
||||
static const mediaControl = "media-control";
|
||||
static const weatherForecast = "weather-forecast";
|
||||
static const thermostat = "thermostat";
|
||||
static const sensor = "sensor";
|
||||
static const plantStatus = "plant-status";
|
||||
static const pictureEntity = "picture-entity";
|
||||
static const pictureElements = "picture-elements";
|
||||
static const picture = "picture";
|
||||
static const map = "map";
|
||||
static const iframe = "iframe";
|
||||
static const gauge = "gauge";
|
||||
static const entityButton = "entity-button";
|
||||
static const conditional = "conditional";
|
||||
static const alarmPanel = "alarm-panel";
|
||||
static const markdown = "markdown";
|
||||
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||
static const VERTICAL_STACK = "vertical-stack";
|
||||
static const ENTITIES = "entities";
|
||||
static const GLANCE = "glance";
|
||||
static const MEDIA_CONTROL = "media-control";
|
||||
static const WEATHER_FORECAST = "weather-forecast";
|
||||
static const THERMOSTAT = "thermostat";
|
||||
static const SENSOR = "sensor";
|
||||
static const PLANT_STATUS = "plant-status";
|
||||
static const PICTURE_ENTITY = "picture-entity";
|
||||
static const PICTURE_ELEMENTS = "picture-elements";
|
||||
static const PICTURE = "picture";
|
||||
static const MAP = "map";
|
||||
static const IFRAME = "iframe";
|
||||
static const GAUGE = "gauge";
|
||||
static const ENTITY_BUTTON = "entity-button";
|
||||
static const CONDITIONAL = "conditional";
|
||||
static const ALARM_PANEL = "alarm-panel";
|
||||
static const MARKDOWN = "markdown";
|
||||
static const LIGHT = "light";
|
||||
}
|
||||
|
||||
class Sizes {
|
||||
static const rightWidgetPadding = 10.0;
|
||||
static const leftWidgetPadding = 10.0;
|
||||
static const buttonPadding = 4.0;
|
||||
static const extendedWidgetHeight = 50.0;
|
||||
static const iconSize = 28.0;
|
||||
static const largeIconSize = 46.0;
|
||||
static const stateFontSize = 15.0;
|
||||
static const nameFontSize = 15.0;
|
||||
static const smallFontSize = 14.0;
|
||||
static const largeFontSize = 24.0;
|
||||
static const inputWidth = 160.0;
|
||||
static const rowPadding = 10.0;
|
||||
static const doubleRowPadding = rowPadding*2;
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class AlarmControlPanelEntity extends Entity {
|
||||
AlarmControlPanelEntity(Map rawData) : super(rawData);
|
||||
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class AlarmControlPanelControlsWidget extends StatefulWidget {
|
||||
|
@ -1,7 +1,8 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class AutomationEntity extends Entity {
|
||||
AutomationEntity(Map rawData) : super(rawData);
|
||||
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
@ -1,7 +1,8 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class ButtonEntity extends Entity {
|
||||
ButtonEntity(Map rawData) : super(rawData);
|
||||
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
@ -1,10 +1,10 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class CameraEntity extends Entity {
|
||||
|
||||
static const SUPPORT_ON_OFF = 1;
|
||||
|
||||
CameraEntity(Map rawData) : super(rawData);
|
||||
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportOnOff => ((supportedFeatures &
|
||||
CameraEntity.SUPPORT_ON_OFF) ==
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class ClimateEntity extends Entity {
|
||||
|
||||
@ -10,69 +10,57 @@ class ClimateEntity extends Entity {
|
||||
);
|
||||
|
||||
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
||||
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
|
||||
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
|
||||
static const SUPPORT_TARGET_HUMIDITY = 8;
|
||||
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
|
||||
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
|
||||
static const SUPPORT_FAN_MODE = 64;
|
||||
static const SUPPORT_OPERATION_MODE = 128;
|
||||
static const SUPPORT_HOLD_MODE = 256;
|
||||
static const SUPPORT_SWING_MODE = 512;
|
||||
static const SUPPORT_AWAY_MODE = 1024;
|
||||
static const SUPPORT_AUX_HEAT = 2048;
|
||||
static const SUPPORT_ON_OFF = 4096;
|
||||
static const SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
|
||||
static const SUPPORT_TARGET_HUMIDITY = 4;
|
||||
static const SUPPORT_FAN_MODE = 8;
|
||||
static const SUPPORT_PRESET_MODE = 16;
|
||||
static const SUPPORT_SWING_MODE = 32;
|
||||
static const SUPPORT_AUX_HEAT = 64;
|
||||
|
||||
|
||||
//static const SUPPORT_OPERATION_MODE = 16;
|
||||
//static const SUPPORT_HOLD_MODE = 256;
|
||||
//static const SUPPORT_AWAY_MODE = 1024;
|
||||
//static const SUPPORT_ON_OFF = 4096;
|
||||
|
||||
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportTargetTemperature => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
||||
bool get supportTargetTemperatureHigh => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
|
||||
bool get supportTargetTemperatureLow => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
|
||||
bool get supportTargetTemperatureRange => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE);
|
||||
bool get supportTargetHumidity => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
||||
bool get supportTargetHumidityHigh => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
|
||||
bool get supportTargetHumidityLow => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
|
||||
bool get supportFanMode =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||
ClimateEntity.SUPPORT_FAN_MODE);
|
||||
bool get supportOperationMode => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_OPERATION_MODE) ==
|
||||
ClimateEntity.SUPPORT_OPERATION_MODE);
|
||||
bool get supportHoldMode =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) ==
|
||||
ClimateEntity.SUPPORT_HOLD_MODE);
|
||||
bool get supportSwingMode =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||
ClimateEntity.SUPPORT_SWING_MODE);
|
||||
bool get supportAwayMode =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) ==
|
||||
ClimateEntity.SUPPORT_AWAY_MODE);
|
||||
bool get supportPresetMode =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
|
||||
ClimateEntity.SUPPORT_PRESET_MODE);
|
||||
bool get supportAuxHeat =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||
ClimateEntity.SUPPORT_AUX_HEAT);
|
||||
bool get supportOnOff =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) ==
|
||||
ClimateEntity.SUPPORT_ON_OFF);
|
||||
|
||||
List<String> get operationList => attributes["operation_list"] != null
|
||||
? (attributes["operation_list"] as List).cast<String>()
|
||||
List<String> get hvacModes => attributes["hvac_modes"] != null
|
||||
? (attributes["hvac_modes"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get fanList => attributes["fan_list"] != null
|
||||
? (attributes["fan_list"] as List).cast<String>()
|
||||
List<String> get fanModes => attributes["fan_modes"] != null
|
||||
? (attributes["fan_modes"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get swingList => attributes["swing_list"] != null
|
||||
? (attributes["swing_list"] as List).cast<String>()
|
||||
List<String> get presetModes => attributes["preset_modes"] != null
|
||||
? (attributes["preset_modes"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get swingModes => attributes["swing_modes"] != null
|
||||
? (attributes["swing_modes"] as List).cast<String>()
|
||||
: null;
|
||||
double get temperature => _getDoubleAttributeValue('temperature');
|
||||
double get currentTemperature => _getDoubleAttributeValue('current_temperature');
|
||||
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
||||
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
||||
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
||||
@ -81,25 +69,22 @@ class ClimateEntity extends Entity {
|
||||
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
|
||||
String get operationMode => attributes['operation_mode'];
|
||||
String get hvacAction => attributes['hvac_action'];
|
||||
String get fanMode => attributes['fan_mode'];
|
||||
String get presetMode => attributes['preset_mode'];
|
||||
String get swingMode => attributes['swing_mode'];
|
||||
bool get awayMode => attributes['away_mode'] == "on";
|
||||
bool get isOff => state == EntityState.off;
|
||||
//bool get isOff => state == EntityState.off;
|
||||
bool get auxHeat => attributes['aux_heat'] == "on";
|
||||
|
||||
ClimateEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
void update(Map rawData) {
|
||||
super.update(rawData);
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
if (supportTargetTemperature) {
|
||||
historyConfig.numericAttributesToShow.add("temperature");
|
||||
}
|
||||
if (supportTargetTemperatureHigh) {
|
||||
if (supportTargetTemperatureRange) {
|
||||
historyConfig.numericAttributesToShow.add("target_temp_high");
|
||||
}
|
||||
if (supportTargetTemperatureLow) {
|
||||
historyConfig.numericAttributesToShow.add("target_temp_low");
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class ClimateControlWidget extends StatefulWidget {
|
||||
|
||||
@ -19,22 +19,22 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
double _tmpTargetLow = 0.0;
|
||||
double _tmpTargetHigh = 0.0;
|
||||
double _tmpTargetHumidity = 0.0;
|
||||
String _tmpOperationMode;
|
||||
String _tmpHVACMode;
|
||||
String _tmpFanMode;
|
||||
String _tmpSwingMode;
|
||||
bool _tmpAwayMode = false;
|
||||
bool _tmpIsOff = false;
|
||||
String _tmpPresetMode;
|
||||
//bool _tmpIsOff = false;
|
||||
bool _tmpAuxHeat = false;
|
||||
|
||||
void _resetVars(ClimateEntity entity) {
|
||||
_tmpTemperature = entity.temperature;
|
||||
_tmpTargetHigh = entity.targetHigh;
|
||||
_tmpTargetLow = entity.targetLow;
|
||||
_tmpOperationMode = entity.operationMode;
|
||||
_tmpHVACMode = entity.state;
|
||||
_tmpFanMode = entity.fanMode;
|
||||
_tmpSwingMode = entity.swingMode;
|
||||
_tmpAwayMode = entity.awayMode;
|
||||
_tmpIsOff = entity.isOff;
|
||||
_tmpPresetMode = entity.presetMode;
|
||||
//_tmpIsOff = entity.isOff;
|
||||
_tmpAuxHeat = entity.auxHeat;
|
||||
_tmpTargetHumidity = entity.targetHumidity;
|
||||
|
||||
@ -116,11 +116,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
void _setOperationMode(ClimateEntity entity, value) {
|
||||
void _setHVACMode(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpOperationMode = value;
|
||||
_tmpHVACMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
@ -143,23 +143,23 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
void _setAwayMode(ClimateEntity entity, value) {
|
||||
void _setPresetMode(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpAwayMode = value;
|
||||
_tmpPresetMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _setOnOf(ClimateEntity entity, value) {
|
||||
/*void _setOnOf(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpIsOff = !value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
}*/
|
||||
|
||||
void _setAuxHeat(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
@ -196,33 +196,34 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_buildOnOffControl(entity),
|
||||
//_buildOnOffControl(entity),
|
||||
_buildTemperatureControls(entity),
|
||||
_buildTargetTemperatureControls(entity),
|
||||
_buildHumidityControls(entity),
|
||||
_buildOperationControl(entity),
|
||||
_buildFanControl(entity),
|
||||
_buildSwingControl(entity),
|
||||
_buildAwayModeControl(entity),
|
||||
_buildPresetModeControl(entity),
|
||||
_buildAuxHeatControl(entity)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAwayModeControl(ClimateEntity entity) {
|
||||
if (entity.supportAwayMode) {
|
||||
return ModeSwitchWidget(
|
||||
caption: "Away mode",
|
||||
onChange: (value) => _setAwayMode(entity, value),
|
||||
value: _tmpAwayMode,
|
||||
Widget _buildPresetModeControl(ClimateEntity entity) {
|
||||
if (entity.supportPresetMode) {
|
||||
return ModeSelectorWidget(
|
||||
options: entity.presetModes,
|
||||
onChange: (mode) => _setPresetMode(entity, mode),
|
||||
caption: "Preset",
|
||||
value: _tmpPresetMode,
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOnOffControl(ClimateEntity entity) {
|
||||
/*Widget _buildOnOffControl(ClimateEntity entity) {
|
||||
if (entity.supportOnOff) {
|
||||
return ModeSwitchWidget(
|
||||
onChange: (value) => _setOnOf(entity, value),
|
||||
@ -232,7 +233,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
||||
if (entity.supportAuxHeat ) {
|
||||
@ -247,12 +248,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
|
||||
Widget _buildOperationControl(ClimateEntity entity) {
|
||||
if (entity.supportOperationMode) {
|
||||
if (entity.hvacModes != null) {
|
||||
return ModeSelectorWidget(
|
||||
onChange: (mode) => _setOperationMode(entity, mode),
|
||||
options: entity.operationList,
|
||||
onChange: (mode) => _setHVACMode(entity, mode),
|
||||
options: entity.hvacModes,
|
||||
caption: "Operation",
|
||||
value: _tmpOperationMode,
|
||||
value: _tmpHVACMode,
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0);
|
||||
@ -262,7 +263,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
Widget _buildFanControl(ClimateEntity entity) {
|
||||
if (entity.supportFanMode) {
|
||||
return ModeSelectorWidget(
|
||||
options: entity.fanList,
|
||||
options: entity.fanModes,
|
||||
onChange: (mode) => _setFanMode(entity, mode),
|
||||
caption: "Fan mode",
|
||||
value: _tmpFanMode,
|
||||
@ -276,7 +277,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
if (entity.supportSwingMode) {
|
||||
return ModeSelectorWidget(
|
||||
onChange: (mode) => _setSwingMode(entity, mode),
|
||||
options: entity.swingList,
|
||||
options: entity.swingModes,
|
||||
value: _tmpSwingMode,
|
||||
caption: "Swing mode"
|
||||
);
|
||||
@ -308,7 +309,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
|
||||
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
||||
List<Widget> controls = [];
|
||||
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
|
||||
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
||||
controls.addAll(<Widget>[
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetLow,
|
||||
@ -321,7 +322,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
)
|
||||
]);
|
||||
}
|
||||
if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) {
|
||||
if ((entity.supportTargetTemperatureRange) && (entity.targetHigh != null)) {
|
||||
controls.add(
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetHigh,
|
||||
@ -408,52 +409,3 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TemperatureControlWidget extends StatelessWidget {
|
||||
final double value;
|
||||
final double fontSize;
|
||||
final Color fontColor;
|
||||
final onInc;
|
||||
final onDec;
|
||||
|
||||
TemperatureControlWidget(
|
||||
{Key key,
|
||||
@required this.value,
|
||||
@required this.onInc,
|
||||
@required this.onDec,
|
||||
this.fontSize,
|
||||
this.fontColor})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"$value",
|
||||
style: TextStyle(
|
||||
fontSize: fontSize ?? 24.0,
|
||||
color: fontColor ?? Colors.black
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
'mdi:chevron-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => onInc(),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
'mdi:chevron-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => onDec(),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class ClimateStateWidget extends StatelessWidget {
|
||||
@override
|
||||
@ -8,13 +8,19 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
String targetTemp = "-";
|
||||
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||
targetTemp = "${entity.temperature}";
|
||||
} else if ((entity.supportTargetTemperatureLow) &&
|
||||
(entity.targetLow != null)) {
|
||||
targetTemp = "${entity.targetLow}";
|
||||
if ((entity.supportTargetTemperatureHigh) &&
|
||||
} else if ((entity.supportTargetTemperatureRange) &&
|
||||
(entity.targetLow != null) &&
|
||||
(entity.targetHigh != null)) {
|
||||
targetTemp += " - ${entity.targetHigh}";
|
||||
targetTemp = "${entity.targetLow} - ${entity.targetHigh}";
|
||||
}
|
||||
String displayState = '';
|
||||
if (entity.hvacAction != null) {
|
||||
displayState = "${entity.hvacAction} (${entity.displayState})";
|
||||
} else {
|
||||
displayState = "${entity.displayState}";
|
||||
}
|
||||
if (entity.presetMode != null) {
|
||||
displayState += " - ${entity.presetMode}";
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
@ -25,7 +31,7 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("${entity.state}",
|
||||
Text("$displayState",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -38,8 +44,8 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
))
|
||||
],
|
||||
),
|
||||
entity.attributes["current_temperature"] != null ?
|
||||
Text("Currently: ${entity.attributes["current_temperature"]}",
|
||||
entity.currentTemperature != null ?
|
||||
Text("Currently: ${entity.currentTemperature}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
50
lib/entities/climate/widgets/temperature_control_widget.dart
Normal file
@ -0,0 +1,50 @@
|
||||
part of '../../../main.dart';
|
||||
|
||||
class TemperatureControlWidget extends StatelessWidget {
|
||||
final double value;
|
||||
final double fontSize;
|
||||
final Color fontColor;
|
||||
final onInc;
|
||||
final onDec;
|
||||
|
||||
TemperatureControlWidget(
|
||||
{Key key,
|
||||
@required this.value,
|
||||
@required this.onInc,
|
||||
@required this.onDec,
|
||||
this.fontSize,
|
||||
this.fontColor})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"$value",
|
||||
style: TextStyle(
|
||||
fontSize: fontSize ?? 24.0,
|
||||
color: fontColor ?? Colors.black
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
'mdi:chevron-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => onInc(),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
'mdi:chevron-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => onDec(),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class CoverEntity extends Entity {
|
||||
|
||||
@ -11,6 +11,8 @@ class CoverEntity extends Entity {
|
||||
static const SUPPORT_STOP_TILT = 64;
|
||||
static const SUPPORT_SET_TILT_POSITION = 128;
|
||||
|
||||
CoverEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportOpen => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_OPEN) ==
|
||||
CoverEntity.SUPPORT_OPEN);
|
||||
@ -45,8 +47,6 @@ class CoverEntity extends Entity {
|
||||
bool get canTiltBeOpened => currentTiltPosition < 100;
|
||||
bool get canTiltBeClosed => currentTiltPosition > 0;
|
||||
|
||||
CoverEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return CoverStateWidget();
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class CoverControlWidget extends StatefulWidget {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class CoverStateWidget extends StatelessWidget {
|
||||
void _open(CoverEntity entity) {
|
@ -1,6 +1,8 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class DateTimeEntity extends Entity {
|
||||
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get hasDate => attributes["has_date"] ?? false;
|
||||
bool get hasTime => attributes["has_time"] ?? false;
|
||||
int get year => attributes["year"] ?? 1970;
|
||||
@ -12,8 +14,6 @@ class DateTimeEntity extends Entity {
|
||||
String get formattedState => _getFormattedState();
|
||||
DateTime get dateTimeState => _getDateTimeState();
|
||||
|
||||
DateTimeEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return DateTimeStateWidget();
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class DateTimeStateWidget extends StatelessWidget {
|
||||
@override
|
@ -27,7 +27,7 @@ class Entity {
|
||||
"cold.on": "Cold",
|
||||
"cold.off": "Normal",
|
||||
"connectivity.on": "Connected",
|
||||
"connectivity.off": "Diconnected",
|
||||
"connectivity.off": "Disconnected",
|
||||
"door.on": "Open",
|
||||
"door.off": "Closed",
|
||||
"garage_door.on": "Open",
|
||||
@ -73,6 +73,7 @@ class Entity {
|
||||
Map attributes;
|
||||
String domain;
|
||||
String entityId;
|
||||
String entityPicture;
|
||||
String state;
|
||||
String displayState;
|
||||
DateTime _lastUpdated;
|
||||
@ -94,7 +95,6 @@ class Entity {
|
||||
bool get isBadge => Entity.badgeDomains.contains(domain);
|
||||
String get icon => attributes["icon"] ?? "";
|
||||
bool get isOn => state == EntityState.on;
|
||||
String get entityPicture => _getEntityPictureUrl();
|
||||
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
||||
List get childEntityIds => attributes["entity_id"] ?? [];
|
||||
String get lastUpdated => _getLastUpdatedFormatted();
|
||||
@ -102,21 +102,21 @@ class Entity {
|
||||
double get doubleState => double.tryParse(state) ?? 0.0;
|
||||
int get supportedFeatures => attributes["supported_features"] ?? 0;
|
||||
|
||||
String _getEntityPictureUrl() {
|
||||
String _getEntityPictureUrl(String webHost) {
|
||||
String result = attributes["entity_picture"];
|
||||
if (result == null) return result;
|
||||
if (!result.startsWith("http")) {
|
||||
if (result.startsWith("/")) {
|
||||
result = "$homeAssistantWebHost$result";
|
||||
result = "$webHost$result";
|
||||
} else {
|
||||
result = "$homeAssistantWebHost/$result";
|
||||
result = "$webHost/$result";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Entity(Map rawData) {
|
||||
update(rawData);
|
||||
Entity(Map rawData, String webHost) {
|
||||
update(rawData, webHost);
|
||||
}
|
||||
|
||||
Entity.missed(String entityId) {
|
||||
@ -148,14 +148,15 @@ class Entity {
|
||||
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||
}
|
||||
|
||||
void update(Map rawData) {
|
||||
void update(Map rawData, String webHost) {
|
||||
attributes = rawData["attributes"] ?? {};
|
||||
domain = rawData["entity_id"].split(".")[0];
|
||||
entityId = rawData["entity_id"];
|
||||
deviceClass = attributes["device_class"];
|
||||
state = rawData["state"];
|
||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
|
||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||
entityPicture = _getEntityPictureUrl(webHost);
|
||||
}
|
||||
|
||||
double _getDoubleAttributeValue(String attributeName) {
|
||||
@ -215,7 +216,7 @@ class Entity {
|
||||
entityWrapper: EntityWrapper(entity: this),
|
||||
child: EntityPageContainer(children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
||||
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
||||
),
|
||||
LastUpdatedWidget(),
|
@ -4,6 +4,7 @@ class EntityWrapper {
|
||||
|
||||
String displayName;
|
||||
String icon;
|
||||
String unitOfMeasurement;
|
||||
String entityPicture;
|
||||
EntityUIAction uiAction;
|
||||
Entity entity;
|
||||
@ -24,6 +25,7 @@ class EntityWrapper {
|
||||
if (uiAction == null) {
|
||||
uiAction = EntityUIAction();
|
||||
}
|
||||
unitOfMeasurement = entity.unitOfMeasurement;
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +62,7 @@ class EntityWrapper {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
} else {
|
||||
HAUtils.launchURL(uiAction.tapService);
|
||||
Launcher.launchURL(uiAction.tapService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -100,7 +102,7 @@ class EntityWrapper {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
} else {
|
||||
HAUtils.launchURL(uiAction.holdService);
|
||||
Launcher.launchURL(uiAction.holdService);
|
||||
}
|
||||
break;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class FanEntity extends Entity {
|
||||
|
||||
@ -6,7 +6,7 @@ class FanEntity extends Entity {
|
||||
static const SUPPORT_OSCILLATE = 2;
|
||||
static const SUPPORT_DIRECTION = 4;
|
||||
|
||||
FanEntity(Map rawData) : super(rawData);
|
||||
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportSetSpeed => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_SET_SPEED) ==
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class FanControlsWidget extends StatefulWidget {
|
||||
|
@ -1,12 +1,13 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class GroupEntity extends Entity {
|
||||
GroupEntity(Map rawData) : super(rawData);
|
||||
|
||||
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
|
||||
String mutualDomain;
|
||||
bool switchable = false;
|
||||
|
||||
GroupEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
if (switchable) {
|
||||
@ -19,8 +20,8 @@ class GroupEntity extends Entity {
|
||||
}
|
||||
|
||||
@override
|
||||
void update(Map rawData) {
|
||||
super.update(rawData);
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
if (_isOneDomain()) {
|
||||
mutualDomain = attributes['entity_id'][0].split(".")[0];
|
||||
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightEntity extends Entity {
|
||||
|
||||
@ -42,7 +42,7 @@ class LightEntity extends Entity {
|
||||
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
|
||||
List<String> get effectList => getStringListAttributeValue("effect_list");
|
||||
|
||||
LightEntity(Map rawData) : super(rawData);
|
||||
LightEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
HSVColor _getColor() {
|
||||
List hs = attributes["hs_color"];
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class LightControlsWidget extends StatefulWidget {
|
||||
|
||||
@ -17,7 +17,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
String _tmpEffect;
|
||||
|
||||
void _resetState(LightEntity entity) {
|
||||
_tmpBrightness = entity.brightness ?? 0;
|
||||
_tmpBrightness = entity.brightness ?? 1;
|
||||
_tmpWhiteValue = entity.whiteValue ?? 0;
|
||||
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
|
||||
_tmpColor = entity.color ?? _tmpColor;
|
||||
@ -28,15 +28,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
setState(() {
|
||||
_tmpBrightness = value.round();
|
||||
_changedHere = true;
|
||||
if (_tmpBrightness > 0) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"brightness": _tmpBrightness}));
|
||||
} else {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_off", entity.entityId,
|
||||
null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -114,10 +108,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
_tmpBrightness = value.round();
|
||||
});
|
||||
},
|
||||
min: 0.0,
|
||||
min: 1.0,
|
||||
max: 255.0,
|
||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||
value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(),
|
||||
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(),
|
||||
leading: Icon(Icons.brightness_5),
|
||||
title: "Brightness",
|
||||
);
|
||||
@ -171,7 +165,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
|
||||
Widget _buildColorControl(LightEntity entity) {
|
||||
if (entity.supportColor) {
|
||||
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
|
||||
HSVColor savedColor = HomeAssistant().savedColor;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
@ -187,10 +181,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
child: Text('Copy color'),
|
||||
onPressed: _tmpColor == null ? null : () {
|
||||
setState(() {
|
||||
HomeAssistantModel
|
||||
.of(context)
|
||||
.homeAssistant
|
||||
.savedColor = _tmpColor;
|
||||
HomeAssistant().savedColor = _tmpColor;
|
||||
});
|
||||
},
|
||||
),
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class LockEntity extends Entity {
|
||||
LockEntity(Map rawData) : super(rawData);
|
||||
LockEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get isLocked => state == "locked";
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class LockStateWidget extends StatelessWidget {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class MediaPlayerEntity extends Entity {
|
||||
|
||||
@ -20,7 +20,7 @@ class MediaPlayerEntity extends Entity {
|
||||
static const SUPPORT_SHUFFLE_SET = 32768;
|
||||
static const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||
|
||||
MediaPlayerEntity(Map rawData) : super(rawData);
|
||||
MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportPause => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PAUSE) ==
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class MediaPlayerWidget extends StatelessWidget {
|
||||
|
||||
@ -73,7 +73,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildImage(MediaPlayerEntity entity) {
|
||||
String state = entity.state;
|
||||
if (homeAssistantWebHost != null && entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Row(
|
@ -1,11 +1,11 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class SelectEntity extends Entity {
|
||||
List<String> get listOptions => attributes["options"] != null
|
||||
? (attributes["options"] as List).cast<String>()
|
||||
: [];
|
||||
|
||||
SelectEntity(Map rawData) : super(rawData);
|
||||
SelectEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class SelectStateWidget extends StatefulWidget {
|
||||
|
@ -1,8 +1,4 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SunEntity extends Entity {
|
||||
SunEntity(Map rawData) : super(rawData);
|
||||
}
|
||||
part of '../../main.dart';
|
||||
|
||||
class SensorEntity extends Entity {
|
||||
|
||||
@ -12,6 +8,6 @@ class SensorEntity extends Entity {
|
||||
numericState: true
|
||||
);
|
||||
|
||||
SensorEntity(Map rawData) : super(rawData);
|
||||
SensorEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class SliderEntity extends Entity {
|
||||
SliderEntity(Map rawData) : super(rawData);
|
||||
SliderEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
double get minValue => _getDoubleAttributeValue("min") ?? 0.0;
|
||||
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class SliderControlsWidget extends StatefulWidget {
|
||||
|
5
lib/entities/sun/sun_entity.class.dart
Normal file
@ -0,0 +1,5 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class SunEntity extends Entity {
|
||||
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class SwitchEntity extends Entity {
|
||||
SwitchEntity(Map rawData) : super(rawData);
|
||||
SwitchEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class SwitchStateWidget extends StatefulWidget {
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class TextEntity extends Entity {
|
||||
TextEntity(Map rawData) : super(rawData);
|
||||
TextEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
int get valueMinLength => attributes["min"] ?? -1;
|
||||
int get valueMaxLength => attributes["max"] ?? -1;
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class TextInputStateWidget extends StatefulWidget {
|
||||
|
||||
@ -73,13 +73,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
obscureText: entity.isPasswordField,
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _tmpValue,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _tmpValue.length)
|
||||
)
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _tmpValue)),
|
||||
onChanged: (value) {
|
||||
_tmpValue = value;
|
||||
}),
|
45
lib/entities/timer/timer_entity.class.dart
Normal file
@ -0,0 +1,45 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class TimerEntity extends Entity {
|
||||
TimerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
Duration duration;
|
||||
|
||||
@override
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
String durationSource = "${attributes["duration"]}";
|
||||
if (durationSource != null && durationSource.isNotEmpty) {
|
||||
try {
|
||||
List<String> durationList = durationSource.split(":");
|
||||
if (durationList.length == 1) {
|
||||
duration = Duration(seconds: int.tryParse(durationList[0] ?? 0));
|
||||
} else if (durationList.length == 2) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0
|
||||
);
|
||||
} else if (durationList.length == 3) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0,
|
||||
seconds: int.tryParse(durationList[2]) ?? 0
|
||||
);
|
||||
} else {
|
||||
Logger.e("Strange $entityId duration format: $durationSource");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("Error parsing duration for $entityId: ${e.toString()}");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} else {
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return TimerState();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class TimerState extends StatefulWidget {
|
||||
//final bool expanded;
|
@ -1,36 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class TimerEntity extends Entity {
|
||||
TimerEntity(Map rawData) : super(rawData);
|
||||
|
||||
Duration duration;
|
||||
|
||||
@override
|
||||
void update(Map rawData) {
|
||||
super.update(rawData);
|
||||
String durationSource = "${attributes["duration"]}";
|
||||
List<String> durationList = durationSource.split(":");
|
||||
if (durationList.length == 1) {
|
||||
duration = Duration(seconds: int.tryParse(durationList[0] ?? 0));
|
||||
} else if (durationList.length == 2) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0
|
||||
);
|
||||
} else if (durationList.length == 3) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0,
|
||||
seconds: int.tryParse(durationList[2]) ?? 0
|
||||
);
|
||||
} else {
|
||||
Logger.e("Cann't parse $entityId duration: $durationSource");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return TimerState();
|
||||
}
|
||||
}
|
@ -2,13 +2,15 @@ part of 'main.dart';
|
||||
|
||||
class EntityCollection {
|
||||
|
||||
final homeAssistantWebHost;
|
||||
|
||||
Map<String, Entity> _allEntities;
|
||||
//Map<String, Entity> views;
|
||||
|
||||
bool get isEmpty => _allEntities.isEmpty;
|
||||
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
||||
|
||||
EntityCollection() {
|
||||
EntityCollection(this.homeAssistantWebHost) {
|
||||
_allEntities = {};
|
||||
//views = {};
|
||||
}
|
||||
@ -33,70 +35,74 @@ class EntityCollection {
|
||||
});
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_allEntities.clear();
|
||||
}
|
||||
|
||||
Entity _createEntityInstance(rawEntityData) {
|
||||
switch (rawEntityData["entity_id"].split(".")[0]) {
|
||||
case 'sun': {
|
||||
return SunEntity(rawEntityData);
|
||||
return SunEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "media_player": {
|
||||
return MediaPlayerEntity(rawEntityData);
|
||||
return MediaPlayerEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case 'sensor': {
|
||||
return SensorEntity(rawEntityData);
|
||||
return SensorEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case 'lock': {
|
||||
return LockEntity(rawEntityData);
|
||||
return LockEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "automation": {
|
||||
return AutomationEntity(rawEntityData);
|
||||
return AutomationEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
|
||||
case "input_boolean":
|
||||
case "switch": {
|
||||
return SwitchEntity(rawEntityData);
|
||||
return SwitchEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "light": {
|
||||
return LightEntity(rawEntityData);
|
||||
return LightEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "group": {
|
||||
return GroupEntity(rawEntityData);
|
||||
return GroupEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "script":
|
||||
case "scene": {
|
||||
return ButtonEntity(rawEntityData);
|
||||
return ButtonEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_datetime": {
|
||||
return DateTimeEntity(rawEntityData);
|
||||
return DateTimeEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_select": {
|
||||
return SelectEntity(rawEntityData);
|
||||
return SelectEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_number": {
|
||||
return SliderEntity(rawEntityData);
|
||||
return SliderEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_text": {
|
||||
return TextEntity(rawEntityData);
|
||||
return TextEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "climate": {
|
||||
return ClimateEntity(rawEntityData);
|
||||
return ClimateEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "cover": {
|
||||
return CoverEntity(rawEntityData);
|
||||
return CoverEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "fan": {
|
||||
return FanEntity(rawEntityData);
|
||||
return FanEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "camera": {
|
||||
return CameraEntity(rawEntityData);
|
||||
return CameraEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "alarm_control_panel": {
|
||||
return AlarmControlPanelEntity(rawEntityData);
|
||||
return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "timer": {
|
||||
return TimerEntity(rawEntityData);
|
||||
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
default: {
|
||||
return Entity(rawEntityData);
|
||||
return Entity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -121,7 +127,7 @@ class EntityCollection {
|
||||
}
|
||||
|
||||
void updateFromRaw(Map rawEntityData) {
|
||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
|
||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
|
||||
Entity get(String entityId) {
|
||||
@ -143,6 +149,17 @@ class EntityCollection {
|
||||
return _allEntities[entityId] != null;
|
||||
}
|
||||
|
||||
List<Entity> getByDomains(List<String> domains) {
|
||||
List<Entity> result = [];
|
||||
_allEntities.forEach((id, entity) {
|
||||
if (domains.contains(entity.domain)) {
|
||||
Logger.d("getByDomain: ${entity.isHidden}");
|
||||
result.add(entity);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Entity> filterEntitiesForDefaultView() {
|
||||
List<Entity> result = [];
|
||||
List<Entity> groups = [];
|
||||
|
@ -35,25 +35,36 @@ class BadgeWidget extends StatelessWidget {
|
||||
break;
|
||||
}
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
color: Colors.black
|
||||
);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.state;
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
||||
double stateFontSize;
|
||||
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
|
||||
stateFontSize = 18.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
|
||||
stateFontSize = 15.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
|
||||
stateFontSize = 10.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||
stateFontSize = 8.0;
|
||||
}
|
||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.state}",
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 17.0),
|
||||
style: TextStyle(fontSize: stateFontSize),
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
@ -16,111 +16,14 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
}
|
||||
|
||||
CameraEntity _entity;
|
||||
|
||||
http.Client client;
|
||||
http.StreamedResponse response;
|
||||
List<int> binaryImage = [];
|
||||
bool timeToStop = false;
|
||||
Completer streamCompleter;
|
||||
bool started = false;
|
||||
bool useSVG = false;
|
||||
String streamUrl = "";
|
||||
|
||||
void _connect() async {
|
||||
started = true;
|
||||
timeToStop = false;
|
||||
String streamUrl = '$homeAssistantWebHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}';
|
||||
client = new http.Client(); // create a client to make api calls
|
||||
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
|
||||
Logger.d("[Sending] ==> $streamUrl");
|
||||
response = await client.send(request);
|
||||
Logger.d("[Received] <== ${response.headers}");
|
||||
String frameBoundary = response.headers['content-type'].split('boundary=')[1];
|
||||
final int frameBoundarySize = frameBoundary.length;
|
||||
List<int> primaryBuffer=[];
|
||||
int imageSizeStart = 59;
|
||||
int imageSizeEnd = 0;
|
||||
int imageStart = 0;
|
||||
int imageSize = 0;
|
||||
String strBuffer = "";
|
||||
String contentType = "";
|
||||
streamCompleter = Completer();
|
||||
response.stream.transform(
|
||||
StreamTransformer.fromHandlers(
|
||||
handleData: (data, sink) {
|
||||
primaryBuffer.addAll(data);
|
||||
imageStart = 0;
|
||||
imageSizeEnd = 0;
|
||||
if (primaryBuffer.length >= imageSizeStart + 10) {
|
||||
contentType = utf8.decode(
|
||||
primaryBuffer.sublist(frameBoundarySize+16, imageSizeStart + 10), allowMalformed: true).split("\r\n")[0];
|
||||
useSVG = contentType == "image/svg+xml";
|
||||
imageSizeStart = frameBoundarySize + 16 + contentType.length + 18;
|
||||
for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) {
|
||||
strBuffer = utf8.decode(
|
||||
primaryBuffer.sublist(i, i + 4), allowMalformed: true);
|
||||
if (strBuffer == "\r\n\r\n") {
|
||||
imageSizeEnd = i;
|
||||
imageStart = i + 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (imageSizeEnd > 0) {
|
||||
imageSize = int.tryParse(utf8.decode(
|
||||
primaryBuffer.sublist(imageSizeStart, imageSizeEnd),
|
||||
allowMalformed: true));
|
||||
//Logger.d("content-length: $imageSize");
|
||||
if (imageSize != null &&
|
||||
primaryBuffer.length >= imageStart + imageSize + 2) {
|
||||
sink.add(
|
||||
primaryBuffer.sublist(
|
||||
imageStart, imageStart + imageSize));
|
||||
primaryBuffer.removeRange(0, imageStart + imageSize + 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (timeToStop) {
|
||||
sink?.close();
|
||||
streamCompleter.complete();
|
||||
}
|
||||
},
|
||||
handleError: (error, stack, sink) {
|
||||
Logger.e("Error parsing MJPEG stream: $error");
|
||||
},
|
||||
handleDone: (sink) {
|
||||
Logger.d("Camera stream finished. Reconnecting...");
|
||||
sink?.close();
|
||||
streamCompleter?.complete();
|
||||
_reconnect();
|
||||
},
|
||||
)
|
||||
).listen((d) {
|
||||
if (!timeToStop) {
|
||||
setState(() {
|
||||
binaryImage = d;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _reconnect() {
|
||||
disconnect().then((_){
|
||||
_connect();
|
||||
});
|
||||
}
|
||||
|
||||
Future disconnect() {
|
||||
Completer disconF = Completer();
|
||||
timeToStop = true;
|
||||
if (streamCompleter != null && !streamCompleter.isCompleted) {
|
||||
streamCompleter.future.then((_) {
|
||||
client?.close();
|
||||
disconF.complete();
|
||||
});
|
||||
} else {
|
||||
client?.close();
|
||||
disconF.complete();
|
||||
}
|
||||
return disconF.future;
|
||||
launchStream() {
|
||||
Launcher.launchURLInCustomTab(
|
||||
context: context,
|
||||
url: streamUrl
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -130,46 +33,26 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
.of(context)
|
||||
.entityWrapper
|
||||
.entity;
|
||||
_connect();
|
||||
started = true;
|
||||
}
|
||||
|
||||
if (binaryImage.isEmpty) {
|
||||
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
.entityId}?token=${_entity.attributes['access_token']}';
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: const CircularProgressIndicator()
|
||||
child: IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
||||
iconSize: 50.0,
|
||||
onPressed: () => launchStream(),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
if (useSVG) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
SvgPicture.memory(
|
||||
Uint8List.fromList(binaryImage),
|
||||
placeholderBuilder: (BuildContext context) =>
|
||||
new Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: const CircularProgressIndicator()
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Image.memory(
|
||||
Uint8List.fromList(binaryImage), gaplessPlayback: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -7,8 +7,10 @@ class SimpleEntityState extends StatelessWidget {
|
||||
final EdgeInsetsGeometry padding;
|
||||
final int maxLines;
|
||||
final String customValue;
|
||||
final double fontSize;
|
||||
final bool bold;
|
||||
|
||||
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||
const SimpleEntityState({Key key,this.bold: false, this.maxLines: 10, this.fontSize: Sizes.stateFontSize, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -21,18 +23,22 @@ class SimpleEntityState extends StatelessWidget {
|
||||
state = customValue;
|
||||
}
|
||||
TextStyle textStyle = TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
fontSize: this.fontSize,
|
||||
fontWeight: FontWeight.normal
|
||||
);
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||
textStyle = textStyle.apply(color: Colors.blue);
|
||||
}
|
||||
if (this.bold) {
|
||||
textStyle = textStyle.apply(fontWeightDelta: 100);
|
||||
}
|
||||
while (state.contains(" ")){
|
||||
state = state.replaceAll(" ", " ");
|
||||
}
|
||||
Widget result = Padding(
|
||||
padding: padding,
|
||||
child: Text(
|
||||
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
||||
"$state ${entityModel.entityWrapper.unitOfMeasurement}",
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
@ -15,14 +15,18 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
||||
return Divider();
|
||||
return Divider(
|
||||
color: Colors.black45,
|
||||
);
|
||||
}
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
Divider(
|
||||
color: Colors.black45,
|
||||
),
|
||||
Text(
|
||||
"${entityModel.entityWrapper.entity.displayName}",
|
||||
style: TextStyle(color: Colors.blue),
|
||||
@ -30,18 +34,7 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onLongPress: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleHold();
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleTap();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
Widget result = Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
EntityIcon(),
|
||||
@ -55,7 +48,23 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
),
|
||||
state
|
||||
],
|
||||
),
|
||||
);
|
||||
if (entityModel.handleTap) {
|
||||
return InkWell(
|
||||
onLongPress: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleHold();
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleTap();
|
||||
}
|
||||
},
|
||||
child: result,
|
||||
);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ class EntityColor {
|
||||
"auto": Colors.amber,
|
||||
EntityState.active: Colors.amber,
|
||||
EntityState.playing: Colors.amber,
|
||||
EntityState.paused: Colors.amber,
|
||||
"above_horizon": Colors.amber,
|
||||
EntityState.home: Colors.amber,
|
||||
EntityState.open: Colors.amber,
|
||||
|
@ -148,7 +148,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
});
|
||||
|
||||
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
|
||||
_selectedId = 0;
|
||||
_selectedId = numericDataLists.length -1;
|
||||
}
|
||||
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
||||
numericDataLists.forEach((attrName, dataList) {
|
||||
@ -202,6 +202,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
_selectedId -= 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = _parsedHistory.first.data.length - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _selectNext() {
|
||||
@ -210,6 +215,12 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
_selectedId += 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = 0;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectionChanged(charts.SelectionModel model) {
|
||||
|
@ -40,14 +40,14 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
_needToUpdateHistory = true;
|
||||
}
|
||||
|
||||
void _loadHistory(HomeAssistant ha, String entityId) {
|
||||
void _loadHistory(String entityId) {
|
||||
DateTime now = DateTime.now();
|
||||
if (_historyLastUpdated != null) {
|
||||
Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
|
||||
}
|
||||
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
||||
_historyLastUpdated = now;
|
||||
ha.getHistory(entityId).then((history){
|
||||
ConnectionManager().getHistory(entityId).then((history){
|
||||
if (!_disposed) {
|
||||
setState(() {
|
||||
_history = history.isNotEmpty ? history[0] : [];
|
||||
@ -68,13 +68,12 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
final Entity entity = entityModel.entityWrapper.entity;
|
||||
if (!_needToUpdateHistory) {
|
||||
_needToUpdateHistory = true;
|
||||
} else {
|
||||
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
|
||||
_loadHistory(entity.entityId);
|
||||
}
|
||||
return _buildChart();
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
||||
id: widget.rawHistory.length
|
||||
));
|
||||
if (_selectedId == -1) {
|
||||
_selectedId = 0;
|
||||
_selectedId = data.length - 1;
|
||||
}
|
||||
return [
|
||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||
@ -132,6 +132,11 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
||||
_selectedId -= 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = _parsedHistory.first.data.length - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _selectNext() {
|
||||
@ -140,6 +145,12 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
||||
_selectedId += 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = 0;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectionChanged(charts.SelectionModel model) {
|
||||
|
@ -101,7 +101,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
||||
colorId: data.last.colorId
|
||||
));
|
||||
if (_selectedId == -1) {
|
||||
_selectedId = 0;
|
||||
_selectedId = data.length - 1;
|
||||
}
|
||||
return [
|
||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||
@ -137,14 +137,25 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
||||
_selectedId -= 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = _parsedHistory.first.data.length - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _selectNext() {
|
||||
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
|
||||
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
|
||||
setState(() {
|
||||
_selectedId += 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = 0;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectionChanged(charts.SelectionModel model) {
|
||||
|
@ -20,23 +20,3 @@ class EntityModel extends InheritedWidget {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class HomeAssistantModel extends InheritedWidget {
|
||||
|
||||
const HomeAssistantModel({
|
||||
Key key,
|
||||
@required this.homeAssistant,
|
||||
@required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final HomeAssistant homeAssistant;
|
||||
|
||||
static HomeAssistantModel of(BuildContext context) {
|
||||
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,40 +1,30 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class HomeAssistant {
|
||||
String _webSocketAPIEndpoint;
|
||||
String _password;
|
||||
bool _useLovelace = false;
|
||||
|
||||
IOWebSocketChannel _hassioChannel;
|
||||
SendMessageQueue _messageQueue;
|
||||
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||
|
||||
factory HomeAssistant() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
int _currentMessageId = 0;
|
||||
int _subscriptionMessageId = 0;
|
||||
Map<int, Completer> _messageResolver = {};
|
||||
EntityCollection entities;
|
||||
HomeAssistantUI ui;
|
||||
Map _instanceConfig = {};
|
||||
Map services;
|
||||
String _userName;
|
||||
HSVColor savedColor;
|
||||
|
||||
String fcmToken;
|
||||
|
||||
Map _rawLovelaceData;
|
||||
|
||||
List<Panel> panels = [];
|
||||
|
||||
Completer _fetchCompleter;
|
||||
Completer _connectionCompleter;
|
||||
Timer _connectionTimer;
|
||||
Timer _fetchTimer;
|
||||
bool autoReconnect = false;
|
||||
|
||||
StreamSubscription _socketSubscription;
|
||||
|
||||
int messageExpirationTime = 30; //seconds
|
||||
Duration fetchTimeout = Duration(seconds: 30);
|
||||
Duration connectTimeout = Duration(seconds: 15);
|
||||
|
||||
String get locationName {
|
||||
if (_useLovelace) {
|
||||
if (ConnectionManager().useLovelace) {
|
||||
return ui?.title ?? "";
|
||||
} else {
|
||||
return _instanceConfig["location_name"] ?? "";
|
||||
@ -42,226 +32,103 @@ class HomeAssistant {
|
||||
}
|
||||
String get userName => _userName ?? locationName;
|
||||
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||
//int get viewsCount => entities.views.length ?? 0;
|
||||
bool get isNoEntities => entities == null || entities.isEmpty;
|
||||
bool get isNoViews => ui == null || ui.isEmpty;
|
||||
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
||||
|
||||
HomeAssistant() {
|
||||
entities = EntityCollection();
|
||||
_messageQueue = SendMessageQueue(messageExpirationTime);
|
||||
HomeAssistant._internal() {
|
||||
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||
DeviceInfoManager().loadDeviceInfo();
|
||||
}
|
||||
|
||||
void updateSettings(String url, String password, bool useLovelace) {
|
||||
_webSocketAPIEndpoint = url;
|
||||
_password = password;
|
||||
_useLovelace = useLovelace;
|
||||
Logger.d( "Use lovelace is $_useLovelace");
|
||||
}
|
||||
Completer _fetchCompleter;
|
||||
|
||||
Future fetch() {
|
||||
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
|
||||
Logger.w("Previous fetch is not complited");
|
||||
} else {
|
||||
_fetchCompleter = new Completer();
|
||||
_fetchTimer = Timer(fetchTimeout, () {
|
||||
Logger.e( "Data fetching timeout");
|
||||
disconnect().then((_) {
|
||||
_completeFetching({
|
||||
"errorCode": 9,
|
||||
"errorMessage": "Couldn't get data from server"
|
||||
});
|
||||
});
|
||||
});
|
||||
_connection().then((r) {
|
||||
_getData();
|
||||
}).catchError((e) {
|
||||
_completeFetching(e);
|
||||
});
|
||||
}
|
||||
Future fetchData() {
|
||||
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||
Logger.w("Previous data fetch is not completed yet");
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
|
||||
disconnect() async {
|
||||
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
|
||||
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
|
||||
onTimeout: () => Logger.d( "Socket sink closed")
|
||||
);
|
||||
await _socketSubscription.cancel();
|
||||
_hassioChannel = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Future _connection() {
|
||||
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
|
||||
Logger.d("Previous connection is not complited");
|
||||
} else {
|
||||
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
|
||||
_connectionCompleter = new Completer();
|
||||
autoReconnect = false;
|
||||
disconnect().then((_){
|
||||
Logger.d( "Socket connecting...");
|
||||
_connectionTimer = Timer(connectTimeout, () {
|
||||
Logger.e( "Socket connection timeout");
|
||||
_handleSocketError(null);
|
||||
});
|
||||
if (_socketSubscription != null) {
|
||||
_socketSubscription.cancel();
|
||||
}
|
||||
_hassioChannel = IOWebSocketChannel.connect(
|
||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
|
||||
_socketSubscription = _hassioChannel.stream.listen(
|
||||
(message) => _handleMessage(message),
|
||||
cancelOnError: true,
|
||||
onDone: () => _handleSocketClose(),
|
||||
onError: (e) => _handleSocketError(e)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
_completeConnecting(null);
|
||||
}
|
||||
}
|
||||
return _connectionCompleter.future;
|
||||
}
|
||||
|
||||
void _handleSocketClose() {
|
||||
Logger.d("Socket disconnected. Automatic reconnect is $autoReconnect");
|
||||
if (autoReconnect) {
|
||||
_reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSocketError(e) {
|
||||
Logger.e("Socket stream Error: $e");
|
||||
Logger.d("Automatic reconnect is $autoReconnect");
|
||||
if (autoReconnect) {
|
||||
_reconnect();
|
||||
} else {
|
||||
disconnect().then((_) {
|
||||
_completeConnecting({
|
||||
"errorCode": 1,
|
||||
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _reconnect() {
|
||||
disconnect().then((_) {
|
||||
_connection().catchError((e){
|
||||
_completeConnecting(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getData() async {
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
_fetchCompleter = Completer();
|
||||
List<Future> futures = [];
|
||||
futures.add(_getStates());
|
||||
if (_useLovelace) {
|
||||
if (ConnectionManager().useLovelace) {
|
||||
futures.add(_getLovelace());
|
||||
}
|
||||
futures.add(_getConfig());
|
||||
futures.add(_getServices());
|
||||
futures.add(_getUserInfo());
|
||||
futures.add(_getPanels());
|
||||
try {
|
||||
await Future.wait(futures);
|
||||
futures.add(ConnectionManager().sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "state_changed"},
|
||||
));
|
||||
Future.wait(futures).then((_) {
|
||||
if (isMobileAppEnabled) {
|
||||
_createUI();
|
||||
_completeFetching(null);
|
||||
} catch (error) {
|
||||
_completeFetching(error);
|
||||
}
|
||||
}
|
||||
|
||||
void _completeFetching(error) {
|
||||
_fetchTimer.cancel();
|
||||
_completeConnecting(error);
|
||||
if (!_fetchCompleter.isCompleted) {
|
||||
if (error != null) {
|
||||
_fetchCompleter.completeError(error);
|
||||
} else {
|
||||
autoReconnect = true;
|
||||
Logger.d( "Fetch complete successful");
|
||||
_fetchCompleter.complete();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
} else {
|
||||
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app-integration")]));
|
||||
}
|
||||
}
|
||||
}).catchError((e) {
|
||||
_fetchCompleter.completeError(e);
|
||||
});
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
|
||||
void _completeConnecting(error) {
|
||||
_connectionTimer.cancel();
|
||||
if (!_connectionCompleter.isCompleted) {
|
||||
if (error != null) {
|
||||
_connectionCompleter.completeError(error);
|
||||
} else {
|
||||
_connectionCompleter.complete();
|
||||
}
|
||||
} else if (error != null) {
|
||||
if (error is Error) {
|
||||
eventBus.fire(ShowErrorEvent(error.toString(), 12));
|
||||
} else {
|
||||
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_handleMessage(String message) {
|
||||
var data = json.decode(message);
|
||||
if (data["type"] == "auth_required") {
|
||||
_sendAuthMessage('{"type": "auth","access_token": "$_password"}');
|
||||
} else if (data["type"] == "auth_ok") {
|
||||
_completeConnecting(null);
|
||||
_sendSubscribe();
|
||||
} else if (data["type"] == "auth_invalid") {
|
||||
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
|
||||
} else if (data["type"] == "result") {
|
||||
Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
|
||||
_messageResolver[data["id"]]?.complete(data);
|
||||
_messageResolver.remove(data["id"]);
|
||||
} else if (data["type"] == "event") {
|
||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||
_handleEntityStateChange(data["event"]["data"]);
|
||||
} else if (data["event"] != null) {
|
||||
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
|
||||
} else {
|
||||
Logger.e("Event is null: $message");
|
||||
}
|
||||
} else {
|
||||
Logger.w("Unknown message type: $message");
|
||||
}
|
||||
}
|
||||
|
||||
void _sendSubscribe() {
|
||||
_incrementMessageId();
|
||||
_subscriptionMessageId = _currentMessageId;
|
||||
_send('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
|
||||
Future logout() async {
|
||||
Logger.d("Logging out...");
|
||||
await ConnectionManager().logout().then((_) {
|
||||
ui?.clear();
|
||||
entities?.clear();
|
||||
panels?.clear();
|
||||
});
|
||||
}
|
||||
|
||||
Future _getConfig() async {
|
||||
await _sendInitialMessage("get_config").then((data) => _instanceConfig = Map.from(data["result"]));
|
||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
}).catchError((e) {
|
||||
throw HAError("Error getting config: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getStates() async {
|
||||
await _sendInitialMessage("get_states").then((data) => entities.parse(data["result"]));
|
||||
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||
(data) => entities.parse(data)
|
||||
).catchError((e) {
|
||||
throw HAError("Error getting states: $e");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getLovelace() async {
|
||||
await _sendInitialMessage("lovelace/config").then((data) => _rawLovelaceData = data["result"]);
|
||||
await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
||||
throw HAError("Error getting lovelace config: $e");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getUserInfo() async {
|
||||
_userName = null;
|
||||
await _sendInitialMessage("auth/current_user").then((data) => _userName = data["result"]["name"]);
|
||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
||||
Logger.w("Can't get user info: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getServices() async {
|
||||
await _sendInitialMessage("get_services").then((data) => Logger.d("We actually don`t need the list of servcies for now"));
|
||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
|
||||
Logger.d("Got ${data.length} services");
|
||||
Logger.d("Media extractor: ${data["media_extractor"]}");
|
||||
services = data;
|
||||
}).catchError((e) {
|
||||
Logger.w("Can't get services: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getPanels() async {
|
||||
panels.clear();
|
||||
await _sendInitialMessage("get_panels").then((data) {
|
||||
if (data["success"]) {
|
||||
data["result"].forEach((k,v) {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
|
||||
data.forEach((k,v) {
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
panels.add(Panel(
|
||||
id: k,
|
||||
@ -273,93 +140,21 @@ class HomeAssistant {
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}).catchError((e) {
|
||||
throw HAError("Error getting panels list: $e");
|
||||
});
|
||||
}
|
||||
|
||||
_incrementMessageId() {
|
||||
_currentMessageId += 1;
|
||||
}
|
||||
|
||||
void _sendAuthMessage(String message) {
|
||||
Logger.d( "[Sending] ==> auth request");
|
||||
_hassioChannel.sink.add(message);
|
||||
}
|
||||
|
||||
Future _sendInitialMessage(String type) {
|
||||
Completer _completer = Completer();
|
||||
_incrementMessageId();
|
||||
_messageResolver[_currentMessageId] = _completer;
|
||||
_send('{"id": $_currentMessageId, "type": "$type"}', false);
|
||||
return _completer.future;
|
||||
}
|
||||
|
||||
_send(String message, bool queued) {
|
||||
var sendCompleter = Completer();
|
||||
if (queued) _messageQueue.add(message);
|
||||
_connection().then((r) {
|
||||
_messageQueue.getActualMessages().forEach((message){
|
||||
Logger.d( "[Sending queued] ==> $message");
|
||||
_hassioChannel.sink.add(message);
|
||||
});
|
||||
if (!queued) {
|
||||
Logger.d( "[Sending] ==> $message");
|
||||
_hassioChannel.sink.add(message);
|
||||
}
|
||||
sendCompleter.complete();
|
||||
}).catchError((e){
|
||||
sendCompleter.completeError(e);
|
||||
});
|
||||
return sendCompleter.future;
|
||||
}
|
||||
|
||||
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
|
||||
_incrementMessageId();
|
||||
String message = "";
|
||||
if (entityId != null) {
|
||||
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
|
||||
if (additionalParams != null) {
|
||||
additionalParams.forEach((name, value) {
|
||||
if ((value is double) || (value is int) || (value is List)) {
|
||||
message += ', "$name" : $value';
|
||||
} else {
|
||||
message += ', "$name" : "$value"';
|
||||
}
|
||||
});
|
||||
}
|
||||
message += '}}';
|
||||
} else {
|
||||
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"';
|
||||
if (additionalParams != null && additionalParams.isNotEmpty) {
|
||||
message += ', "service_data": {';
|
||||
bool first = true;
|
||||
additionalParams.forEach((name, value) {
|
||||
if (!first) {
|
||||
message += ', ';
|
||||
}
|
||||
if ((value is double) || (value is int) || (value is List)) {
|
||||
message += '"$name" : $value';
|
||||
} else {
|
||||
message += '"$name" : "$value"';
|
||||
}
|
||||
first = false;
|
||||
});
|
||||
|
||||
message += '}';
|
||||
}
|
||||
message += '}';
|
||||
}
|
||||
return _send(message, true);
|
||||
}
|
||||
|
||||
void _handleEntityStateChange(Map eventData) {
|
||||
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||
if (_fetchCompleter.isCompleted) {
|
||||
Map data = Map.from(eventData);
|
||||
eventBus.fire(new StateChangedEvent(
|
||||
entityId: data["entity_id"],
|
||||
needToRebuildUI: entities.updateState(data)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _parseLovelace() {
|
||||
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
||||
@ -372,7 +167,8 @@ class HomeAssistant {
|
||||
count: viewCounter,
|
||||
id: "${rawView['id']}",
|
||||
name: rawView['title'],
|
||||
iconName: rawView['icon']
|
||||
iconName: rawView['icon'],
|
||||
panel: rawView['panel'] ?? false,
|
||||
);
|
||||
|
||||
if (rawView['badges'] != null && rawView['badges'] is List) {
|
||||
@ -396,31 +192,29 @@ class HomeAssistant {
|
||||
List<HACard> result = [];
|
||||
rawCards.forEach((rawCard){
|
||||
try {
|
||||
bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
var rawCardInfo = rawCard["card"] ?? rawCard;
|
||||
HACard card = HACard(
|
||||
id: "card",
|
||||
name: isThereCardOptionsInside ? rawCard["card"]["title"] ??
|
||||
rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
|
||||
type: isThereCardOptionsInside
|
||||
? rawCard["card"]['type']
|
||||
: rawCard['type'],
|
||||
columnsCount: isThereCardOptionsInside
|
||||
? rawCard["card"]['columns'] ?? 4
|
||||
: rawCard['columns'] ?? 4,
|
||||
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ??
|
||||
true : rawCard['show_name'] ?? true,
|
||||
showState: isThereCardOptionsInside
|
||||
? rawCard["card"]['show_state'] ?? true
|
||||
: rawCard['show_state'] ?? true,
|
||||
showEmpty: rawCard['show_empty'] ?? true,
|
||||
stateFilter: rawCard['state_filter'] ?? [],
|
||||
states: rawCard['states'],
|
||||
content: rawCard['content']
|
||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||
showName: rawCardInfo['show_name'] ?? true,
|
||||
showState: rawCardInfo['show_state'] ?? true,
|
||||
showEmpty: rawCardInfo['show_empty'] ?? true,
|
||||
stateFilter: rawCardInfo['state_filter'] ?? [],
|
||||
states: rawCardInfo['states'],
|
||||
conditions: rawCard['conditions'] ?? [],
|
||||
content: rawCardInfo['content'],
|
||||
min: rawCardInfo['min'] ?? 0,
|
||||
max: rawCardInfo['max'] ?? 100,
|
||||
unit: rawCardInfo['unit'],
|
||||
severity: rawCardInfo['severity']
|
||||
);
|
||||
if (rawCard["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCard["cards"]);
|
||||
if (rawCardInfo["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||
}
|
||||
rawCard["entities"]?.forEach((rawEntity) {
|
||||
rawCardInfo["entities"]?.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (entities.isExist(rawEntity)) {
|
||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||
@ -483,15 +277,15 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (rawCard["entity"] != null) {
|
||||
var en = rawCard["entity"];
|
||||
if (rawCardInfo["entity"] != null) {
|
||||
var en = rawCardInfo["entity"];
|
||||
if (en is String) {
|
||||
if (entities.isExist(en)) {
|
||||
Entity e = entities.get(en);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: rawCard["icon"],
|
||||
displayName: rawCard["name"],
|
||||
icon: rawCardInfo["icon"],
|
||||
displayName: rawCardInfo["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
@ -521,7 +315,7 @@ class HomeAssistant {
|
||||
|
||||
void _createUI() {
|
||||
ui = HomeAssistantUI();
|
||||
if ((_useLovelace) && (_rawLovelaceData != null)) {
|
||||
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
||||
Logger.d("Creating Lovelace UI");
|
||||
_parseLovelace();
|
||||
} else {
|
||||
@ -555,31 +349,12 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildViews(BuildContext context, bool lovelace, TabController tabController) {
|
||||
Widget buildViews(BuildContext context, TabController tabController) {
|
||||
return ui.build(context, tabController);
|
||||
}
|
||||
|
||||
Future<List> getHistory(String entityId) async {
|
||||
DateTime now = DateTime.now();
|
||||
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||
Logger.d("[Sending] ==> $url");
|
||||
http.Response historyResponse;
|
||||
historyResponse = await http.get(url, headers: {
|
||||
"authorization": "Bearer $_password",
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
var history = json.decode(historyResponse.body);
|
||||
if (history is List) {
|
||||
Logger.d( "[Received] <== ${history.first.length} history recors");
|
||||
return history;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class SendMessageQueue {
|
||||
int _messageTimeout;
|
||||
List<HAMessage> _queue = [];
|
||||
@ -618,4 +393,4 @@ class HAMessage {
|
||||
bool isExpired() {
|
||||
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
723
lib/main.dart
@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -15,36 +15,48 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
import 'package:progress_indicators/progress_indicators.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/circular_slider/single_circular_slider.dart';
|
||||
import 'package:share/receive_share_state.dart';
|
||||
import 'package:share/share.dart';
|
||||
|
||||
part 'entity_class/const.dart';
|
||||
part 'entity_class/entity.class.dart';
|
||||
part 'entity_class/entity_wrapper.class.dart';
|
||||
part 'entity_class/timer_entity.dart';
|
||||
part 'entity_class/switch_entity.class.dart';
|
||||
part 'entity_class/button_entity.class.dart';
|
||||
part 'entity_class/text_entity.class.dart';
|
||||
part 'entity_class/climate_entity.class.dart';
|
||||
part 'entity_class/cover_entity.class.dart';
|
||||
part 'entity_class/date_time_entity.class.dart';
|
||||
part 'entity_class/light_entity.class.dart';
|
||||
part 'entity_class/select_entity.class.dart';
|
||||
part 'entity_class/other_entity.class.dart';
|
||||
part 'entity_class/slider_entity.dart';
|
||||
part 'entity_class/media_player_entity.class.dart';
|
||||
part 'entity_class/lock_entity.class.dart';
|
||||
part 'entity_class/group_entity.class.dart';
|
||||
part 'entity_class/fan_entity.class.dart';
|
||||
part 'entity_class/automation_entity.dart';
|
||||
part 'entity_class/camera_entity.class.dart';
|
||||
part 'entity_class/alarm_control_panel.class.dart';
|
||||
import 'utils/logger.dart';
|
||||
|
||||
part 'const.dart';
|
||||
part 'utils/launcher.dart';
|
||||
part 'entities/entity.class.dart';
|
||||
part 'entities/entity_wrapper.class.dart';
|
||||
part 'entities/timer/timer_entity.class.dart';
|
||||
part 'entities/switch/switch_entity.class.dart';
|
||||
part 'entities/button/button_entity.class.dart';
|
||||
part 'entities/text/text_entity.class.dart';
|
||||
part 'entities/climate/climate_entity.class.dart';
|
||||
part 'entities/cover/cover_entity.class.dart';
|
||||
part 'entities/date_time/date_time_entity.class.dart';
|
||||
part 'entities/light/light_entity.class.dart';
|
||||
part 'entities/select/select_entity.class.dart';
|
||||
part 'entities/sun/sun_entity.class.dart';
|
||||
part 'entities/sensor/sensor_entity.class.dart';
|
||||
part 'entities/slider/slider_entity.dart';
|
||||
part 'entities/media_player/media_player_entity.class.dart';
|
||||
part 'entities/lock/lock_entity.class.dart';
|
||||
part 'entities/group/group_entity.class.dart';
|
||||
part 'entities/fan/fan_entity.class.dart';
|
||||
part 'entities/automation/automation_entity.class.dart';
|
||||
part 'entities/camera/camera_entity.class.dart';
|
||||
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
|
||||
part 'entity_widgets/common/badge.dart';
|
||||
part 'entity_widgets/model_widgets.dart';
|
||||
part 'entity_widgets/default_entity_container.dart';
|
||||
part 'entity_widgets/missed_entity.dart';
|
||||
part 'entity_widgets/glance_entity_container.dart';
|
||||
part 'entity_widgets/button_entity_container.dart';
|
||||
part 'cards/widgets/glance_card_entity_container.dart';
|
||||
part 'cards/widgets/entity_button_card_body.widget.dart';
|
||||
part 'entity_widgets/common/entity_attributes_list.dart';
|
||||
part 'entity_widgets/entity_icon.dart';
|
||||
part 'entity_widgets/entity_name.dart';
|
||||
@ -63,48 +75,64 @@ part 'entity_widgets/history_chart/numeric_state_history_chart.dart';
|
||||
part 'entity_widgets/history_chart/combined_history_chart.dart';
|
||||
part 'entity_widgets/history_chart/history_control_widget.dart';
|
||||
part 'entity_widgets/history_chart/entity_history_moment.dart';
|
||||
part 'entity_widgets/state/switch_state.dart';
|
||||
part 'entity_widgets/controls/slider_controls.dart';
|
||||
part 'entity_widgets/state/text_input_state.dart';
|
||||
part 'entity_widgets/state/select_state.dart';
|
||||
part 'entity_widgets/state/simple_state.dart';
|
||||
part 'entity_widgets/state/timer_state.dart';
|
||||
part 'entity_widgets/state/climate_state.dart';
|
||||
part 'entity_widgets/state/cover_state.dart';
|
||||
part 'entity_widgets/state/date_time_state.dart';
|
||||
part 'entity_widgets/state/lock_state.dart';
|
||||
part 'entity_widgets/controls/climate_controls.dart';
|
||||
part 'entity_widgets/controls/cover_controls.dart';
|
||||
part 'entity_widgets/controls/light_controls.dart';
|
||||
part 'entity_widgets/controls/media_player_widgets.dart';
|
||||
part 'entity_widgets/controls/fan_controls.dart';
|
||||
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
|
||||
part 'settings.page.dart';
|
||||
part 'panel.page.dart';
|
||||
part 'entities/switch/widget/switch_state.dart';
|
||||
part 'entities/slider/widgets/slider_controls.dart';
|
||||
part 'entities/text/widgets/text_input_state.dart';
|
||||
part 'entities/select/widgets/select_state.dart';
|
||||
part 'entity_widgets/common/simple_state.dart';
|
||||
part 'entities/timer/widgets/timer_state.dart';
|
||||
part 'entities/climate/widgets/climate_state.widget.dart';
|
||||
part 'entities/cover/widgets/cover_state.dart';
|
||||
part 'entities/date_time/widgets/date_time_state.dart';
|
||||
part 'entities/lock/widgets/lock_state.dart';
|
||||
part 'entities/climate/widgets/climate_controls.dart';
|
||||
part 'entities/climate/widgets/temperature_control_widget.dart';
|
||||
part 'entities/cover/widgets/cover_controls.widget.dart';
|
||||
part 'entities/light/widgets/light_controls.dart';
|
||||
part 'entities/media_player/widgets/media_player_widgets.dart';
|
||||
part 'entities/fan/widgets/fan_controls.dart';
|
||||
part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.dart';
|
||||
part 'pages/settings.page.dart';
|
||||
part 'pages/purchase.page.dart';
|
||||
part 'pages/widgets/product_purchase.widget.dart';
|
||||
part 'pages/widgets/page_loading_indicator.dart';
|
||||
part 'pages/widgets/page_loading_error.dart';
|
||||
part 'pages/panel.page.dart';
|
||||
part 'pages/main.page.dart';
|
||||
part 'home_assistant.class.dart';
|
||||
part 'log.page.dart';
|
||||
part 'entity.page.dart';
|
||||
part 'utils.class.dart';
|
||||
part 'pages/log.page.dart';
|
||||
part 'pages/entity.page.dart';
|
||||
part 'mdi.class.dart';
|
||||
part 'entity_collection.class.dart';
|
||||
part 'ui_class/ui.dart';
|
||||
part 'ui_class/view.class.dart';
|
||||
part 'ui_class/card.class.dart';
|
||||
part 'ui_class/sizes_class.dart';
|
||||
part 'ui_class/panel_class.dart';
|
||||
part 'ui_widgets/view.dart';
|
||||
part 'ui_widgets/card_widget.dart';
|
||||
part 'ui_widgets/card_header_widget.dart';
|
||||
part 'ui_widgets/config_panel_widget.dart';
|
||||
part 'managers/auth_manager.class.dart';
|
||||
part 'managers/location_manager.class.dart';
|
||||
part 'managers/mobile_app_integration_manager.class.dart';
|
||||
part 'managers/connection_manager.class.dart';
|
||||
part 'managers/device_info_manager.class.dart';
|
||||
part 'managers/startup_user_messages_manager.class.dart';
|
||||
part 'ui.dart';
|
||||
part 'view.class.dart';
|
||||
part 'cards/card.class.dart';
|
||||
part 'panels/panel_class.dart';
|
||||
part 'view.dart';
|
||||
part 'cards/card_widget.dart';
|
||||
part 'cards/widgets/card_header.widget.dart';
|
||||
part 'panels/config_panel_widget.dart';
|
||||
part 'panels/widgets/link_to_web_config.dart';
|
||||
part 'types/ha_error.dart';
|
||||
part 'types/event_bus_events.dart';
|
||||
part 'cards/widgets/gauge_card_body.dart';
|
||||
part 'cards/widgets/light_card_body.dart';
|
||||
part 'pages/play_media.page.dart';
|
||||
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = "HA Client";
|
||||
const appVersion = "0.5.0";
|
||||
const appVersion = "0.6.6";
|
||||
|
||||
String homeAssistantWebHost;
|
||||
|
||||
void main() {
|
||||
void main() async {
|
||||
FlutterError.onError = (errorDetails) {
|
||||
Logger.e( "${errorDetails.exception}");
|
||||
if (Logger.isInDebugMode) {
|
||||
@ -113,7 +141,11 @@ void main() {
|
||||
};
|
||||
|
||||
runZoned(() {
|
||||
//AndroidAlarmManager.initialize().then((_) {
|
||||
runApp(new HAClientApp());
|
||||
// print("Running MAIN isolate ${Isolate.current.hashCode}");
|
||||
//});
|
||||
|
||||
}, onError: (error, stack) {
|
||||
Logger.e("$error");
|
||||
Logger.e("$stack");
|
||||
@ -124,6 +156,7 @@ void main() {
|
||||
}
|
||||
|
||||
class HAClientApp extends StatelessWidget {
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -136,564 +169,38 @@ class HAClientApp extends StatelessWidget {
|
||||
routes: {
|
||||
"/": (context) => MainPage(title: 'HA Client'),
|
||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||
"/configuration": (context) => PanelPage(title: "Configuration"),
|
||||
"/log-view": (context) => LogViewPage(title: "Log")
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
MainPage({Key key, this.title}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
_MainPageState createState() => new _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||
HomeAssistant _homeAssistant;
|
||||
//Map _instanceConfig;
|
||||
String _webSocketApiEndpoint;
|
||||
String _password;
|
||||
//int _uiViewsCount = 0;
|
||||
String _instanceHost;
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _settingsSubscription;
|
||||
StreamSubscription _serviceCallSubscription;
|
||||
StreamSubscription _showEntityPageSubscription;
|
||||
StreamSubscription _showErrorSubscription;
|
||||
bool _settingsLoaded = false;
|
||||
bool _accountMenuExpanded = false;
|
||||
bool _useLovelaceUI;
|
||||
int _previousViewCount;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_settingsLoaded = false;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
Logger.d("<!!!> Creating new HomeAssistant instance");
|
||||
_homeAssistant = HomeAssistant();
|
||||
|
||||
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
||||
Logger.d("Settings change event: reconnect=${event.reconnect}");
|
||||
if (event.reconnect) {
|
||||
_homeAssistant.disconnect().then((_){
|
||||
_initialLoad();
|
||||
});
|
||||
}
|
||||
});
|
||||
_initialLoad();
|
||||
}
|
||||
|
||||
void _initialLoad() {
|
||||
_loadConnectionSettings().then((_){
|
||||
_subscribe();
|
||||
_refreshData();
|
||||
}, onError: (_) {
|
||||
_showErrorBottomBar(message: _, errorCode: 5);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
Logger.d("$state");
|
||||
if (state == AppLifecycleState.resumed && _settingsLoaded) {
|
||||
_refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
_loadConnectionSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String domain = prefs.getString('hassio-domain');
|
||||
String port = prefs.getString('hassio-port');
|
||||
_instanceHost = "$domain:$port";
|
||||
_webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
|
||||
homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
|
||||
_password = prefs.getString('hassio-password');
|
||||
_useLovelaceUI = prefs.getBool('use-lovelace') ?? true;
|
||||
if ((domain == null) || (port == null) || (_password == null) ||
|
||||
(domain.length == 0) || (port.length == 0) || (_password.length == 0)) {
|
||||
throw("Check connection settings");
|
||||
} else {
|
||||
_settingsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
_subscribe() {
|
||||
if (_stateSubscription == null) {
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.needToRebuildUI) {
|
||||
Logger.d("New entity. Need to rebuild UI");
|
||||
_refreshData();
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (_serviceCallSubscription == null) {
|
||||
_serviceCallSubscription =
|
||||
eventBus.on<ServiceCallEvent>().listen((event) {
|
||||
_callService(event.domain, event.service, event.entityId,
|
||||
event.additionalParams);
|
||||
});
|
||||
}
|
||||
|
||||
if (_showEntityPageSubscription == null) {
|
||||
_showEntityPageSubscription =
|
||||
eventBus.on<ShowEntityPageEvent>().listen((event) {
|
||||
_showEntityPage(event.entity.entityId);
|
||||
});
|
||||
}
|
||||
|
||||
if (_showErrorSubscription == null) {
|
||||
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
||||
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_refreshData() async {
|
||||
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
|
||||
_hideBottomBar();
|
||||
_showInfoBottomBar(progress: true,);
|
||||
await _homeAssistant.fetch().then((result) {
|
||||
_hideBottomBar();
|
||||
int currentViewCount = _homeAssistant.ui?.views?.length ?? 0;
|
||||
if (_previousViewCount != currentViewCount) {
|
||||
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
||||
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
||||
_previousViewCount = currentViewCount;
|
||||
}
|
||||
}).catchError((e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
eventBus.fire(RefreshDataFinishedEvent());
|
||||
}
|
||||
|
||||
_setErrorState(e) {
|
||||
if (e is Error) {
|
||||
Logger.e(e.toString());
|
||||
Logger.e("${e.stackTrace}");
|
||||
_showErrorBottomBar(
|
||||
message: "There was some error",
|
||||
errorCode: 13
|
||||
);
|
||||
} else {
|
||||
_showErrorBottomBar(
|
||||
message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error",
|
||||
errorCode: e["errorCode"] != null ? e["errorCode"] : 99
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
|
||||
_showInfoBottomBar(
|
||||
message: "Calling $domain.$service",
|
||||
duration: Duration(seconds: 3)
|
||||
);
|
||||
_homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e));
|
||||
}
|
||||
|
||||
void _showEntityPage(String entityId) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
List<Tab> buildUIViewTabs() {
|
||||
List<Tab> result = [];
|
||||
|
||||
if (_homeAssistant.ui.views.isNotEmpty) {
|
||||
_homeAssistant.ui.views.forEach((HAView view) {
|
||||
result.add(view.buildTab());
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Drawer _buildAppDrawer() {
|
||||
List<Widget> menuItems = [];
|
||||
menuItems.add(
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: Text(_homeAssistant.userName),
|
||||
accountEmail: Text(_instanceHost ?? "Not configured"),
|
||||
onDetailsPressed: () {
|
||||
setState(() {
|
||||
_accountMenuExpanded = !_accountMenuExpanded;
|
||||
});
|
||||
},
|
||||
currentAccountPicture: CircleAvatar(
|
||||
child: Text(
|
||||
_homeAssistant.userAvatarText,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
if (_accountMenuExpanded) {
|
||||
menuItems.addAll([
|
||||
ListTile(
|
||||
leading: Icon(Icons.settings),
|
||||
title: Text("Settings"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/connection-settings');
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
]);
|
||||
} else {
|
||||
if (_homeAssistant != null && _homeAssistant.panels.isNotEmpty) {
|
||||
_homeAssistant.panels.forEach((Panel panel) {
|
||||
if (!panel.isHidden) {
|
||||
menuItems.add(
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
||||
title: Text("${panel.title}"),
|
||||
onTap: () => panel.handleOpen(context)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
menuItems.addAll([
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
|
||||
title: Text("Open Web UI"),
|
||||
onTap: () => HAUtils.launchURL(homeAssistantWebHost),
|
||||
),
|
||||
Divider()
|
||||
]);
|
||||
}
|
||||
menuItems.addAll([
|
||||
new ListTile(
|
||||
leading: Icon(Icons.insert_drive_file),
|
||||
title: Text("Log"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/log-view');
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
|
||||
title: Text("Report an issue"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
new AboutListTile(
|
||||
aboutBoxChildren: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
HAUtils.launchURL("http://ha-client.homemade.systems/");
|
||||
},
|
||||
child: Text(
|
||||
"ha-client.homemade.systems",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
applicationName: appName,
|
||||
applicationVersion: appVersion
|
||||
)
|
||||
]);
|
||||
}
|
||||
return new Drawer(
|
||||
child: ListView(
|
||||
children: menuItems,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _hideBottomBar() {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
setState(() {
|
||||
_showBottomBar = false;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _bottomBarAction;
|
||||
bool _showBottomBar = false;
|
||||
String _bottomBarText;
|
||||
bool _bottomBarProgress;
|
||||
Color _bottomBarColor;
|
||||
Timer _bottomBarTimer;
|
||||
|
||||
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
||||
_bottomBarTimer?.cancel();
|
||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
||||
_bottomBarColor = Colors.grey.shade50;
|
||||
setState(() {
|
||||
_bottomBarText = message;
|
||||
_bottomBarProgress = progress;
|
||||
_showBottomBar = true;
|
||||
});
|
||||
if (duration != null) {
|
||||
_bottomBarTimer = Timer(duration, () {
|
||||
_hideBottomBar();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showErrorBottomBar({Key key, @required String message, @required int errorCode}) {
|
||||
TextStyle textStyle = TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: Sizes.nameFontSize
|
||||
);
|
||||
_bottomBarColor = Colors.red.shade100;
|
||||
switch (errorCode) {
|
||||
case 9:
|
||||
case 11:
|
||||
case 7:
|
||||
case 1: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Retry", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 5: {
|
||||
message = "Check connection settings";
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Open", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
Navigator.pushNamed(context, '/connection-settings');
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 6: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Settings", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
Navigator.pushNamed(context, '/connection-settings');
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 10: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Refresh", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 8: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Reconnect", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Reload", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_bottomBarProgress = false;
|
||||
_bottomBarText = "$message (code: $errorCode)";
|
||||
_showBottomBar = true;
|
||||
});
|
||||
/*_scaffoldKey.currentState.hideCurrentSnackBar();
|
||||
_scaffoldKey.currentState.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("$message (code: $errorCode)"),
|
||||
action: action,
|
||||
duration: Duration(hours: 1),
|
||||
)
|
||||
);*/
|
||||
}
|
||||
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
|
||||
Widget _buildScaffoldBody(bool empty) {
|
||||
return NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
pinned: true,
|
||||
primary: true,
|
||||
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical"), color: Colors.white,),
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
||||
context: context,
|
||||
items: [PopupMenuItem<String>(
|
||||
child: new Text("Reload"),
|
||||
value: "reload",
|
||||
)]
|
||||
).then((String val) {
|
||||
if (val == "reload") {
|
||||
_refreshData();
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
],
|
||||
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
||||
"/play-media": (context) => PlayMediaPage(mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",),
|
||||
"/log-view": (context) => LogViewPage(title: "Log"),
|
||||
"/login": (context) => WebviewScaffold(
|
||||
url: "${ConnectionManager().oauthUrl}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.menu),
|
||||
icon: Icon(Icons.help),
|
||||
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
|
||||
),
|
||||
title: new Text("Login with HA"),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text("Manual", style: TextStyle(color: Colors.white)),
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
setState(() {
|
||||
_accountMenuExpanded = false;
|
||||
});
|
||||
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
|
||||
},
|
||||
),
|
||||
bottom: empty ? null : TabBar(
|
||||
controller: _viewsTabController,
|
||||
tabs: buildUIViewTabs(),
|
||||
isScrollable: true,
|
||||
),
|
||||
),
|
||||
|
||||
];
|
||||
},
|
||||
body: empty ?
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant"),
|
||||
size: 100.0,
|
||||
color: Colors.blue,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
:
|
||||
_homeAssistant.buildViews(context, _useLovelaceUI, _viewsTabController),
|
||||
);
|
||||
}
|
||||
|
||||
TabController _viewsTabController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget bottomBar;
|
||||
if (_showBottomBar) {
|
||||
List<Widget> bottomBarChildren = [];
|
||||
if (_bottomBarText != null) {
|
||||
bottomBarChildren.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
|
||||
Sizes.rowPadding),
|
||||
child: Text(
|
||||
"$_bottomBarText",
|
||||
textAlign: TextAlign.left,
|
||||
softWrap: true,
|
||||
),
|
||||
)
|
||||
|
||||
);
|
||||
}
|
||||
if (_bottomBarProgress) {
|
||||
bottomBarChildren.add(
|
||||
CollectionScaleTransition(
|
||||
children: <Widget>[
|
||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
|
||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
|
||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (bottomBarChildren.isNotEmpty) {
|
||||
bottomBar = Container(
|
||||
color: _bottomBarColor,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: bottomBarChildren,
|
||||
),
|
||||
"/webview": (context) => WebviewScaffold(
|
||||
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop()
|
||||
),
|
||||
_bottomBarAction
|
||||
],
|
||||
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// This method is rerun every time setState is called.
|
||||
if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
primary: false,
|
||||
drawer: _buildAppDrawer(),
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: _buildScaffoldBody(true)
|
||||
);
|
||||
} else {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
drawer: _buildAppDrawer(),
|
||||
primary: false,
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: HomeAssistantModel(
|
||||
child: _buildScaffoldBody(false),
|
||||
homeAssistant: _homeAssistant
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_viewsTabController.dispose();
|
||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||
if (_settingsSubscription != null) _settingsSubscription.cancel();
|
||||
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
|
||||
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
|
||||
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
|
||||
_homeAssistant.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
45
lib/managers/auth_manager.class.dart
Normal file
@ -0,0 +1,45 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class AuthManager {
|
||||
|
||||
static final AuthManager _instance = AuthManager._internal();
|
||||
|
||||
factory AuthManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
AuthManager._internal();
|
||||
|
||||
Future getTempToken({String oauthUrl}) {
|
||||
Completer completer = Completer();
|
||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
||||
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
|
||||
String authCode = url.split("=")[1];
|
||||
Logger.d("We have auth code. Getting temporary access token...");
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/auth/token",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
includeAuthHeader: false,
|
||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}"
|
||||
).then((response) {
|
||||
Logger.d("Got temp token");
|
||||
String tempToken = json.decode(response)['access_token'];
|
||||
Logger.d("Closing webview...");
|
||||
//flutterWebviewPlugin.close();
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.complete(tempToken);
|
||||
}).catchError((e) {
|
||||
//flutterWebviewPlugin.close();
|
||||
Logger.e("Error getting temp token: ${e.toString()}");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.completeError(HAError("Error getting temp token"));
|
||||
});
|
||||
}
|
||||
});
|
||||
Logger.d("Launching OAuth");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
}
|
413
lib/managers/connection_manager.class.dart
Normal file
@ -0,0 +1,413 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ConnectionManager {
|
||||
|
||||
static final ConnectionManager _instance = ConnectionManager._internal();
|
||||
|
||||
factory ConnectionManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
ConnectionManager._internal();
|
||||
|
||||
String _domain;
|
||||
String _port;
|
||||
String displayHostname;
|
||||
String _webSocketAPIEndpoint;
|
||||
String httpWebHost;
|
||||
String _token;
|
||||
String _tempToken;
|
||||
String oauthUrl;
|
||||
String webhookId;
|
||||
bool useLovelace = true;
|
||||
bool settingsLoaded = false;
|
||||
bool get isAuthenticated => _token != null;
|
||||
StreamSubscription _socketSubscription;
|
||||
Duration connectTimeout = Duration(seconds: 15);
|
||||
|
||||
bool isConnected = false;
|
||||
|
||||
var onStateChangeCallback;
|
||||
|
||||
IOWebSocketChannel _socket;
|
||||
|
||||
int _currentMessageId = 0;
|
||||
Map<String, Completer> _messageResolver = {};
|
||||
|
||||
Future init({bool loadSettings, bool forceReconnect: false}) async {
|
||||
Completer completer = Completer();
|
||||
bool stopInit = false;
|
||||
if (loadSettings) {
|
||||
Logger.e("Loading settings...");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
||||
_domain = prefs.getString('hassio-domain');
|
||||
_port = prefs.getString('hassio-port');
|
||||
webhookId = prefs.getString('app-webhook-id');
|
||||
displayHostname = "$_domain:$_port";
|
||||
_webSocketAPIEndpoint =
|
||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||
httpWebHost =
|
||||
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||
if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
completer.completeError(HAError.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
} else {
|
||||
final storage = new FlutterSecureStorage();
|
||||
try {
|
||||
_token = await storage.read(key: "hacl_llt");
|
||||
Logger.e("Long-lived token read successful");
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
||||
.encodeComponent(
|
||||
'http://ha-client.homemade.systems/service/auth_callback.html')}";
|
||||
settingsLoaded = true;
|
||||
} catch (e) {
|
||||
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||
Logger.e("Cannt read secure storage. Need to relogin.");
|
||||
stopInit = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
completer.completeError(HAError.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stopInit) {
|
||||
if (_token == null) {
|
||||
AuthManager().getTempToken(
|
||||
oauthUrl: oauthUrl
|
||||
).then((token) {
|
||||
Logger.d("Token from AuthManager recived");
|
||||
_tempToken = token;
|
||||
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
||||
}).catchError((e) {
|
||||
completer.completeError(e);
|
||||
});
|
||||
} else {
|
||||
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
||||
}
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void _doConnect({Completer completer, bool forceReconnect}) {
|
||||
if (forceReconnect || !isConnected) {
|
||||
_connect().timeout(connectTimeout, onTimeout: () {
|
||||
_disconnect().then((_) {
|
||||
completer?.completeError(HAError("Connection timeout"));
|
||||
});
|
||||
}).then((_) {
|
||||
completer?.complete();
|
||||
}).catchError((e) {
|
||||
completer?.completeError(e);
|
||||
});
|
||||
} else {
|
||||
completer?.complete();
|
||||
}
|
||||
}
|
||||
|
||||
Completer connecting;
|
||||
|
||||
Future _connect() {
|
||||
if (connecting != null && !connecting.isCompleted) {
|
||||
Logger.w("Previous connection attempt pending...");
|
||||
return connecting.future;
|
||||
} else {
|
||||
connecting = Completer();
|
||||
_disconnect().then((_) {
|
||||
Logger.d("Socket connecting...");
|
||||
_socket = IOWebSocketChannel.connect(
|
||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||
_socketSubscription = _socket.stream.listen(
|
||||
(message) {
|
||||
isConnected = true;
|
||||
var data = json.decode(message);
|
||||
if (data["type"] == "auth_required") {
|
||||
Logger.d("[Received] <== ${data.toString()}");
|
||||
_authenticate().then((_) {
|
||||
Logger.d('Authentication complete');
|
||||
connecting.complete();
|
||||
}).catchError((e) {
|
||||
if (!connecting.isCompleted) connecting.completeError(e);
|
||||
});
|
||||
} else if (data["type"] == "auth_ok") {
|
||||
Logger.d("[Received] <== ${data.toString()}");
|
||||
_messageResolver["auth"]?.complete();
|
||||
_messageResolver.remove("auth");
|
||||
if (_token != null) {
|
||||
if (!connecting.isCompleted) connecting.complete();
|
||||
}
|
||||
} else if (data["type"] == "auth_invalid") {
|
||||
Logger.d("[Received] <== ${data.toString()}");
|
||||
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||
_messageResolver.remove("auth");
|
||||
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
} else {
|
||||
_handleMessage(data);
|
||||
}
|
||||
},
|
||||
cancelOnError: true,
|
||||
onDone: () => _handleSocketClose(connecting),
|
||||
onError: (e) => _handleSocketError(e, connecting)
|
||||
);
|
||||
});
|
||||
return connecting.future;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future _disconnect() {
|
||||
Completer completer = Completer();
|
||||
if (!isConnected) {
|
||||
completer.complete();
|
||||
} else {
|
||||
isConnected = false;
|
||||
List<Future> fl = [];
|
||||
Logger.d("Socket disconnecting...");
|
||||
if (_socketSubscription != null) {
|
||||
fl.add(_socketSubscription.cancel());
|
||||
}
|
||||
if (_socket != null && _socket.sink != null &&
|
||||
_socket.closeCode == null) {
|
||||
fl.add(_socket.sink.close().timeout(Duration(seconds: 3)));
|
||||
}
|
||||
Future.wait(fl).whenComplete(() => completer.complete());
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
_handleMessage(data) {
|
||||
if (data["type"] == "result") {
|
||||
if (data["id"] != null && data["success"]) {
|
||||
Logger.d("[Received] <== Request id ${data['id']} was successful");
|
||||
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
||||
} else if (data["id"] != null) {
|
||||
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
||||
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
|
||||
}
|
||||
_messageResolver.remove("${data["id"]}");
|
||||
} else if (data["type"] == "event") {
|
||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||
onStateChangeCallback(data["event"]["data"]);
|
||||
} else if (data["event"] != null) {
|
||||
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
|
||||
} else {
|
||||
Logger.e("Event is null: $data");
|
||||
}
|
||||
} else {
|
||||
Logger.d("[Received unhandled] <== ${data.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSocketClose(Completer connectionCompleter) {
|
||||
Logger.d("Socket disconnected.");
|
||||
if (!connectionCompleter.isCompleted) {
|
||||
isConnected = false;
|
||||
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||
} else {
|
||||
_disconnect().then((_) {
|
||||
Timer(Duration(seconds: 5), () {
|
||||
Logger.d("Trying to reconnect...");
|
||||
_connect().catchError((e) {
|
||||
isConnected = false;
|
||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSocketError(e, Completer connectionCompleter) {
|
||||
Logger.e("Socket stream Error: $e");
|
||||
if (!connectionCompleter.isCompleted) {
|
||||
isConnected = false;
|
||||
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
|
||||
} else {
|
||||
_disconnect().then((_) {
|
||||
Timer(Duration(seconds: 5), () {
|
||||
Logger.d("Trying to reconnect...");
|
||||
_connect().catchError((e) {
|
||||
isConnected = false;
|
||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future _authenticate() {
|
||||
Completer completer = Completer();
|
||||
if (_token != null) {
|
||||
Logger.d( "Long-lived token exist");
|
||||
Logger.d( "[Sending] ==> auth request");
|
||||
sendSocketMessage(
|
||||
type: "auth",
|
||||
additionalData: {"access_token": "$_token"},
|
||||
auth: true
|
||||
).then((_) {
|
||||
completer.complete();
|
||||
}).catchError((e) => completer.completeError(e));
|
||||
} else if (_tempToken != null) {
|
||||
Logger.d("We have temp token. Loging in...");
|
||||
sendSocketMessage(
|
||||
type: "auth",
|
||||
additionalData: {"access_token": "$_tempToken"},
|
||||
auth: true
|
||||
).then((_) {
|
||||
Logger.d("Requesting long-lived token...");
|
||||
_getLongLivedToken().then((_) {
|
||||
Logger.d("getLongLivedToken finished");
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
Logger.e("Can't get long-lived token: $e");
|
||||
throw e;
|
||||
});
|
||||
}).catchError((e) => completer.completeError(e));
|
||||
} else {
|
||||
completer.completeError(HAError("General login error"));
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future logout() {
|
||||
Logger.d("Logging out");
|
||||
Completer completer = Completer();
|
||||
_disconnect().whenComplete(() {
|
||||
_token = null;
|
||||
_tempToken = null;
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.delete(key: "hacl_llt").whenComplete((){
|
||||
completer.complete();
|
||||
});
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future _getLongLivedToken() {
|
||||
Completer completer = Completer();
|
||||
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
|
||||
Logger.d("Got long-lived token.");
|
||||
_token = data;
|
||||
_tempToken = null;
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.write(key: "hacl_llt", value: "$_token").then((_) {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool("oauth-used", true);
|
||||
completer.complete();
|
||||
});
|
||||
}).catchError((e) {
|
||||
throw e;
|
||||
});
|
||||
}).catchError((e) {
|
||||
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future sendSocketMessage({String type, Map additionalData, bool auth: false}) {
|
||||
Completer _completer = Completer();
|
||||
Map dataObject = {"type": "$type"};
|
||||
String callbackName;
|
||||
if (!auth) {
|
||||
_incrementMessageId();
|
||||
dataObject["id"] = _currentMessageId;
|
||||
callbackName = "$_currentMessageId";
|
||||
} else {
|
||||
callbackName = "auth";
|
||||
}
|
||||
if (additionalData != null) {
|
||||
dataObject.addAll(additionalData);
|
||||
}
|
||||
_messageResolver[callbackName] = _completer;
|
||||
String rawMessage = json.encode(dataObject);
|
||||
if (!isConnected) {
|
||||
_connect().timeout(connectTimeout, onTimeout: (){
|
||||
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||
}).then((_) {
|
||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||
_socket.sink.add(rawMessage);
|
||||
}).catchError((e) {
|
||||
_completer.completeError(e);
|
||||
});
|
||||
} else {
|
||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||
_socket.sink.add(rawMessage);
|
||||
}
|
||||
return _completer.future;
|
||||
}
|
||||
|
||||
void _incrementMessageId() {
|
||||
_currentMessageId += 1;
|
||||
}
|
||||
|
||||
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
|
||||
Map serviceData = {};
|
||||
if (entityId != null) {
|
||||
serviceData["entity_id"] = entityId;
|
||||
}
|
||||
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
|
||||
serviceData.addAll(additionalServiceData);
|
||||
}
|
||||
if (serviceData.isNotEmpty)
|
||||
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
|
||||
else
|
||||
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
|
||||
}
|
||||
|
||||
Future<List> getHistory(String entityId) async {
|
||||
DateTime now = DateTime.now();
|
||||
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||
Logger.d("[Sending] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
|
||||
http.Response historyResponse;
|
||||
historyResponse = await http.get(url, headers: {
|
||||
"authorization": "Bearer $_token",
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
var history = json.decode(historyResponse.body);
|
||||
if (history is List) {
|
||||
Logger.d( "[Received] <== HTTP ${history.first.length} history recors");
|
||||
return history;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
|
||||
Completer completer = Completer();
|
||||
String url = "$httpWebHost$endPoint";
|
||||
Logger.d("[Sending] ==> HTTP $endPoint");
|
||||
Map<String, String> headers = {};
|
||||
if (contentType != null) {
|
||||
headers["Content-Type"] = contentType;
|
||||
}
|
||||
if (includeAuthHeader) {
|
||||
headers["authorization"] = "Bearer $_token";
|
||||
}
|
||||
http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: data
|
||||
).then((response) {
|
||||
Logger.d("[Received] <== HTTP ${response.statusCode}");
|
||||
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
||||
completer.complete(response.body);
|
||||
} else {
|
||||
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
|
||||
}
|
||||
}).catchError((e) {
|
||||
completer.completeError(e);
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
}
|
29
lib/managers/device_info_manager.class.dart
Normal file
@ -0,0 +1,29 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class DeviceInfoManager {
|
||||
|
||||
static final DeviceInfoManager _instance = DeviceInfoManager._internal();
|
||||
|
||||
factory DeviceInfoManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
String unicDeviceId;
|
||||
String manufacturer;
|
||||
String model;
|
||||
String osName;
|
||||
String osVersion;
|
||||
|
||||
DeviceInfoManager._internal();
|
||||
|
||||
loadDeviceInfo() {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
deviceInfo.androidInfo.then((androidInfo) {
|
||||
unicDeviceId = "${androidInfo.model.toLowerCase().replaceAll(' ', '_')}_${androidInfo.androidId}";
|
||||
manufacturer = "${androidInfo.manufacturer}";
|
||||
model = "${androidInfo.model}";
|
||||
osName = "Android";
|
||||
osVersion = "${androidInfo.version.release}";
|
||||
});
|
||||
}
|
||||
}
|
5
lib/managers/location_manager.class.dart
Normal file
@ -0,0 +1,5 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class LocationManager {
|
||||
|
||||
}
|
121
lib/managers/mobile_app_integration_manager.class.dart
Normal file
@ -0,0 +1,121 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MobileAppIntegrationManager {
|
||||
|
||||
static final _appRegistrationData = {
|
||||
"app_version": "$appVersion",
|
||||
"device_name": "${HomeAssistant().userName}'s ${DeviceInfoManager().model}",
|
||||
"manufacturer": DeviceInfoManager().manufacturer,
|
||||
"model": DeviceInfoManager().model,
|
||||
"os_version": DeviceInfoManager().osVersion,
|
||||
"app_data": {
|
||||
"push_token": "${HomeAssistant().fcmToken}",
|
||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
||||
}
|
||||
};
|
||||
|
||||
static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
|
||||
Completer completer = Completer();
|
||||
if (ConnectionManager().webhookId == null || forceRegister) {
|
||||
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
||||
var registrationData = Map.from(_appRegistrationData);
|
||||
registrationData.addAll({
|
||||
"app_id": "ha_client",
|
||||
"app_name": "$appName",
|
||||
"os_name": DeviceInfoManager().osName,
|
||||
"supports_encryption": false,
|
||||
});
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/api/mobile_app/registrations",
|
||||
includeAuthHeader: true,
|
||||
data: json.encode(registrationData)
|
||||
).then((response) {
|
||||
Logger.d("Processing registration responce...");
|
||||
var responseObject = json.decode(response);
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
||||
ConnectionManager().webhookId = responseObject["webhook_id"];
|
||||
completer.complete();
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Mobile app Integration was created",
|
||||
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
|
||||
positiveText: "Restart now",
|
||||
negativeText: "Later",
|
||||
onPositive: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
|
||||
},
|
||||
));
|
||||
});
|
||||
}).catchError((e) {
|
||||
completer.complete();
|
||||
Logger.e("Error registering the app: ${e.toString()}");
|
||||
});
|
||||
return completer.future;
|
||||
} else {
|
||||
Logger.d("App was previously registered. Checking...");
|
||||
var updateData = {
|
||||
"type": "update_registration",
|
||||
"data": _appRegistrationData
|
||||
};
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/api/webhook/${ConnectionManager().webhookId}",
|
||||
includeAuthHeader: false,
|
||||
data: json.encode(updateData)
|
||||
).then((response) {
|
||||
if (response == null || response.isEmpty) {
|
||||
Logger.d("No registration data in response. MobileApp integration was removed");
|
||||
_askToRegisterApp();
|
||||
} else {
|
||||
Logger.d("App registration works fine");
|
||||
if (showOkDialog) {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "All good",
|
||||
body: "HA Client integration with your Home Assistant server works fine",
|
||||
positiveText: "Nice!",
|
||||
negativeText: "Ok"
|
||||
));
|
||||
}
|
||||
}
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
if (e['code'] != null && e['code'] == 410) {
|
||||
Logger.e("MobileApp integration was removed");
|
||||
_askToRegisterApp();
|
||||
} else {
|
||||
Logger.e("Error updating app registration: ${e.toString()}");
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "App integration is not working properly",
|
||||
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue.",
|
||||
positiveText: "Report to GitHub",
|
||||
negativeText: "Report to Discord",
|
||||
onPositive: () {
|
||||
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
onNegative: () {
|
||||
Launcher.launchURL("https://discord.gg/AUzEvwn");
|
||||
},
|
||||
));
|
||||
}
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
static void _askToRegisterApp() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "App integration was removed",
|
||||
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
|
||||
positiveText: "Register now",
|
||||
negativeText: "Cancel",
|
||||
onPositive: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.remove("app-webhook-id");
|
||||
ConnectionManager().webhookId = null;
|
||||
checkAppRegistration();
|
||||
});
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
}
|
71
lib/managers/startup_user_messages_manager.class.dart
Normal file
@ -0,0 +1,71 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class StartupUserMessagesManager {
|
||||
|
||||
static final StartupUserMessagesManager _instance = StartupUserMessagesManager
|
||||
._internal();
|
||||
|
||||
factory StartupUserMessagesManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
StartupUserMessagesManager._internal() {}
|
||||
|
||||
bool _supportAppDevelopmentMessageShown;
|
||||
bool _whatsNewMessageShown;
|
||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||
static final _whatsNewMessageKey = "user-message-shown-whats-new-660";
|
||||
|
||||
void checkMessagesToShow() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.reload();
|
||||
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
|
||||
_whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false;
|
||||
if (!_whatsNewMessageShown) {
|
||||
_showWhatsNewMessage();
|
||||
} else if (!_supportAppDevelopmentMessageShown) {
|
||||
_showSupportAppDevelopmentMessage();
|
||||
}
|
||||
}
|
||||
|
||||
void _showSupportAppDevelopmentMessage() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Hi!",
|
||||
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
|
||||
positiveText: "Show options",
|
||||
negativeText: "Cancel",
|
||||
onPositive: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
||||
eventBus.fire(ShowPageEvent(path: "/putchase"));
|
||||
});
|
||||
},
|
||||
onNegative: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
||||
});
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
void _showWhatsNewMessage() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "What's new",
|
||||
body: "You can now share any media url to HA Client via Android share menu. It will try to play that media on one of your media player. There is also 'tv' button available in app header if you want to send some url manually",
|
||||
positiveText: "Full release notes",
|
||||
negativeText: "Ok",
|
||||
onPositive: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_whatsNewMessageKey, true);
|
||||
Launcher.launchURL("https://github.com/estevez-dev/ha_client/releases");
|
||||
});
|
||||
},
|
||||
onNegative: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_whatsNewMessageKey, true);
|
||||
});
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
}
|
@ -24,10 +24,12 @@ class MaterialDesignIcons {
|
||||
"cover.opening": "mdi:window-open",
|
||||
"camera": "mdi:cctv",
|
||||
"calendar": "mdi:calendar",
|
||||
"device_tracker": "mdi:account",
|
||||
"timer": "mdi:timer",
|
||||
"lock.locked": "mdi:lock",
|
||||
"lock.unlocked": "mdi:lock-open",
|
||||
"fan": "mdi:fan",
|
||||
"remote": "mdi:remote",
|
||||
"alarm_control_panel.disarmed" : "mdi:bell-outline",
|
||||
"alarm_control_panel.armed_home" : "mdi:bell-plus",
|
||||
"alarm_control_panel.armed_away" : "mdi:bell",
|
||||
@ -64,18 +66,18 @@ class MaterialDesignIcons {
|
||||
"binary_sensor.occupancy.off": "mdi:home-outline",
|
||||
"binary_sensor.opening.on": "mdi:square-outline",
|
||||
"binary_sensor.opening.off": "mdi:square",
|
||||
//"binary_sensor.plug.on": "mdi:",
|
||||
//"binary_sensor.plug.off": "mdi:",
|
||||
"binary_sensor.plug.on": "mdi:power-plug",
|
||||
"binary_sensor.plug.off": "mdi:power-plug-off",
|
||||
"binary_sensor.power.on": "mdi:alert",
|
||||
"binary_sensor.power.off": "mdi:verified",
|
||||
"binary_sensor.power.off": "mdi:shield-check",
|
||||
//"binary_sensor.presence.on": "mdi:",
|
||||
//"binary_sensor.presence.off": "mdi:",
|
||||
"binary_sensor.problem.on": "mdi:alert-outline",
|
||||
"binary_sensor.problem.off": "mdi:check-outline",
|
||||
"binary_sensor.safety.on": "mdi:alert",
|
||||
"binary_sensor.safety.off": "mdi:verified",
|
||||
"binary_sensor.safety.off": "mdi:shield-check",
|
||||
"binary_sensor.smoke.on": "mdi:alert",
|
||||
"binary_sensor.smoke.off": "mdi:verified",
|
||||
"binary_sensor.smoke.off": "mdi:shield-check",
|
||||
"binary_sensor.sound.on": "mdi:music-note",
|
||||
"binary_sensor.sound.off": "mdi:music-note-off",
|
||||
"binary_sensor.vibration.on": "mdi:vibrate",
|
||||
@ -96,8 +98,8 @@ class MaterialDesignIcons {
|
||||
"cover.window.closing": "mdi:window-open",
|
||||
"cover.window.opening": "mdi:window-open",
|
||||
};
|
||||
static final Map iconsDataMap = {
|
||||
|
||||
static final Map iconsDataMap = {
|
||||
"mdi:access-point": 0xf002,
|
||||
"mdi:access-point-network": 0xf003,
|
||||
"mdi:access-point-network-off": 0xfbbd,
|
||||
@ -111,6 +113,8 @@ class MaterialDesignIcons {
|
||||
"mdi:account-badge": 0xfd83,
|
||||
"mdi:account-badge-alert": 0xfd84,
|
||||
"mdi:account-badge-alert-outline": 0xfd85,
|
||||
"mdi:account-badge-horizontal": 0xfdf0,
|
||||
"mdi:account-badge-horizontal-outline": 0xfdf1,
|
||||
"mdi:account-badge-outline": 0xfd86,
|
||||
"mdi:account-box": 0xf006,
|
||||
"mdi:account-box-multiple": 0xf933,
|
||||
@ -492,6 +496,7 @@ class MaterialDesignIcons {
|
||||
"mdi:auto-upload": 0xf069,
|
||||
"mdi:autorenew": 0xf06a,
|
||||
"mdi:av-timer": 0xf06b,
|
||||
"mdi:aws": 0xfdf2,
|
||||
"mdi:axe": 0xf8c7,
|
||||
"mdi:axis": 0xfd24,
|
||||
"mdi:axis-arrow": 0xfd25,
|
||||
@ -519,6 +524,10 @@ class MaterialDesignIcons {
|
||||
"mdi:backspace-outline": 0xfb38,
|
||||
"mdi:backup-restore": 0xf06f,
|
||||
"mdi:badminton": 0xf850,
|
||||
"mdi:bag-personal": 0xfdf3,
|
||||
"mdi:bag-personal-off": 0xfdf4,
|
||||
"mdi:bag-personal-off-outline": 0xfdf5,
|
||||
"mdi:bag-personal-outline": 0xfdf6,
|
||||
"mdi:balloon": 0xfa25,
|
||||
"mdi:ballot": 0xf9c8,
|
||||
"mdi:ballot-outline": 0xf9c9,
|
||||
@ -626,6 +635,7 @@ class MaterialDesignIcons {
|
||||
"mdi:bell-sleep-outline": 0xfa92,
|
||||
"mdi:beta": 0xf0a1,
|
||||
"mdi:betamax": 0xf9ca,
|
||||
"mdi:biathlon": 0xfdf7,
|
||||
"mdi:bible": 0xf0a2,
|
||||
"mdi:bike": 0xf0a3,
|
||||
"mdi:billiards": 0xfb3d,
|
||||
@ -680,6 +690,8 @@ class MaterialDesignIcons {
|
||||
"mdi:bookmark-check": 0xf0c1,
|
||||
"mdi:bookmark-minus": 0xf9cb,
|
||||
"mdi:bookmark-minus-outline": 0xf9cc,
|
||||
"mdi:bookmark-multiple": 0xfdf8,
|
||||
"mdi:bookmark-multiple-outline": 0xfdf9,
|
||||
"mdi:bookmark-music": 0xf0c2,
|
||||
"mdi:bookmark-off": 0xf9cd,
|
||||
"mdi:bookmark-off-outline": 0xf9ce,
|
||||
@ -791,6 +803,8 @@ class MaterialDesignIcons {
|
||||
"mdi:calendar-heart": 0xf9d1,
|
||||
"mdi:calendar-import": 0xfb0a,
|
||||
"mdi:calendar-minus": 0xfd38,
|
||||
"mdi:calendar-month": 0xfdfa,
|
||||
"mdi:calendar-month-outline": 0xfdfb,
|
||||
"mdi:calendar-multiple": 0xf0f1,
|
||||
"mdi:calendar-multiple-check": 0xf0f2,
|
||||
"mdi:calendar-multiselect": 0xfa31,
|
||||
@ -837,6 +851,8 @@ class MaterialDesignIcons {
|
||||
"mdi:camera-party-mode": 0xf105,
|
||||
"mdi:camera-rear": 0xf106,
|
||||
"mdi:camera-rear-variant": 0xf107,
|
||||
"mdi:camera-retake": 0xfdfc,
|
||||
"mdi:camera-retake-outline": 0xfdfd,
|
||||
"mdi:camera-switch": 0xf108,
|
||||
"mdi:camera-timer": 0xf109,
|
||||
"mdi:camera-wireless": 0xfd92,
|
||||
@ -847,6 +863,7 @@ class MaterialDesignIcons {
|
||||
"mdi:cannabis": 0xf7a5,
|
||||
"mdi:caps-lock": 0xfa9a,
|
||||
"mdi:car": 0xf10b,
|
||||
"mdi:car-back": 0xfdfe,
|
||||
"mdi:car-battery": 0xf10c,
|
||||
"mdi:car-brake-abs": 0xfc23,
|
||||
"mdi:car-brake-alert": 0xfc24,
|
||||
@ -868,6 +885,7 @@ class MaterialDesignIcons {
|
||||
"mdi:car-light-high": 0xfc28,
|
||||
"mdi:car-limousine": 0xf8cc,
|
||||
"mdi:car-multiple": 0xfb4a,
|
||||
"mdi:car-off": 0xfdff,
|
||||
"mdi:car-parking-lights": 0xfd3f,
|
||||
"mdi:car-pickup": 0xf7a9,
|
||||
"mdi:car-side": 0xf7aa,
|
||||
@ -916,6 +934,7 @@ class MaterialDesignIcons {
|
||||
"mdi:cassette": 0xf9d3,
|
||||
"mdi:cast": 0xf118,
|
||||
"mdi:cast-connected": 0xf119,
|
||||
"mdi:cast-education": 0xfe6d,
|
||||
"mdi:cast-off": 0xf789,
|
||||
"mdi:castle": 0xf11a,
|
||||
"mdi:cat": 0xf11b,
|
||||
@ -966,6 +985,7 @@ class MaterialDesignIcons {
|
||||
"mdi:chat-processing": 0xfb57,
|
||||
"mdi:check": 0xf12c,
|
||||
"mdi:check-all": 0xf12d,
|
||||
"mdi:check-bold": 0xfe6e,
|
||||
"mdi:check-box-multiple-outline": 0xfc2d,
|
||||
"mdi:check-box-outline": 0xfc2e,
|
||||
"mdi:check-circle": 0xf5e0,
|
||||
@ -974,6 +994,9 @@ class MaterialDesignIcons {
|
||||
"mdi:check-network": 0xfc2f,
|
||||
"mdi:check-network-outline": 0xfc30,
|
||||
"mdi:check-outline": 0xf854,
|
||||
"mdi:check-underline": 0xfe70,
|
||||
"mdi:check-underline-circle": 0xfe71,
|
||||
"mdi:check-underline-circle-outline": 0xfe72,
|
||||
"mdi:checkbook": 0xfa9c,
|
||||
"mdi:checkbox-blank": 0xf12e,
|
||||
"mdi:checkbox-blank-circle": 0xf12f,
|
||||
@ -1049,6 +1072,7 @@ class MaterialDesignIcons {
|
||||
"mdi:circle-slice-7": 0xfaa3,
|
||||
"mdi:circle-slice-8": 0xfaa4,
|
||||
"mdi:circle-small": 0xf9de,
|
||||
"mdi:circular-saw": 0xfe73,
|
||||
"mdi:cisco-webex": 0xf145,
|
||||
"mdi:city": 0xf146,
|
||||
"mdi:city-variant": 0xfa35,
|
||||
@ -1148,6 +1172,11 @@ class MaterialDesignIcons {
|
||||
"mdi:collapse-all": 0xfaa5,
|
||||
"mdi:collapse-all-outline": 0xfaa6,
|
||||
"mdi:color-helper": 0xf179,
|
||||
"mdi:comma": 0xfe74,
|
||||
"mdi:comma-box": 0xfe75,
|
||||
"mdi:comma-box-outline": 0xfe76,
|
||||
"mdi:comma-circle": 0xfe77,
|
||||
"mdi:comma-circle-outline": 0xfe78,
|
||||
"mdi:comment": 0xf17a,
|
||||
"mdi:comment-account": 0xf17b,
|
||||
"mdi:comment-account-outline": 0xf17c,
|
||||
@ -1201,6 +1230,8 @@ class MaterialDesignIcons {
|
||||
"mdi:content-save-all": 0xf194,
|
||||
"mdi:content-save-edit": 0xfcd7,
|
||||
"mdi:content-save-edit-outline": 0xfcd8,
|
||||
"mdi:content-save-move": 0xfe79,
|
||||
"mdi:content-save-move-outline": 0xfe7a,
|
||||
"mdi:content-save-outline": 0xf817,
|
||||
"mdi:content-save-settings": 0xf61b,
|
||||
"mdi:content-save-settings-outline": 0xfb13,
|
||||
@ -1546,6 +1577,7 @@ class MaterialDesignIcons {
|
||||
"mdi:file-cancel-outline": 0xfda3,
|
||||
"mdi:file-chart": 0xf215,
|
||||
"mdi:file-check": 0xf216,
|
||||
"mdi:file-check-outline": 0xfe7b,
|
||||
"mdi:file-cloud": 0xf217,
|
||||
"mdi:file-compare": 0xf8a9,
|
||||
"mdi:file-delimited": 0xf218,
|
||||
@ -1573,9 +1605,11 @@ class MaterialDesignIcons {
|
||||
"mdi:file-move": 0xfab8,
|
||||
"mdi:file-multiple": 0xf222,
|
||||
"mdi:file-music": 0xf223,
|
||||
"mdi:file-music-outline": 0xfe7c,
|
||||
"mdi:file-outline": 0xf224,
|
||||
"mdi:file-pdf": 0xf225,
|
||||
"mdi:file-pdf-box": 0xf226,
|
||||
"mdi:file-pdf-outline": 0xfe7d,
|
||||
"mdi:file-percent": 0xf81d,
|
||||
"mdi:file-plus": 0xf751,
|
||||
"mdi:file-powerpoint": 0xf227,
|
||||
@ -1596,6 +1630,7 @@ class MaterialDesignIcons {
|
||||
"mdi:file-upload": 0xfa4c,
|
||||
"mdi:file-upload-outline": 0xfa4d,
|
||||
"mdi:file-video": 0xf22b,
|
||||
"mdi:file-video-outline": 0xfe10,
|
||||
"mdi:file-word": 0xf22c,
|
||||
"mdi:file-word-box": 0xf22d,
|
||||
"mdi:file-xml": 0xf22e,
|
||||
@ -1614,6 +1649,9 @@ class MaterialDesignIcons {
|
||||
"mdi:fire-truck": 0xf8aa,
|
||||
"mdi:firebase": 0xf966,
|
||||
"mdi:firefox": 0xf239,
|
||||
"mdi:fireplace": 0xfe11,
|
||||
"mdi:fireplace-off": 0xfe12,
|
||||
"mdi:firework": 0xfe13,
|
||||
"mdi:fish": 0xf23a,
|
||||
"mdi:flag": 0xf23b,
|
||||
"mdi:flag-checkered": 0xf23c,
|
||||
@ -1715,6 +1753,7 @@ class MaterialDesignIcons {
|
||||
"mdi:format-bold": 0xf264,
|
||||
"mdi:format-clear": 0xf265,
|
||||
"mdi:format-color-fill": 0xf266,
|
||||
"mdi:format-color-highlight": 0xfe14,
|
||||
"mdi:format-color-text": 0xf69d,
|
||||
"mdi:format-columns": 0xf8de,
|
||||
"mdi:format-float-center": 0xf267,
|
||||
@ -1769,6 +1808,7 @@ class MaterialDesignIcons {
|
||||
"mdi:format-text": 0xf284,
|
||||
"mdi:format-text-rotation-down": 0xfd4f,
|
||||
"mdi:format-text-rotation-none": 0xfd50,
|
||||
"mdi:format-text-variant": 0xfe15,
|
||||
"mdi:format-text-wrapping-clip": 0xfcea,
|
||||
"mdi:format-text-wrapping-overflow": 0xfceb,
|
||||
"mdi:format-text-wrapping-wrap": 0xfcec,
|
||||
@ -1805,6 +1845,22 @@ class MaterialDesignIcons {
|
||||
"mdi:fuse": 0xfc61,
|
||||
"mdi:fuse-blade": 0xfc62,
|
||||
"mdi:gamepad": 0xf296,
|
||||
"mdi:gamepad-circle": 0xfe16,
|
||||
"mdi:gamepad-circle-down": 0xfe17,
|
||||
"mdi:gamepad-circle-left": 0xfe18,
|
||||
"mdi:gamepad-circle-outline": 0xfe19,
|
||||
"mdi:gamepad-circle-right": 0xfe1a,
|
||||
"mdi:gamepad-circle-up": 0xfe1b,
|
||||
"mdi:gamepad-down": 0xfe1c,
|
||||
"mdi:gamepad-left": 0xfe1d,
|
||||
"mdi:gamepad-right": 0xfe1e,
|
||||
"mdi:gamepad-round": 0xfe1f,
|
||||
"mdi:gamepad-round-down": 0xfe7e,
|
||||
"mdi:gamepad-round-left": 0xfe7f,
|
||||
"mdi:gamepad-round-outline": 0xfe80,
|
||||
"mdi:gamepad-round-right": 0xfe81,
|
||||
"mdi:gamepad-round-up": 0xfe82,
|
||||
"mdi:gamepad-up": 0xfe83,
|
||||
"mdi:gamepad-variant": 0xf297,
|
||||
"mdi:gantry-crane": 0xfdad,
|
||||
"mdi:garage": 0xf6d8,
|
||||
@ -1820,6 +1876,7 @@ class MaterialDesignIcons {
|
||||
"mdi:gate-or": 0xf8e4,
|
||||
"mdi:gate-xnor": 0xf8e5,
|
||||
"mdi:gate-xor": 0xf8e6,
|
||||
"mdi:gatsby": 0xfe84,
|
||||
"mdi:gauge": 0xf29a,
|
||||
"mdi:gauge-empty": 0xf872,
|
||||
"mdi:gauge-full": 0xf873,
|
||||
@ -1848,7 +1905,8 @@ class MaterialDesignIcons {
|
||||
"mdi:ghost": 0xf2a0,
|
||||
"mdi:ghost-off": 0xf9f4,
|
||||
"mdi:gif": 0xfd54,
|
||||
"mdi:gift": 0xf2a1,
|
||||
"mdi:gift": 0xfe85,
|
||||
"mdi:gift-outline": 0xf2a1,
|
||||
"mdi:git": 0xf2a2,
|
||||
"mdi:github-box": 0xf2a3,
|
||||
"mdi:github-circle": 0xf2a4,
|
||||
@ -1915,6 +1973,7 @@ class MaterialDesignIcons {
|
||||
"mdi:grid": 0xf2c1,
|
||||
"mdi:grid-large": 0xf757,
|
||||
"mdi:grid-off": 0xf2c2,
|
||||
"mdi:grill": 0xfe86,
|
||||
"mdi:group": 0xf2c3,
|
||||
"mdi:guitar-acoustic": 0xf770,
|
||||
"mdi:guitar-electric": 0xf2c4,
|
||||
@ -1927,6 +1986,7 @@ class MaterialDesignIcons {
|
||||
"mdi:hamburger": 0xf684,
|
||||
"mdi:hammer": 0xf8e9,
|
||||
"mdi:hand": 0xfa4e,
|
||||
"mdi:hand-left": 0xfe87,
|
||||
"mdi:hand-okay": 0xfa4f,
|
||||
"mdi:hand-peace": 0xfa50,
|
||||
"mdi:hand-peace-variant": 0xfa51,
|
||||
@ -1934,6 +1994,8 @@ class MaterialDesignIcons {
|
||||
"mdi:hand-pointing-left": 0xfa53,
|
||||
"mdi:hand-pointing-right": 0xf2c7,
|
||||
"mdi:hand-pointing-up": 0xfa54,
|
||||
"mdi:hand-right": 0xfe88,
|
||||
"mdi:hand-saw": 0xfe89,
|
||||
"mdi:hanger": 0xf2c8,
|
||||
"mdi:hard-hat": 0xf96e,
|
||||
"mdi:harddisk": 0xf2ca,
|
||||
@ -2070,6 +2132,7 @@ class MaterialDesignIcons {
|
||||
"mdi:image-filter-none": 0xf2f6,
|
||||
"mdi:image-filter-tilt-shift": 0xf2f7,
|
||||
"mdi:image-filter-vintage": 0xf2f8,
|
||||
"mdi:image-frame": 0xfe8a,
|
||||
"mdi:image-move": 0xf9f7,
|
||||
"mdi:image-multiple": 0xf2f9,
|
||||
"mdi:image-off": 0xf82a,
|
||||
@ -2095,6 +2158,7 @@ class MaterialDesignIcons {
|
||||
"mdi:instapaper": 0xf2ff,
|
||||
"mdi:internet-explorer": 0xf300,
|
||||
"mdi:invert-colors": 0xf301,
|
||||
"mdi:invert-colors-off": 0xfe8b,
|
||||
"mdi:ip": 0xfa5e,
|
||||
"mdi:ip-network": 0xfa5f,
|
||||
"mdi:ip-network-outline": 0xfc6c,
|
||||
@ -2124,6 +2188,7 @@ class MaterialDesignIcons {
|
||||
"mdi:keyboard-caps": 0xf30e,
|
||||
"mdi:keyboard-close": 0xf30f,
|
||||
"mdi:keyboard-off": 0xf310,
|
||||
"mdi:keyboard-off-outline": 0xfe8c,
|
||||
"mdi:keyboard-outline": 0xf97a,
|
||||
"mdi:keyboard-return": 0xf311,
|
||||
"mdi:keyboard-settings": 0xf9f8,
|
||||
@ -2175,9 +2240,12 @@ class MaterialDesignIcons {
|
||||
"mdi:launch": 0xf327,
|
||||
"mdi:lava-lamp": 0xf7d4,
|
||||
"mdi:layers": 0xf328,
|
||||
"mdi:layers-minus": 0xfe8d,
|
||||
"mdi:layers-off": 0xf329,
|
||||
"mdi:layers-off-outline": 0xf9fc,
|
||||
"mdi:layers-outline": 0xf9fd,
|
||||
"mdi:layers-plus": 0xfe30,
|
||||
"mdi:layers-remove": 0xfe31,
|
||||
"mdi:lead-pencil": 0xf64f,
|
||||
"mdi:leaf": 0xf32a,
|
||||
"mdi:leaf-maple": 0xfc6f,
|
||||
@ -2202,6 +2270,8 @@ class MaterialDesignIcons {
|
||||
"mdi:lifebuoy": 0xf87d,
|
||||
"mdi:light-switch": 0xf97d,
|
||||
"mdi:lightbulb": 0xf335,
|
||||
"mdi:lightbulb-off": 0xfe32,
|
||||
"mdi:lightbulb-off-outline": 0xfe33,
|
||||
"mdi:lightbulb-on": 0xf6e7,
|
||||
"mdi:lightbulb-on-outline": 0xf6e8,
|
||||
"mdi:lightbulb-outline": 0xf336,
|
||||
@ -2374,6 +2444,7 @@ class MaterialDesignIcons {
|
||||
"mdi:monitor-lock": 0xfdb7,
|
||||
"mdi:monitor-multiple": 0xf37a,
|
||||
"mdi:monitor-off": 0xfd6c,
|
||||
"mdi:monitor-screenshot": 0xfe34,
|
||||
"mdi:monitor-star": 0xfdb8,
|
||||
"mdi:more": 0xf37b,
|
||||
"mdi:mother-nurse": 0xfcfd,
|
||||
@ -2437,8 +2508,11 @@ class MaterialDesignIcons {
|
||||
"mdi:new-box": 0xf394,
|
||||
"mdi:newspaper": 0xf395,
|
||||
"mdi:nfc": 0xf396,
|
||||
"mdi:nfc-off": 0xfe35,
|
||||
"mdi:nfc-search-variant": 0xfe36,
|
||||
"mdi:nfc-tap": 0xf397,
|
||||
"mdi:nfc-variant": 0xf398,
|
||||
"mdi:nfc-variant-off": 0xfe37,
|
||||
"mdi:ninja": 0xf773,
|
||||
"mdi:nintendo-switch": 0xf7e0,
|
||||
"mdi:nodejs": 0xf399,
|
||||
@ -2452,6 +2526,7 @@ class MaterialDesignIcons {
|
||||
"mdi:note-plus-outline": 0xf39d,
|
||||
"mdi:note-text": 0xf39e,
|
||||
"mdi:notebook": 0xf82d,
|
||||
"mdi:notebook-multiple": 0xfe38,
|
||||
"mdi:notification-clear-all": 0xf39f,
|
||||
"mdi:npm": 0xf6f6,
|
||||
"mdi:npm-variant": 0xf98e,
|
||||
@ -2573,7 +2648,7 @@ class MaterialDesignIcons {
|
||||
"mdi:page-previous-outline": 0xfb8f,
|
||||
"mdi:palette": 0xf3d8,
|
||||
"mdi:palette-advanced": 0xf3d9,
|
||||
"mdi:palette-outline": 0xfde8,
|
||||
"mdi:palette-outline": 0xfe6c,
|
||||
"mdi:palette-swatch": 0xf8b4,
|
||||
"mdi:pan": 0xfb90,
|
||||
"mdi:pan-bottom-left": 0xfb91,
|
||||
@ -2609,6 +2684,7 @@ class MaterialDesignIcons {
|
||||
"mdi:paw": 0xf3e9,
|
||||
"mdi:paw-off": 0xf657,
|
||||
"mdi:paypal": 0xf882,
|
||||
"mdi:pdf-box": 0xfe39,
|
||||
"mdi:peace": 0xf883,
|
||||
"mdi:pen": 0xf3ea,
|
||||
"mdi:pen-lock": 0xfdbe,
|
||||
@ -2667,6 +2743,10 @@ class MaterialDesignIcons {
|
||||
"mdi:pi-hole": 0xfdcd,
|
||||
"mdi:piano": 0xf67c,
|
||||
"mdi:pickaxe": 0xf8b6,
|
||||
"mdi:picture-in-picture-bottom-right": 0xfe3a,
|
||||
"mdi:picture-in-picture-bottom-right-outline": 0xfe3b,
|
||||
"mdi:picture-in-picture-top-right": 0xfe3c,
|
||||
"mdi:picture-in-picture-top-right-outline": 0xfe3d,
|
||||
"mdi:pier": 0xf886,
|
||||
"mdi:pier-crane": 0xf887,
|
||||
"mdi:pig": 0xf401,
|
||||
@ -2762,7 +2842,10 @@ class MaterialDesignIcons {
|
||||
"mdi:presentation-play": 0xf429,
|
||||
"mdi:printer": 0xf42a,
|
||||
"mdi:printer-3d": 0xf42b,
|
||||
"mdi:printer-3d-nozzle": 0xfe3e,
|
||||
"mdi:printer-3d-nozzle-outline": 0xfe3f,
|
||||
"mdi:printer-alert": 0xf42c,
|
||||
"mdi:printer-off": 0xfe40,
|
||||
"mdi:printer-settings": 0xf706,
|
||||
"mdi:printer-wireless": 0xfa0a,
|
||||
"mdi:priority-high": 0xf603,
|
||||
@ -2822,6 +2905,8 @@ class MaterialDesignIcons {
|
||||
"mdi:record": 0xf44a,
|
||||
"mdi:record-player": 0xf999,
|
||||
"mdi:record-rec": 0xf44b,
|
||||
"mdi:rectangle": 0xfe41,
|
||||
"mdi:rectangle-outline": 0xfe42,
|
||||
"mdi:recycle": 0xf44c,
|
||||
"mdi:reddit": 0xf44d,
|
||||
"mdi:redo": 0xf44e,
|
||||
@ -2866,6 +2951,7 @@ class MaterialDesignIcons {
|
||||
"mdi:ribbon": 0xf460,
|
||||
"mdi:rice": 0xf7e9,
|
||||
"mdi:ring": 0xf7ea,
|
||||
"mdi:rivet": 0xfe43,
|
||||
"mdi:road": 0xf461,
|
||||
"mdi:road-variant": 0xf462,
|
||||
"mdi:robot": 0xf6a8,
|
||||
@ -2908,6 +2994,7 @@ class MaterialDesignIcons {
|
||||
"mdi:satellite-uplink": 0xf908,
|
||||
"mdi:satellite-variant": 0xf471,
|
||||
"mdi:sausage": 0xf8b9,
|
||||
"mdi:saw-blade": 0xfe44,
|
||||
"mdi:saxophone": 0xf609,
|
||||
"mdi:scale": 0xf472,
|
||||
"mdi:scale-balance": 0xf5d1,
|
||||
@ -2919,10 +3006,10 @@ class MaterialDesignIcons {
|
||||
"mdi:screen-rotation": 0xf475,
|
||||
"mdi:screen-rotation-lock": 0xf476,
|
||||
"mdi:screw-flat-top": 0xfdcf,
|
||||
"mdi:screw-lag": 0xfdd0,
|
||||
"mdi:screw-machine-flat-top": 0xfdd1,
|
||||
"mdi:screw-machine-round-top": 0xfdd2,
|
||||
"mdi:screw-round-top": 0xfdd3,
|
||||
"mdi:screw-lag": 0xfe54,
|
||||
"mdi:screw-machine-flat-top": 0xfe55,
|
||||
"mdi:screw-machine-round-top": 0xfe56,
|
||||
"mdi:screw-round-top": 0xfe57,
|
||||
"mdi:screwdriver": 0xf477,
|
||||
"mdi:script": 0xfb9d,
|
||||
"mdi:script-outline": 0xf478,
|
||||
@ -2944,6 +3031,8 @@ class MaterialDesignIcons {
|
||||
"mdi:seatbelt": 0xfca1,
|
||||
"mdi:security": 0xf483,
|
||||
"mdi:security-network": 0xf484,
|
||||
"mdi:seed": 0xfe45,
|
||||
"mdi:seed-outline": 0xfe46,
|
||||
"mdi:select": 0xf485,
|
||||
"mdi:select-all": 0xf486,
|
||||
"mdi:select-color": 0xfd0d,
|
||||
@ -2956,8 +3045,8 @@ class MaterialDesignIcons {
|
||||
"mdi:selection-ellipse": 0xfd0e,
|
||||
"mdi:selection-off": 0xf776,
|
||||
"mdi:send": 0xf48a,
|
||||
"mdi:send-circle": 0xfdd4,
|
||||
"mdi:send-circle-outline": 0xfdd5,
|
||||
"mdi:send-circle": 0xfe58,
|
||||
"mdi:send-circle-outline": 0xfe59,
|
||||
"mdi:send-lock": 0xf7ec,
|
||||
"mdi:serial-port": 0xf65c,
|
||||
"mdi:server": 0xf48b,
|
||||
@ -3021,7 +3110,7 @@ class MaterialDesignIcons {
|
||||
"mdi:ship-wheel": 0xf832,
|
||||
"mdi:shoe-formal": 0xfb22,
|
||||
"mdi:shoe-heel": 0xfb23,
|
||||
"mdi:shoe-print": 0xfdd6,
|
||||
"mdi:shoe-print": 0xfe5a,
|
||||
"mdi:shopify": 0xfadd,
|
||||
"mdi:shopping": 0xf49a,
|
||||
"mdi:shopping-music": 0xf49b,
|
||||
@ -3047,14 +3136,15 @@ class MaterialDesignIcons {
|
||||
"mdi:signal-cellular-2": 0xf8bc,
|
||||
"mdi:signal-cellular-3": 0xf8bd,
|
||||
"mdi:signal-cellular-outline": 0xf8be,
|
||||
"mdi:signal-distance-variant": 0xfe47,
|
||||
"mdi:signal-hspa": 0xf714,
|
||||
"mdi:signal-hspa-plus": 0xf715,
|
||||
"mdi:signal-off": 0xf782,
|
||||
"mdi:signal-variant": 0xf60a,
|
||||
"mdi:signature": 0xfdd7,
|
||||
"mdi:signature-freehand": 0xfdd8,
|
||||
"mdi:signature-image": 0xfdd9,
|
||||
"mdi:signature-text": 0xfdda,
|
||||
"mdi:signature": 0xfe5b,
|
||||
"mdi:signature-freehand": 0xfe5c,
|
||||
"mdi:signature-image": 0xfe5d,
|
||||
"mdi:signature-text": 0xfe5e,
|
||||
"mdi:silo": 0xfb24,
|
||||
"mdi:silverware": 0xf4a3,
|
||||
"mdi:silverware-fork": 0xf4a4,
|
||||
@ -3087,8 +3177,8 @@ class MaterialDesignIcons {
|
||||
"mdi:slackware": 0xf90a,
|
||||
"mdi:sleep": 0xf4b2,
|
||||
"mdi:sleep-off": 0xf4b3,
|
||||
"mdi:slope-downhill": 0xfddb,
|
||||
"mdi:slope-uphill": 0xfddc,
|
||||
"mdi:slope-downhill": 0xfe5f,
|
||||
"mdi:slope-uphill": 0xfe60,
|
||||
"mdi:smog": 0xfa70,
|
||||
"mdi:smoke-detector": 0xf392,
|
||||
"mdi:smoking": 0xf4b4,
|
||||
@ -3129,6 +3219,7 @@ class MaterialDesignIcons {
|
||||
"mdi:spa": 0xfcad,
|
||||
"mdi:spa-outline": 0xfcae,
|
||||
"mdi:space-invaders": 0xfba5,
|
||||
"mdi:spade": 0xfe48,
|
||||
"mdi:speaker": 0xf4c3,
|
||||
"mdi:speaker-bluetooth": 0xf9a1,
|
||||
"mdi:speaker-multiple": 0xfd14,
|
||||
@ -3142,6 +3233,8 @@ class MaterialDesignIcons {
|
||||
"mdi:spotlight-beam": 0xf4c9,
|
||||
"mdi:spray": 0xf665,
|
||||
"mdi:spray-bottle": 0xfadf,
|
||||
"mdi:sprout": 0xfe49,
|
||||
"mdi:sprout-outline": 0xfe4a,
|
||||
"mdi:square": 0xf763,
|
||||
"mdi:square-edit-outline": 0xf90b,
|
||||
"mdi:square-inc": 0xf4ca,
|
||||
@ -3246,6 +3339,7 @@ class MaterialDesignIcons {
|
||||
"mdi:table-row-remove": 0xf4f5,
|
||||
"mdi:table-search": 0xf90e,
|
||||
"mdi:table-settings": 0xf837,
|
||||
"mdi:table-tennis": 0xfe4b,
|
||||
"mdi:tablet": 0xf4f6,
|
||||
"mdi:tablet-android": 0xf4f7,
|
||||
"mdi:tablet-cellphone": 0xf9a6,
|
||||
@ -3301,12 +3395,12 @@ class MaterialDesignIcons {
|
||||
"mdi:theater": 0xf50d,
|
||||
"mdi:theme-light-dark": 0xf50e,
|
||||
"mdi:thermometer": 0xf50f,
|
||||
"mdi:thermometer-alert": 0xfddd,
|
||||
"mdi:thermometer-chevron-down": 0xfdde,
|
||||
"mdi:thermometer-chevron-up": 0xfddf,
|
||||
"mdi:thermometer-alert": 0xfe61,
|
||||
"mdi:thermometer-chevron-down": 0xfe62,
|
||||
"mdi:thermometer-chevron-up": 0xfe63,
|
||||
"mdi:thermometer-lines": 0xf510,
|
||||
"mdi:thermometer-minus": 0xfde0,
|
||||
"mdi:thermometer-plus": 0xfde1,
|
||||
"mdi:thermometer-minus": 0xfe64,
|
||||
"mdi:thermometer-plus": 0xfe65,
|
||||
"mdi:thermostat": 0xf393,
|
||||
"mdi:thermostat-box": 0xf890,
|
||||
"mdi:thought-bubble": 0xf7f5,
|
||||
@ -3384,12 +3478,13 @@ class MaterialDesignIcons {
|
||||
"mdi:transition": 0xf914,
|
||||
"mdi:transition-masked": 0xf915,
|
||||
"mdi:translate": 0xf5ca,
|
||||
"mdi:translate-off": 0xfde2,
|
||||
"mdi:translate-off": 0xfe66,
|
||||
"mdi:transmission-tower": 0xfd1a,
|
||||
"mdi:trash-can": 0xfa78,
|
||||
"mdi:trash-can-outline": 0xfa79,
|
||||
"mdi:treasure-chest": 0xf725,
|
||||
"mdi:tree": 0xf531,
|
||||
"mdi:tree-outline": 0xfe4c,
|
||||
"mdi:trello": 0xf532,
|
||||
"mdi:trending-down": 0xf533,
|
||||
"mdi:trending-neutral": 0xf534,
|
||||
@ -3450,7 +3545,7 @@ class MaterialDesignIcons {
|
||||
"mdi:upload-multiple": 0xf83c,
|
||||
"mdi:upload-network": 0xf6f5,
|
||||
"mdi:upload-network-outline": 0xfcb4,
|
||||
"mdi:upload-outline": 0xfde3,
|
||||
"mdi:upload-outline": 0xfe67,
|
||||
"mdi:usb": 0xf553,
|
||||
"mdi:van-passenger": 0xf7f9,
|
||||
"mdi:van-utility": 0xf7fa,
|
||||
@ -3503,6 +3598,9 @@ class MaterialDesignIcons {
|
||||
"mdi:view-array": 0xf56b,
|
||||
"mdi:view-carousel": 0xf56c,
|
||||
"mdi:view-column": 0xf56d,
|
||||
"mdi:view-comfy": 0xfe4d,
|
||||
"mdi:view-compact": 0xfe4e,
|
||||
"mdi:view-compact-outline": 0xfe4f,
|
||||
"mdi:view-dashboard": 0xf56e,
|
||||
"mdi:view-dashboard-outline": 0xfa1c,
|
||||
"mdi:view-dashboard-variant": 0xf842,
|
||||
@ -3537,11 +3635,12 @@ class MaterialDesignIcons {
|
||||
"mdi:volume-mute": 0xf75e,
|
||||
"mdi:volume-off": 0xf581,
|
||||
"mdi:volume-plus": 0xf75c,
|
||||
"mdi:volume-variant-off": 0xfde4,
|
||||
"mdi:volume-variant-off": 0xfe68,
|
||||
"mdi:vote": 0xfa1e,
|
||||
"mdi:vote-outline": 0xfa1f,
|
||||
"mdi:vpn": 0xf582,
|
||||
"mdi:vuejs": 0xf843,
|
||||
"mdi:vuetify": 0xfe50,
|
||||
"mdi:walk": 0xf583,
|
||||
"mdi:wall": 0xf7fd,
|
||||
"mdi:wall-sconce": 0xf91b,
|
||||
@ -3552,7 +3651,7 @@ class MaterialDesignIcons {
|
||||
"mdi:wallet-membership": 0xf586,
|
||||
"mdi:wallet-outline": 0xfbb9,
|
||||
"mdi:wallet-travel": 0xf587,
|
||||
"mdi:wallpaper": 0xfde5,
|
||||
"mdi:wallpaper": 0xfe69,
|
||||
"mdi:wan": 0xf588,
|
||||
"mdi:washing-machine": 0xf729,
|
||||
"mdi:watch": 0xf589,
|
||||
@ -3565,13 +3664,14 @@ class MaterialDesignIcons {
|
||||
"mdi:watch-vibrate-off": 0xfcb6,
|
||||
"mdi:water": 0xf58c,
|
||||
"mdi:water-off": 0xf58d,
|
||||
"mdi:water-outline": 0xfde6,
|
||||
"mdi:water-outline": 0xfe6a,
|
||||
"mdi:water-percent": 0xf58e,
|
||||
"mdi:water-pump": 0xf58f,
|
||||
"mdi:watermark": 0xf612,
|
||||
"mdi:waves": 0xf78c,
|
||||
"mdi:waze": 0xfbba,
|
||||
"mdi:weather-cloudy": 0xf590,
|
||||
"mdi:weather-cloudy-arrow-right": 0xfe51,
|
||||
"mdi:weather-fog": 0xf591,
|
||||
"mdi:weather-hail": 0xf592,
|
||||
"mdi:weather-hurricane": 0xf897,
|
||||
@ -3608,7 +3708,7 @@ class MaterialDesignIcons {
|
||||
"mdi:widgets": 0xf72b,
|
||||
"mdi:wifi": 0xf5a9,
|
||||
"mdi:wifi-off": 0xf5aa,
|
||||
"mdi:wifi-star": 0xfde7,
|
||||
"mdi:wifi-star": 0xfe6b,
|
||||
"mdi:wifi-strength-1": 0xf91e,
|
||||
"mdi:wifi-strength-1-alert": 0xf91f,
|
||||
"mdi:wifi-strength-1-lock": 0xf920,
|
||||
@ -3659,7 +3759,9 @@ class MaterialDesignIcons {
|
||||
"mdi:xbox-controller-battery-low": 0xf74d,
|
||||
"mdi:xbox-controller-battery-medium": 0xf74e,
|
||||
"mdi:xbox-controller-battery-unknown": 0xf74f,
|
||||
"mdi:xbox-controller-menu": 0xfe52,
|
||||
"mdi:xbox-controller-off": 0xf5bb,
|
||||
"mdi:xbox-controller-view": 0xfe53,
|
||||
"mdi:xda": 0xf5bc,
|
||||
"mdi:xing": 0xf5bd,
|
||||
"mdi:xing-box": 0xf5be,
|
||||
|
@ -1,10 +1,9 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class EntityViewPage extends StatefulWidget {
|
||||
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
|
||||
EntityViewPage({Key key, @required this.entityId}) : super(key: key);
|
||||
|
||||
final String entityId;
|
||||
final HomeAssistant homeAssistant;
|
||||
|
||||
@override
|
||||
_EntityViewPageState createState() => new _EntityViewPageState();
|
||||
@ -31,7 +30,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
||||
}
|
||||
|
||||
void _prepareData() async {
|
||||
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
|
||||
_title = HomeAssistant().entities.get(widget.entityId).displayName;
|
||||
}
|
||||
|
||||
|
||||
@ -46,10 +45,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: new Text(_title),
|
||||
),
|
||||
body: HomeAssistantModel(
|
||||
homeAssistant: widget.homeAssistant,
|
||||
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
|
||||
),
|
||||
body: HomeAssistant().entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class LogViewPage extends StatefulWidget {
|
||||
LogViewPage({Key key, this.title}) : super(key: key);
|
820
lib/pages/main.page.dart
Normal file
@ -0,0 +1,820 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
MainPage({Key key, this.title}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
_MainPageState createState() => new _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||
|
||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _settingsSubscription;
|
||||
StreamSubscription _serviceCallSubscription;
|
||||
StreamSubscription _showEntityPageSubscription;
|
||||
StreamSubscription _showErrorSubscription;
|
||||
StreamSubscription _startAuthSubscription;
|
||||
StreamSubscription _showPopupDialogSubscription;
|
||||
StreamSubscription _showPopupMessageSubscription;
|
||||
StreamSubscription _reloadUISubscription;
|
||||
StreamSubscription _showPageSubscription;
|
||||
int _previousViewCount;
|
||||
bool _showLoginButton = false;
|
||||
bool _preventAppRefresh = false;
|
||||
String _savedSharedText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final Stream purchaseUpdates =
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
_subscription = purchaseUpdates.listen((purchases) {
|
||||
_handlePurchaseUpdates(purchases);
|
||||
});
|
||||
super.initState();
|
||||
enableShareReceiving();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_firebaseMessaging.configure(
|
||||
onLaunch: (data) {
|
||||
Logger.d("Notification [onLaunch]: $data");
|
||||
return Future.value();
|
||||
},
|
||||
onMessage: (data) {
|
||||
Logger.d("Notification [onMessage]: $data");
|
||||
return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
|
||||
},
|
||||
onResume: (data) {
|
||||
Logger.d("Notification [onResume]: $data");
|
||||
return Future.value();
|
||||
}
|
||||
);
|
||||
|
||||
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
||||
|
||||
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
||||
var initializationSettingsAndroid =
|
||||
new AndroidInitializationSettings('mini_icon');
|
||||
var initializationSettingsIOS = new IOSInitializationSettings(
|
||||
onDidReceiveLocalNotification: null);
|
||||
var initializationSettings = new InitializationSettings(
|
||||
initializationSettingsAndroid, initializationSettingsIOS);
|
||||
flutterLocalNotificationsPlugin.initialize(initializationSettings,
|
||||
onSelectNotification: onSelectNotification);
|
||||
|
||||
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
||||
Logger.d("Settings change event: reconnect=${event.reconnect}");
|
||||
if (event.reconnect) {
|
||||
_preventAppRefresh = false;
|
||||
_fullLoad();
|
||||
}
|
||||
});
|
||||
|
||||
_fullLoad();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@override void receiveShare(Share shared) {
|
||||
if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) {
|
||||
_savedSharedText = shared.text;
|
||||
}
|
||||
}
|
||||
|
||||
Future onSelectNotification(String payload) async {
|
||||
if (payload != null) {
|
||||
Logger.d('Notification clicked: ' + payload);
|
||||
}
|
||||
}
|
||||
|
||||
Future _showNotification({String title, String text}) async {
|
||||
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
|
||||
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
|
||||
importance: Importance.Max, priority: Priority.High);
|
||||
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
|
||||
var platformChannelSpecifics = new NotificationDetails(
|
||||
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
title ?? appName,
|
||||
text,
|
||||
platformChannelSpecifics
|
||||
);
|
||||
}
|
||||
|
||||
void _fullLoad() async {
|
||||
_showInfoBottomBar(progress: true,);
|
||||
_subscribe().then((_) {
|
||||
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||
_fetchData();
|
||||
StartupUserMessagesManager().checkMessagesToShow();
|
||||
}, onError: (e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _quickLoad() {
|
||||
_hideBottomBar();
|
||||
_showInfoBottomBar(progress: true,);
|
||||
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
||||
_fetchData();
|
||||
//StartupUserMessagesManager().checkMessagesToShow();
|
||||
}, onError: (e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
}
|
||||
|
||||
_fetchData() async {
|
||||
if (_savedSharedText != null && !HomeAssistant().isNoEntities) {
|
||||
Logger.d("Got shared text: $_savedSharedText");
|
||||
Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText});
|
||||
_savedSharedText = null;
|
||||
}
|
||||
await HomeAssistant().fetchData().then((_) {
|
||||
_hideBottomBar();
|
||||
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||
if (_previousViewCount != currentViewCount) {
|
||||
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
||||
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
||||
_previousViewCount = currentViewCount;
|
||||
}
|
||||
}).catchError((e) {
|
||||
if (e is HAError) {
|
||||
_setErrorState(e);
|
||||
} else {
|
||||
_setErrorState(HAError(e.toString()));
|
||||
}
|
||||
});
|
||||
eventBus.fire(RefreshDataFinishedEvent());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
Logger.d("$state");
|
||||
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||
_quickLoad();
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePurchaseUpdates(purchase) {
|
||||
if (purchase is List<PurchaseDetails>) {
|
||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||
eventBus.fire(ShowPopupMessageEvent(
|
||||
title: "Thanks a lot!",
|
||||
body: "Thank you for supporting HA Client development!",
|
||||
buttonText: "Ok"
|
||||
));
|
||||
} else {
|
||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||
}
|
||||
} else {
|
||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
||||
}
|
||||
}
|
||||
|
||||
Future _subscribe() {
|
||||
Completer completer = Completer();
|
||||
if (_stateSubscription == null) {
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.needToRebuildUI) {
|
||||
Logger.d("New entity. Need to rebuild UI");
|
||||
_quickLoad();
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (_reloadUISubscription == null) {
|
||||
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
||||
_quickLoad();
|
||||
});
|
||||
}
|
||||
if (_showPopupDialogSubscription == null) {
|
||||
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
|
||||
_showPopupDialog(
|
||||
title: event.title,
|
||||
body: event.body,
|
||||
onPositive: event.onPositive,
|
||||
onNegative: event.onNegative,
|
||||
positiveText: event.positiveText,
|
||||
negativeText: event.negativeText
|
||||
);
|
||||
});
|
||||
}
|
||||
if (_showPopupMessageSubscription == null) {
|
||||
_showPopupMessageSubscription = eventBus.on<ShowPopupMessageEvent>().listen((event){
|
||||
_showPopupDialog(
|
||||
title: event.title,
|
||||
body: event.body,
|
||||
onPositive: event.onButtonClick,
|
||||
positiveText: event.buttonText,
|
||||
negativeText: null
|
||||
);
|
||||
});
|
||||
}
|
||||
if (_serviceCallSubscription == null) {
|
||||
_serviceCallSubscription =
|
||||
eventBus.on<ServiceCallEvent>().listen((event) {
|
||||
_callService(event.domain, event.service, event.entityId,
|
||||
event.additionalParams);
|
||||
});
|
||||
}
|
||||
|
||||
if (_showEntityPageSubscription == null) {
|
||||
_showEntityPageSubscription =
|
||||
eventBus.on<ShowEntityPageEvent>().listen((event) {
|
||||
_showEntityPage(event.entity.entityId);
|
||||
});
|
||||
}
|
||||
|
||||
if (_showPageSubscription == null) {
|
||||
_showPageSubscription =
|
||||
eventBus.on<ShowPageEvent>().listen((event) {
|
||||
_showPage(event.path, event.goBackFirst);
|
||||
});
|
||||
}
|
||||
|
||||
if (_showErrorSubscription == null) {
|
||||
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
||||
_showErrorBottomBar(event.error);
|
||||
});
|
||||
}
|
||||
|
||||
if (_startAuthSubscription == null) {
|
||||
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
|
||||
setState(() {
|
||||
_showLoginButton = event.showButton;
|
||||
});
|
||||
if (event.showButton) {
|
||||
_showOAuth();
|
||||
} else {
|
||||
_preventAppRefresh = false;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_firebaseMessaging.getToken().then((String token) {
|
||||
HomeAssistant().fcmToken = token;
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void _showOAuth() {
|
||||
_preventAppRefresh = true;
|
||||
Navigator.of(context).pushNamed('/login');
|
||||
}
|
||||
|
||||
_setErrorState(HAError e) {
|
||||
if (e == null) {
|
||||
_showErrorBottomBar(
|
||||
HAError("Unknown error")
|
||||
);
|
||||
} else {
|
||||
_showErrorBottomBar(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _showPopupDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) {
|
||||
List<Widget> buttons = [];
|
||||
buttons.add(FlatButton(
|
||||
child: new Text("$positiveText"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
if (onPositive != null) {
|
||||
onPositive();
|
||||
}
|
||||
},
|
||||
));
|
||||
if (negativeText != null) {
|
||||
buttons.add(FlatButton(
|
||||
child: new Text("$negativeText"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
if (onNegative != null) {
|
||||
onNegative();
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
// flutter defined function
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
// return object of type Dialog
|
||||
return AlertDialog(
|
||||
title: new Text("$title"),
|
||||
content: new Text("$body"),
|
||||
actions: buttons,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//TODO remove this shit
|
||||
void _callService(String domain, String service, String entityId, Map additionalParams) {
|
||||
_showInfoBottomBar(
|
||||
message: "Calling $domain.$service",
|
||||
duration: Duration(seconds: 3)
|
||||
);
|
||||
ConnectionManager().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
|
||||
}
|
||||
|
||||
void _showEntityPage(String entityId) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EntityViewPage(entityId: entityId),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _showPage(String path, bool goBackFirst) {
|
||||
if (goBackFirst) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
List<Tab> buildUIViewTabs() {
|
||||
List<Tab> result = [];
|
||||
|
||||
if (HomeAssistant().ui.views.isNotEmpty) {
|
||||
HomeAssistant().ui.views.forEach((HAView view) {
|
||||
result.add(view.buildTab());
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Drawer _buildAppDrawer() {
|
||||
List<Widget> menuItems = [];
|
||||
menuItems.add(
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: Text(HomeAssistant().userName),
|
||||
accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"),
|
||||
onDetailsPressed: () {
|
||||
final flutterWebViewPlugin = new FlutterWebviewPlugin();
|
||||
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
|
||||
if (viewState.type == WebViewState.startLoad) {
|
||||
Logger.d("[WebView] Injecting external auth JS");
|
||||
rootBundle.loadString('assets/js/externalAuth.js').then((js){
|
||||
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
|
||||
});
|
||||
}
|
||||
});
|
||||
Navigator.of(context).pushNamed(
|
||||
"/webview",
|
||||
arguments: {
|
||||
"url": "${ConnectionManager().httpWebHost}/profile?external_auth=1",
|
||||
"title": "Profile"
|
||||
}
|
||||
);
|
||||
},
|
||||
currentAccountPicture: CircleAvatar(
|
||||
child: Text(
|
||||
HomeAssistant().userAvatarText,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
if (HomeAssistant().panels.isNotEmpty) {
|
||||
HomeAssistant().panels.forEach((Panel panel) {
|
||||
if (!panel.isHidden) {
|
||||
menuItems.add(
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("${panel.title}"),
|
||||
Container(width: 4.0,),
|
||||
panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
panel.handleOpen(context);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
menuItems.addAll([
|
||||
Divider(),
|
||||
ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
|
||||
title: Text("Connection settings"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/connection-settings');
|
||||
},
|
||||
)
|
||||
]);
|
||||
menuItems.addAll([
|
||||
Divider(),
|
||||
new ListTile(
|
||||
leading: Icon(Icons.insert_drive_file),
|
||||
title: Text("Log"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/log-view');
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
|
||||
title: Text("Report an issue"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:food")),
|
||||
title: Text("Support app development"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/putchase');
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
new ListTile(
|
||||
leading: Icon(Icons.help),
|
||||
title: Text("Help"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("http://ha-client.homemade.systems/docs");
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
||||
title: Text("Join Discord channel"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("https://discord.gg/AUzEvwn");
|
||||
},
|
||||
),
|
||||
new AboutListTile(
|
||||
aboutBoxChildren: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("http://ha-client.homemade.systems/");
|
||||
},
|
||||
child: Text(
|
||||
"ha-client.homemade.systems",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 10.0,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
|
||||
},
|
||||
child: Text(
|
||||
"Terms and Conditions",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 10.0,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
|
||||
},
|
||||
child: Text(
|
||||
"Privacy Policy",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
applicationName: appName,
|
||||
applicationVersion: appVersion
|
||||
)
|
||||
]);
|
||||
return new Drawer(
|
||||
child: ListView(
|
||||
children: menuItems,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _hideBottomBar() {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
setState(() {
|
||||
_showBottomBar = false;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _bottomBarAction;
|
||||
bool _showBottomBar = false;
|
||||
String _bottomBarText;
|
||||
bool _bottomBarProgress;
|
||||
Color _bottomBarColor;
|
||||
Timer _bottomBarTimer;
|
||||
|
||||
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
||||
_bottomBarTimer?.cancel();
|
||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
||||
_bottomBarColor = Colors.grey.shade50;
|
||||
setState(() {
|
||||
_bottomBarText = message;
|
||||
_bottomBarProgress = progress;
|
||||
_showBottomBar = true;
|
||||
});
|
||||
if (duration != null) {
|
||||
_bottomBarTimer = Timer(duration, () {
|
||||
_hideBottomBar();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showErrorBottomBar(HAError error) {
|
||||
TextStyle textStyle = TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: Sizes.nameFontSize
|
||||
);
|
||||
_bottomBarColor = Colors.red.shade100;
|
||||
List<Widget> actions = [];
|
||||
error.actions.forEach((HAErrorAction action) {
|
||||
switch (action.type) {
|
||||
case HAErrorActionType.FULL_RELOAD: {
|
||||
actions.add(FlatButton(
|
||||
child: Text("${action.title}", style: textStyle),
|
||||
onPressed: () {
|
||||
_fullLoad();
|
||||
},
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
case HAErrorActionType.QUICK_RELOAD: {
|
||||
actions.add(FlatButton(
|
||||
child: Text("${action.title}", style: textStyle),
|
||||
onPressed: () {
|
||||
_quickLoad();
|
||||
},
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
case HAErrorActionType.RELOGIN: {
|
||||
actions.add(FlatButton(
|
||||
child: Text("${action.title}", style: textStyle),
|
||||
onPressed: () {
|
||||
ConnectionManager().logout().then((_) => _fullLoad());
|
||||
},
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
case HAErrorActionType.URL: {
|
||||
actions.add(FlatButton(
|
||||
child: Text("${action.title}", style: textStyle),
|
||||
onPressed: () {
|
||||
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
|
||||
},
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
|
||||
actions.add(FlatButton(
|
||||
child: Text("${action.title}", style: textStyle),
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/connection-settings');
|
||||
},
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (actions.isNotEmpty) {
|
||||
_bottomBarAction = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: actions,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
);
|
||||
} else {
|
||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
setState(() {
|
||||
_bottomBarProgress = false;
|
||||
_bottomBarText = "${error.message}";
|
||||
_showBottomBar = true;
|
||||
});
|
||||
}
|
||||
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
|
||||
Widget _buildScaffoldBody(bool empty) {
|
||||
List<PopupMenuItem<String>> popupMenuItems = [];
|
||||
|
||||
popupMenuItems.add(PopupMenuItem<String>(
|
||||
child: new Text("Reload"),
|
||||
value: "reload",
|
||||
));
|
||||
List<Widget> emptyBody = [
|
||||
Text("."),
|
||||
];
|
||||
if (ConnectionManager().isAuthenticated) {
|
||||
_showLoginButton = false;
|
||||
popupMenuItems.add(
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Logout"),
|
||||
value: "logout",
|
||||
));
|
||||
}
|
||||
if (_showLoginButton) {
|
||||
emptyBody = [
|
||||
FlatButton(
|
||||
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
||||
color: Colors.blue,
|
||||
onPressed: () => _fullLoad(),
|
||||
)
|
||||
];
|
||||
}
|
||||
return NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
pinned: true,
|
||||
primary: true,
|
||||
title: Text(HomeAssistant().locationName ?? ""),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:television"), color: Colors.white,),
|
||||
onPressed: () => Navigator.pushNamed(context, "/play-media", arguments: {"url": ""})
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical"), color: Colors.white,),
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
||||
context: context,
|
||||
items: popupMenuItems
|
||||
).then((String val) {
|
||||
if (val == "reload") {
|
||||
_quickLoad();
|
||||
} else if (val == "logout") {
|
||||
HomeAssistant().logout().then((_) {
|
||||
_quickLoad();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
],
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
},
|
||||
),
|
||||
bottom: empty ? null : TabBar(
|
||||
controller: _viewsTabController,
|
||||
tabs: buildUIViewTabs(),
|
||||
isScrollable: true,
|
||||
),
|
||||
),
|
||||
|
||||
];
|
||||
},
|
||||
body: empty ?
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: emptyBody
|
||||
),
|
||||
)
|
||||
:
|
||||
HomeAssistant().buildViews(context, _viewsTabController),
|
||||
);
|
||||
}
|
||||
|
||||
TabController _viewsTabController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget bottomBar;
|
||||
if (_showBottomBar) {
|
||||
List<Widget> bottomBarChildren = [];
|
||||
if (_bottomBarText != null) {
|
||||
bottomBarChildren.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
|
||||
Sizes.rowPadding),
|
||||
child: Text(
|
||||
"$_bottomBarText",
|
||||
textAlign: TextAlign.left,
|
||||
softWrap: true,
|
||||
),
|
||||
)
|
||||
|
||||
);
|
||||
}
|
||||
if (_bottomBarProgress) {
|
||||
bottomBarChildren.add(
|
||||
CollectionScaleTransition(
|
||||
children: <Widget>[
|
||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
|
||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
|
||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (bottomBarChildren.isNotEmpty) {
|
||||
bottomBar = Container(
|
||||
color: _bottomBarColor,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: bottomBarChildren,
|
||||
),
|
||||
),
|
||||
_bottomBarAction
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// This method is rerun every time setState is called.
|
||||
if (HomeAssistant().isNoViews) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
primary: false,
|
||||
drawer: _buildAppDrawer(),
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: _buildScaffoldBody(true)
|
||||
);
|
||||
} else {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
drawer: _buildAppDrawer(),
|
||||
primary: false,
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: _buildScaffoldBody(false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
flutterWebviewPlugin.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_viewsTabController?.dispose();
|
||||
_stateSubscription?.cancel();
|
||||
_settingsSubscription?.cancel();
|
||||
_serviceCallSubscription?.cancel();
|
||||
_showPopupDialogSubscription?.cancel();
|
||||
_showPopupMessageSubscription?.cancel();
|
||||
_showEntityPageSubscription?.cancel();
|
||||
_showErrorSubscription?.cancel();
|
||||
_startAuthSubscription?.cancel();
|
||||
_subscription?.cancel();
|
||||
_showPageSubscription?.cancel();
|
||||
_reloadUISubscription?.cancel();
|
||||
//TODO disconnect
|
||||
//widget.homeAssistant?.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class PanelPage extends StatefulWidget {
|
||||
PanelPage({Key key, this.title, this.panel}) : super(key: key);
|
||||
@ -12,8 +12,6 @@ class PanelPage extends StatefulWidget {
|
||||
|
||||
class _PanelPageState extends State<PanelPage> {
|
||||
|
||||
List<ConfigurationItem> _items;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
225
lib/pages/play_media.page.dart
Normal file
@ -0,0 +1,225 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class PlayMediaPage extends StatefulWidget {
|
||||
|
||||
final String mediaUrl;
|
||||
|
||||
PlayMediaPage({Key key, this.mediaUrl}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PlayMediaPageState createState() => new _PlayMediaPageState();
|
||||
}
|
||||
|
||||
class _PlayMediaPageState extends State<PlayMediaPage> {
|
||||
|
||||
bool _loaded = false;
|
||||
String _error = "";
|
||||
String _validationMessage = "";
|
||||
List<Entity> _players;
|
||||
String _mediaUrl;
|
||||
String _contentType;
|
||||
bool _useMediaExtractor = false;
|
||||
bool _isMediaExtractorExist = false;
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _refreshDataSubscription;
|
||||
final List<String> _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mediaUrl = widget.mediaUrl;
|
||||
_contentType = _contentTypes[0];
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.entityId.contains("media_player")) {
|
||||
Logger.d("State change event handled by play media page: ${event.entityId}");
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||
_loadMediaEntities();
|
||||
});
|
||||
_loadMediaEntities();
|
||||
}
|
||||
|
||||
_loadMediaEntities() async {
|
||||
if (HomeAssistant().isNoEntities) {
|
||||
setState(() {
|
||||
_loaded = false;
|
||||
});
|
||||
} else {
|
||||
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
|
||||
//_useMediaExtractor = _isMediaExtractorExist;
|
||||
_players = HomeAssistant().entities.getByDomains(["media_player"]);
|
||||
setState(() {
|
||||
if (_players.isNotEmpty) {
|
||||
_loaded = true;
|
||||
} else {
|
||||
_loaded = false;
|
||||
_error = "Looks like you don't have any media player";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _playMedia(Entity entity) {
|
||||
if (_mediaUrl == null || _mediaUrl.isEmpty) {
|
||||
setState(() {
|
||||
_validationMessage = "Media url must be specified";
|
||||
});
|
||||
} else {
|
||||
String serviceDomain;
|
||||
if (_useMediaExtractor) {
|
||||
serviceDomain = "media_extractor";
|
||||
} else {
|
||||
serviceDomain = "media_player";
|
||||
}
|
||||
Navigator.pop(context);
|
||||
ConnectionManager().callService(
|
||||
domain: serviceDomain,
|
||||
entityId: entity.entityId,
|
||||
service: "play_media",
|
||||
additionalServiceData: {
|
||||
"media_content_id": _mediaUrl,
|
||||
"media_content_type": _contentType
|
||||
}
|
||||
);
|
||||
eventBus.fire(ShowEntityPageEvent(entity));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (!_loaded) {
|
||||
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
||||
} else {
|
||||
List<Widget> children = [];
|
||||
children.add(CardHeader(name: "Media:"));
|
||||
children.add(
|
||||
TextField(
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Media url"
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _mediaUrl)),
|
||||
onChanged: (value) {
|
||||
_mediaUrl = value;
|
||||
}
|
||||
),
|
||||
);
|
||||
if (_validationMessage.isNotEmpty) {
|
||||
children.add(Text(
|
||||
"$_validationMessage",
|
||||
style: TextStyle(color: Colors.red)
|
||||
));
|
||||
}
|
||||
children.addAll(<Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
DropdownButton<String>(
|
||||
value: _contentType,
|
||||
isExpanded: true,
|
||||
items: _contentTypes.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: new Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_contentType = value;
|
||||
});
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
if (_isMediaExtractorExist) {
|
||||
children.addAll(<Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text("Use media extractor"),
|
||||
),
|
||||
Switch(
|
||||
value: _useMediaExtractor,
|
||||
onChanged: (value) => setState((){_useMediaExtractor = value;}),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: Sizes.rowPadding,
|
||||
)
|
||||
]
|
||||
);
|
||||
} else {
|
||||
children.addAll(<Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text("You can use media extractor here"),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Launcher.launchURLInCustomTab(
|
||||
context: context,
|
||||
url: "https://www.home-assistant.io/components/media_extractor/"
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"How?",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: Sizes.doubleRowPadding,
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
children.add(CardHeader(name: "Play on:"));
|
||||
children.addAll(
|
||||
_players.map((player) => InkWell(
|
||||
child: EntityModel(
|
||||
entityWrapper: EntityWrapper(entity: player),
|
||||
handleTap: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||
child: DefaultEntityContainer(state: player._buildStatePart(context)),
|
||||
)
|
||||
),
|
||||
onTap: () => _playMedia(player),
|
||||
))
|
||||
);
|
||||
body = ListView(
|
||||
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
|
||||
scrollDirection: Axis.vertical,
|
||||
children: children
|
||||
);
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text("Play media"),
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
_stateSubscription?.cancel();
|
||||
_refreshDataSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
107
lib/pages/purchase.page.dart
Normal file
@ -0,0 +1,107 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class PurchasePage extends StatefulWidget {
|
||||
PurchasePage({Key key, this.title}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
_PurchasePageState createState() => new _PurchasePageState();
|
||||
}
|
||||
|
||||
class _PurchasePageState extends State<PurchasePage> {
|
||||
|
||||
bool _loaded = false;
|
||||
String _error = "";
|
||||
List<ProductDetails> _products;
|
||||
List<PurchaseDetails> _purchases;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
}
|
||||
|
||||
_loadProducts() async {
|
||||
final bool available = await InAppPurchaseConnection.instance.isAvailable();
|
||||
if (!available) {
|
||||
setState(() {
|
||||
_error = "Error connecting to store";
|
||||
});
|
||||
} else {
|
||||
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||
if (!response.notFoundIDs.isEmpty) {
|
||||
Logger.d("Products not found: ${response.notFoundIDs}");
|
||||
}
|
||||
_products = response.productDetails;
|
||||
_loadPreviousPurchases();
|
||||
}
|
||||
}
|
||||
|
||||
_loadPreviousPurchases() async {
|
||||
final QueryPurchaseDetailsResponse response = await InAppPurchaseConnection.instance.queryPastPurchases();
|
||||
if (response.error != null) {
|
||||
setState(() {
|
||||
_error = "Error loading previous purchases";
|
||||
});
|
||||
} else {
|
||||
_purchases = response.pastPurchases;
|
||||
for (PurchaseDetails purchase in _purchases) {
|
||||
Logger.d("Previous purchase: ${purchase.status}");
|
||||
}
|
||||
if (_products.isEmpty) {
|
||||
setState(() {
|
||||
_error = "No data found in store";
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildProducts() {
|
||||
List<Widget> productWidgets = [];
|
||||
for (ProductDetails product in _products) {
|
||||
productWidgets.add(
|
||||
ProductPurchase(
|
||||
product: product,
|
||||
onBuy: (product) => _buyProduct(product),
|
||||
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
children: productWidgets
|
||||
);
|
||||
}
|
||||
|
||||
void _buyProduct(ProductDetails product) {
|
||||
Logger.d("Starting purchase of ${product.id}");
|
||||
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
|
||||
InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (!_loaded) {
|
||||
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
||||
} else {
|
||||
body = _buildProducts();
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text(widget.title),
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class ConnectionSettingsPage extends StatefulWidget {
|
||||
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
||||
@ -14,26 +14,46 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
String _newHassioDomain = "";
|
||||
String _hassioPort = "";
|
||||
String _newHassioPort = "";
|
||||
String _hassioPassword = "";
|
||||
String _newHassioPassword = "";
|
||||
String _socketProtocol = "wss";
|
||||
String _newSocketProtocol = "wss";
|
||||
String _longLivedToken = "";
|
||||
String _newLongLivedToken = "";
|
||||
bool _useLovelace = true;
|
||||
bool _newUseLovelace = true;
|
||||
|
||||
String oauthUrl;
|
||||
bool useOAuth = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
|
||||
}
|
||||
|
||||
_loadSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final storage = new FlutterSecureStorage();
|
||||
|
||||
try {
|
||||
useOAuth = prefs.getBool("oauth-used") ?? true;
|
||||
} catch (e) {
|
||||
useOAuth = true;
|
||||
}
|
||||
|
||||
if (!useOAuth) {
|
||||
try {
|
||||
_longLivedToken = _newLongLivedToken =
|
||||
await storage.read(key: "hacl_llt");
|
||||
} catch (e) {
|
||||
_longLivedToken = _newLongLivedToken = "";
|
||||
await storage.delete(key: "hacl_llt");
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
||||
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
||||
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
|
||||
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
||||
try {
|
||||
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
|
||||
@ -44,22 +64,37 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
}
|
||||
|
||||
bool _checkConfigChanged() {
|
||||
return ((_newHassioPassword != _hassioPassword) ||
|
||||
return (
|
||||
(_newHassioPort != _hassioPort) ||
|
||||
(_newHassioDomain != _hassioDomain) ||
|
||||
(_newSocketProtocol != _socketProtocol) ||
|
||||
(_newUseLovelace != _useLovelace));
|
||||
(_newUseLovelace != _useLovelace) ||
|
||||
(_newLongLivedToken != _longLivedToken));
|
||||
|
||||
}
|
||||
|
||||
_saveSettings() async {
|
||||
_newHassioDomain = _newHassioDomain.trim();
|
||||
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||
}
|
||||
_newHassioDomain = _newHassioDomain.split("/")[0];
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final storage = new FlutterSecureStorage();
|
||||
if (_newLongLivedToken.isNotEmpty) {
|
||||
_newLongLivedToken = _newLongLivedToken.trim();
|
||||
prefs.setBool("oauth-used", false);
|
||||
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
||||
} else if (!useOAuth) {
|
||||
await storage.delete(key: "hacl_llt");
|
||||
}
|
||||
prefs.setString("hassio-domain", _newHassioDomain);
|
||||
if (_newHassioPort == null || _newHassioPort.isEmpty) {
|
||||
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
|
||||
} else {
|
||||
_newHassioPort = _newHassioPort.trim();
|
||||
}
|
||||
prefs.setString("hassio-port", _newHassioPort);
|
||||
prefs.setString("hassio-password", _newHassioPassword);
|
||||
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||
prefs.setBool("use-lovelace", _newUseLovelace);
|
||||
@ -119,13 +154,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: "Home Assistant domain or ip address"
|
||||
),
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioDomain,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioDomain.length)
|
||||
)
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
|
||||
onChanged: (value) {
|
||||
_newHassioDomain = value;
|
||||
}
|
||||
@ -134,13 +163,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: "Home Assistant port (default is 8123)"
|
||||
),
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioPort,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioPort.length)
|
||||
)
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
|
||||
onChanged: (value) {
|
||||
_newHassioPort = value;
|
||||
}
|
||||
@ -149,21 +172,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
"Try ports 80 and 443 if default is not working and you don't know why.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
new TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Access token"
|
||||
),
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioPassword,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioPassword.length)
|
||||
)
|
||||
),
|
||||
onChanged: (value) {
|
||||
_newHassioPassword = value;
|
||||
}
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20.0),
|
||||
child: Text(
|
||||
@ -187,6 +195,27 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Authentication settings",
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontSize: 20.0
|
||||
),
|
||||
),
|
||||
Container(height: 10.0,),
|
||||
Text(
|
||||
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
new TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Long-lived token"
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
|
||||
onChanged: (value) {
|
||||
_newLongLivedToken = value;
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
32
lib/pages/widgets/page_loading_error.dart
Normal file
@ -0,0 +1,32 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class PageLoadingError extends StatelessWidget {
|
||||
|
||||
final String errorText;
|
||||
|
||||
const PageLoadingError({Key key, this.errorText: "Error"}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||
child: Icon(
|
||||
Icons.error,
|
||||
color: Colors.redAccent,
|
||||
size: 48.0
|
||||
)
|
||||
),
|
||||
Text(this.errorText, style: TextStyle(color: Colors.black45))
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
23
lib/pages/widgets/page_loading_indicator.dart
Normal file
@ -0,0 +1,23 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class PageLoadingIndicator extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||
child: CircularProgressIndicator()
|
||||
),
|
||||
Text("Loading...", style: TextStyle(color: Colors.black45))
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
74
lib/pages/widgets/product_purchase.widget.dart
Normal file
@ -0,0 +1,74 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class ProductPurchase extends StatelessWidget {
|
||||
|
||||
final ProductDetails product;
|
||||
final onBuy;
|
||||
final purchased;
|
||||
|
||||
const ProductPurchase({Key key, @required this.product, @required this.onBuy, this.purchased}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String period = "";
|
||||
Color priceColor;
|
||||
String buttonText = '';
|
||||
String buttonTextInactive = '';
|
||||
if (product.id.contains("year")) {
|
||||
period += "/ year";
|
||||
buttonText = "Subscribe";
|
||||
buttonTextInactive = "Already";
|
||||
priceColor = Colors.amber;
|
||||
} else {
|
||||
period += "";
|
||||
buttonText = "Pay";
|
||||
buttonTextInactive = "Paid";
|
||||
priceColor = Colors.deepOrangeAccent;
|
||||
}
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${product.title}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text(
|
||||
"${product.description}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 4,
|
||||
softWrap: true,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("${product.price} $period", style: TextStyle(color: priceColor)),
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: RaisedButton(
|
||||
child: Text(this.purchased ? buttonTextInactive : buttonText, style: TextStyle(color: Colors.white)),
|
||||
color: Colors.blue,
|
||||
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|