Compare commits
208 Commits
Author | SHA1 | Date | |
---|---|---|---|
753df3c724 | |||
dc62a08da3 | |||
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 | |||
db77cc43aa | |||
b2269cc96d | |||
8b28bb2e9e | |||
fb456878bc | |||
8b961ebd69 | |||
9bd3a41cf5 | |||
491ae55a2a | |||
e1d2981782 | |||
74572168ae | |||
92d0b5c055 | |||
3504d3276c | |||
736b38b64c | |||
cb118b599a | |||
a08a056cff | |||
0ef2ebfe31 | |||
4f4ac3b574 | |||
7064cb0e30 | |||
91a99e17e0 | |||
2e9b7d20b9 | |||
b8aa808de4 | |||
2cfa92a42b | |||
146efef72d | |||
8c9804e16f | |||
a4736bfb5a | |||
15c54df629 | |||
32ffef21e9 | |||
848d3cb510 | |||
8a4caeebba | |||
aa923f0fba | |||
4d8f50ddd5 | |||
fe06b21a6c | |||
efed7fb1b5 | |||
df2cbb7d13 | |||
03edaa9ca2 | |||
1a7457abf9 | |||
00889b13e0 | |||
0615073ec4 | |||
eb7d17d147 | |||
24f80feeee | |||
4b6dda5a9c | |||
4099fa0c83 | |||
76057e8797 | |||
538d3603dc | |||
bc0e72ca52 | |||
f25a47beb2 | |||
cc3c6b0087 | |||
6cf80c0bfd | |||
8ce9bdb7a5 | |||
31e50150b1 |
2
.gitignore
vendored
@ -11,3 +11,5 @@ build/
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
|
premium_features_manager.class.dart
|
||||||
|
pubspec.lock
|
10
README.md
@ -2,12 +2,8 @@
|
|||||||
## Native Android client for Home Assistant
|
## Native Android client for Home Assistant
|
||||||
### With Lovelace UI support
|
### With Lovelace UI support
|
||||||
|
|
||||||
Home Assistant Android client on Dart with Flutter.
|
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
||||||
|
|
||||||
Visit [www.vynn.co](https://www.vynn.co/ha-client) for more info.
|
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||||
|
|
||||||
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
|
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)
|
||||||
|
|
||||||
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) or in [Discord](https://discord.gg/NSaQEQ8)
|
|
||||||
|
@ -29,7 +29,7 @@ def keystoreProperties = new Properties()
|
|||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 28
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -43,7 +43,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.keyboardcrumbs.haclient"
|
applicationId "com.keyboardcrumbs.haclient"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 27
|
targetSdkVersion 28
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
@ -70,7 +70,10 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'com.google.firebase:firebase-core:16.0.8'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.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.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<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
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
@ -13,9 +18,15 @@
|
|||||||
additional functionality it is fine to subclass or reimplement
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<application
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name=".Application"
|
||||||
android:label="HA Client"
|
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
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
@ -26,14 +37,33 @@
|
|||||||
<!-- This keeps the window background of the activity showing
|
<!-- This keeps the window background of the activity showing
|
||||||
until Flutter renders its first frame. It can be removed if
|
until Flutter renders its first frame. It can be removed if
|
||||||
there is no splash screen (such as the default splash screen
|
there is no splash screen (such as the default splash screen
|
||||||
defined in @style/LaunchTheme). -->
|
defined in @style/LaunchTheme).
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
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>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
</manifest>
|
</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 android.os.Bundle;
|
||||||
import io.flutter.app.FlutterActivity;
|
import io.flutter.app.FlutterActivity;
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
import io.flutter.plugins.share.FlutterShareReceiverActivity;
|
||||||
|
|
||||||
public class MainActivity extends FlutterActivity {
|
public class MainActivity extends FlutterShareReceiverActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(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 {
|
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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1,5 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx2g
|
||||||
|
org.gradle.daemon=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
||||||
|
0
android/gradlew
vendored
Normal file → Executable file
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;
|
int columnsCount;
|
||||||
List stateFilter;
|
List stateFilter;
|
||||||
List states;
|
List states;
|
||||||
|
List conditions;
|
||||||
String content;
|
String content;
|
||||||
|
String unit;
|
||||||
|
int min;
|
||||||
|
int max;
|
||||||
|
Map severity;
|
||||||
|
|
||||||
HACard({
|
HACard({
|
||||||
this.name,
|
this.name,
|
||||||
@ -26,8 +31,17 @@ class HACard {
|
|||||||
this.showEmpty: true,
|
this.showEmpty: true,
|
||||||
this.content,
|
this.content,
|
||||||
this.states,
|
this.states,
|
||||||
|
this.conditions: const [],
|
||||||
|
this.unit,
|
||||||
|
this.min,
|
||||||
|
this.max,
|
||||||
|
this.severity,
|
||||||
@required this.type
|
@required this.type
|
||||||
});
|
}) {
|
||||||
|
if (this.columnsCount <= 0) {
|
||||||
|
this.columnsCount = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<EntityWrapper> getEntitiesToShow() {
|
List<EntityWrapper> getEntitiesToShow() {
|
||||||
return entities.where((entityWrapper) {
|
return entities.where((entityWrapper) {
|
363
lib/cards/card_widget.dart
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class CardWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final HACard card;
|
||||||
|
|
||||||
|
const CardWidget({
|
||||||
|
Key key,
|
||||||
|
this.card
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (card.linkedEntityWrapper!= null) {
|
||||||
|
if (card.linkedEntityWrapper.entity.isHidden) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: MissedEntityWidget(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {
|
||||||
|
return _buildEntitiesCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.GLANCE: {
|
||||||
|
return _buildGlanceCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.MEDIA_CONTROL: {
|
||||||
|
return _buildMediaControlsCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.ENTITY_BUTTON: {
|
||||||
|
return _buildEntityButtonCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.GAUGE: {
|
||||||
|
return _buildGaugeCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* case CardType.LIGHT: {
|
||||||
|
return _buildLightCard(context);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
case CardType.MARKDOWN: {
|
||||||
|
return _buildMarkdownCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.ALARM_PANEL: {
|
||||||
|
return _buildAlarmPanelCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.HORIZONTAL_STACK: {
|
||||||
|
if (card.childCards.isNotEmpty) {
|
||||||
|
List<Widget> children = [];
|
||||||
|
card.childCards.forEach((card) {
|
||||||
|
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
|
||||||
|
children.add(
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
child: card.build(context),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.VERTICAL_STACK: {
|
||||||
|
if (card.childCards.isNotEmpty) {
|
||||||
|
List<Widget> children = [];
|
||||||
|
card.childCards.forEach((card) {
|
||||||
|
children.add(
|
||||||
|
card.build(context)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
|
||||||
|
return _buildEntitiesCard(context);
|
||||||
|
} else {
|
||||||
|
return _buildUnsupportedCard(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntitiesCard(BuildContext context) {
|
||||||
|
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||||
|
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(name: card.name));
|
||||||
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
|
if (!entity.entity.isHidden) {
|
||||||
|
body.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
handleTap: true,
|
||||||
|
child: entity.entity.buildDefaultWidget(context)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMarkdownCard(BuildContext context) {
|
||||||
|
if (card.content == null) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(name: card.name));
|
||||||
|
body.add(MarkdownBody(data: card.content));
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(
|
||||||
|
name: card.name ?? "",
|
||||||
|
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
EntityIcon(
|
||||||
|
size: 50.0,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 26.0,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
"mdi:dots-vertical")),
|
||||||
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
));
|
||||||
|
body.add(
|
||||||
|
AlarmControlPanelControlsWidget(
|
||||||
|
extended: true,
|
||||||
|
states: card.states,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
handleTap: null,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: body
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGlanceCard(BuildContext context) {
|
||||||
|
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||||
|
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> rows = [];
|
||||||
|
rows.add(CardHeader(name: card.name));
|
||||||
|
|
||||||
|
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) {
|
||||||
|
buttons.add(
|
||||||
|
SizedBox(
|
||||||
|
width: buttonWidth,
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
child: GlanceCardEntityContainer(
|
||||||
|
showName: card.showName,
|
||||||
|
showState: card.showState,
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Wrap(
|
||||||
|
//spacing: 5.0,
|
||||||
|
//alignment: WrapAlignment.spaceEvenly,
|
||||||
|
runSpacing: Sizes.doubleRowPadding,
|
||||||
|
children: buttons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: rows
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMediaControlsCard(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
handleTap: null,
|
||||||
|
child: MediaPlayerWidget()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntityButtonCard(BuildContext context) {
|
||||||
|
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
||||||
|
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUnsupportedCard(BuildContext context) {
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(name: card.name ?? ""));
|
||||||
|
List<Widget> result = [];
|
||||||
|
if (card.linkedEntityWrapper != null) {
|
||||||
|
result.addAll(<Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
handleTap: true,
|
||||||
|
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
result.addAll(<Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Text("'${card.type}' card is not supported yet"),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
body.addAll(result);
|
||||||
|
return Card(
|
||||||
|
child: new Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: body
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class CardHeaderWidget extends StatelessWidget {
|
class CardHeader extends StatelessWidget {
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final Widget trailing;
|
final Widget trailing;
|
||||||
final Widget subtitle;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
51
lib/cards/widgets/entity_button_card_body.widget.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class EntityButtonCardBody extends StatelessWidget {
|
||||||
|
|
||||||
|
EntityButtonCardBody({
|
||||||
|
Key key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
}
|
||||||
|
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(
|
||||||
|
children: <Widget>[
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
return EntityIcon(
|
||||||
|
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||||
|
size: constraints.maxWidth / 2.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_buildName()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildName() {
|
||||||
|
return EntityName(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 3,
|
||||||
|
wordsWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
fontSize: Sizes.nameFontSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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 showName;
|
||||||
final bool showState;
|
final bool showState;
|
||||||
@ -9,7 +9,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
final double nameFontSize;
|
final double nameFontSize;
|
||||||
final bool wordsWrapInName;
|
final bool wordsWrapInName;
|
||||||
|
|
||||||
GlanceEntityContainer({
|
GlanceCardEntityContainer({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.showName,
|
@required this.showName,
|
||||||
@required this.showState,
|
@required this.showState,
|
||||||
@ -22,6 +22,12 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
}
|
||||||
|
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (!nameInTheBottom) {
|
if (!nameInTheBottom) {
|
||||||
if (showName) {
|
if (showName) {
|
||||||
@ -35,7 +41,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
result.add(
|
result.add(
|
||||||
EntityIcon(
|
EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
iconSize: iconSize,
|
size: iconSize,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (!nameInTheBottom) {
|
if (!nameInTheBottom) {
|
||||||
@ -48,15 +54,10 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: InkResponse(
|
child: InkResponse(
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
//mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
//crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: result,
|
children: result,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
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,105 +0,0 @@
|
|||||||
part of 'main.dart';
|
|
||||||
|
|
||||||
class ConfigurationPage extends StatefulWidget {
|
|
||||||
ConfigurationPage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ConfigurationPageState createState() => new _ConfigurationPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConfigurationItem {
|
|
||||||
ConfigurationItem({ this.isExpanded: false, this.header, this.body });
|
|
||||||
|
|
||||||
bool isExpanded;
|
|
||||||
final String header;
|
|
||||||
final Widget body;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConfigurationPageState extends State<ConfigurationPage> {
|
|
||||||
|
|
||||||
List<ConfigurationItem> _items;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_items = <ConfigurationItem>[
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'General',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text("Control your Home Assistant server from HA Client."),
|
|
||||||
Divider(),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatServiceButton(
|
|
||||||
text: "Restart",
|
|
||||||
serviceName: "restart",
|
|
||||||
serviceDomain: "homeassistant",
|
|
||||||
entityId: null,
|
|
||||||
),
|
|
||||||
FlatServiceButton(
|
|
||||||
text: "Stop",
|
|
||||||
serviceName: "stop",
|
|
||||||
serviceDomain: "homeassistant",
|
|
||||||
entityId: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
|
|
||||||
return new Scaffold(
|
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
|
||||||
Navigator.pop(context);
|
|
||||||
}),
|
|
||||||
title: new Text(widget.title),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
children: [
|
|
||||||
new ExpansionPanelList(
|
|
||||||
expansionCallback: (int index, bool isExpanded) {
|
|
||||||
setState(() {
|
|
||||||
_items[index].isExpanded = !_items[index].isExpanded;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
children: _items.map((ConfigurationItem item) {
|
|
||||||
return new ExpansionPanel(
|
|
||||||
headerBuilder: (BuildContext context, bool isExpanded) {
|
|
||||||
return CardHeaderWidget(
|
|
||||||
name: item.header,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isExpanded: item.isExpanded,
|
|
||||||
body: new Container(
|
|
||||||
child: item.body,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
part of '../main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class EntityState {
|
class EntityState {
|
||||||
static const on = 'on';
|
static const on = 'on';
|
||||||
@ -28,6 +28,7 @@ class EntityState {
|
|||||||
static const unavailable = 'unavailable';
|
static const unavailable = 'unavailable';
|
||||||
static const ok = 'ok';
|
static const ok = 'ok';
|
||||||
static const problem = 'problem';
|
static const problem = 'problem';
|
||||||
|
static const active = 'active';
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntityUIAction {
|
class EntityUIAction {
|
||||||
@ -76,23 +77,40 @@ class EntityUIAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CardType {
|
class CardType {
|
||||||
static const horizontalStack = "horizontal-stack";
|
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||||
static const verticalStack = "vertical-stack";
|
static const VERTICAL_STACK = "vertical-stack";
|
||||||
static const entities = "entities";
|
static const ENTITIES = "entities";
|
||||||
static const glance = "glance";
|
static const GLANCE = "glance";
|
||||||
static const mediaControl = "media-control";
|
static const MEDIA_CONTROL = "media-control";
|
||||||
static const weatherForecast = "weather-forecast";
|
static const WEATHER_FORECAST = "weather-forecast";
|
||||||
static const thermostat = "thermostat";
|
static const THERMOSTAT = "thermostat";
|
||||||
static const sensor = "sensor";
|
static const SENSOR = "sensor";
|
||||||
static const plantStatus = "plant-status";
|
static const PLANT_STATUS = "plant-status";
|
||||||
static const pictureEntity = "picture-entity";
|
static const PICTURE_ENTITY = "picture-entity";
|
||||||
static const pictureElements = "picture-elements";
|
static const PICTURE_ELEMENTS = "picture-elements";
|
||||||
static const picture = "picture";
|
static const PICTURE = "picture";
|
||||||
static const map = "map";
|
static const MAP = "map";
|
||||||
static const iframe = "iframe";
|
static const IFRAME = "iframe";
|
||||||
static const gauge = "gauge";
|
static const GAUGE = "gauge";
|
||||||
static const entityButton = "entity-button";
|
static const ENTITY_BUTTON = "entity-button";
|
||||||
static const conditional = "conditional";
|
static const CONDITIONAL = "conditional";
|
||||||
static const alarmPanel = "alarm-panel";
|
static const ALARM_PANEL = "alarm-panel";
|
||||||
static const markdown = "markdown";
|
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 {
|
class AlarmControlPanelEntity extends Entity {
|
||||||
AlarmControlPanelEntity(Map rawData) : super(rawData);
|
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class AlarmControlPanelControlsWidget extends StatefulWidget {
|
class AlarmControlPanelControlsWidget extends StatefulWidget {
|
||||||
|
|
@ -1,7 +1,8 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class AutomationEntity extends Entity {
|
class AutomationEntity extends Entity {
|
||||||
AutomationEntity(Map rawData) : super(rawData);
|
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
@ -1,7 +1,8 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class ButtonEntity extends Entity {
|
class ButtonEntity extends Entity {
|
||||||
ButtonEntity(Map rawData) : super(rawData);
|
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
@ -9,7 +10,7 @@ class ButtonEntity extends Entity {
|
|||||||
entityId: entityId,
|
entityId: entityId,
|
||||||
serviceDomain: domain,
|
serviceDomain: domain,
|
||||||
serviceName: 'turn_on',
|
serviceName: 'turn_on',
|
||||||
text: "EXECUTE",
|
text: domain == "scene" ? "ACTIVATE" : "EXECUTE",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
17
lib/entities/camera/camera_entity.class.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class CameraEntity extends Entity {
|
||||||
|
|
||||||
|
static const SUPPORT_ON_OFF = 1;
|
||||||
|
|
||||||
|
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
bool get supportOnOff => ((supportedFeatures &
|
||||||
|
CameraEntity.SUPPORT_ON_OFF) ==
|
||||||
|
CameraEntity.SUPPORT_ON_OFF);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return CameraStreamView();
|
||||||
|
}
|
||||||
|
}
|
114
lib/entities/climate/climate_entity.class.dart
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class ClimateEntity extends Entity {
|
||||||
|
|
||||||
|
@override
|
||||||
|
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||||
|
chartType: EntityHistoryWidgetType.numericAttributes,
|
||||||
|
numericState: false,
|
||||||
|
numericAttributesToShow: ["current_temperature"]
|
||||||
|
);
|
||||||
|
|
||||||
|
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
||||||
|
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 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 supportFanMode =>
|
||||||
|
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_FAN_MODE);
|
||||||
|
bool get supportSwingMode =>
|
||||||
|
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_SWING_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);
|
||||||
|
|
||||||
|
List<String> get hvacModes => attributes["hvac_modes"] != null
|
||||||
|
? (attributes["hvac_modes"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
List<String> get fanModes => attributes["fan_modes"] != null
|
||||||
|
? (attributes["fan_modes"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
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;
|
||||||
|
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
|
||||||
|
double get targetHumidity => _getDoubleAttributeValue('humidity');
|
||||||
|
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||||
|
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||||
|
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
|
||||||
|
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 auxHeat => attributes['aux_heat'] == "on";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(Map rawData, String webHost) {
|
||||||
|
super.update(rawData, webHost);
|
||||||
|
if (supportTargetTemperature) {
|
||||||
|
historyConfig.numericAttributesToShow.add("temperature");
|
||||||
|
}
|
||||||
|
if (supportTargetTemperatureRange) {
|
||||||
|
historyConfig.numericAttributesToShow.add("target_temp_high");
|
||||||
|
historyConfig.numericAttributesToShow.add("target_temp_low");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return ClimateStateWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return ClimateControlWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double _getDoubleAttributeValue(String attributeName) {
|
||||||
|
var temp1 = attributes["$attributeName"];
|
||||||
|
if (temp1 is int) {
|
||||||
|
return temp1.toDouble();
|
||||||
|
} else if (temp1 is double) {
|
||||||
|
return temp1;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class ClimateControlWidget extends StatefulWidget {
|
class ClimateControlWidget extends StatefulWidget {
|
||||||
|
|
||||||
@ -19,22 +19,22 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
double _tmpTargetLow = 0.0;
|
double _tmpTargetLow = 0.0;
|
||||||
double _tmpTargetHigh = 0.0;
|
double _tmpTargetHigh = 0.0;
|
||||||
double _tmpTargetHumidity = 0.0;
|
double _tmpTargetHumidity = 0.0;
|
||||||
String _tmpOperationMode;
|
String _tmpHVACMode;
|
||||||
String _tmpFanMode;
|
String _tmpFanMode;
|
||||||
String _tmpSwingMode;
|
String _tmpSwingMode;
|
||||||
bool _tmpAwayMode = false;
|
String _tmpPresetMode;
|
||||||
bool _tmpIsOff = false;
|
//bool _tmpIsOff = false;
|
||||||
bool _tmpAuxHeat = false;
|
bool _tmpAuxHeat = false;
|
||||||
|
|
||||||
void _resetVars(ClimateEntity entity) {
|
void _resetVars(ClimateEntity entity) {
|
||||||
_tmpTemperature = entity.temperature;
|
_tmpTemperature = entity.temperature;
|
||||||
_tmpTargetHigh = entity.targetHigh;
|
_tmpTargetHigh = entity.targetHigh;
|
||||||
_tmpTargetLow = entity.targetLow;
|
_tmpTargetLow = entity.targetLow;
|
||||||
_tmpOperationMode = entity.operationMode;
|
_tmpHVACMode = entity.state;
|
||||||
_tmpFanMode = entity.fanMode;
|
_tmpFanMode = entity.fanMode;
|
||||||
_tmpSwingMode = entity.swingMode;
|
_tmpSwingMode = entity.swingMode;
|
||||||
_tmpAwayMode = entity.awayMode;
|
_tmpPresetMode = entity.presetMode;
|
||||||
_tmpIsOff = entity.isOff;
|
//_tmpIsOff = entity.isOff;
|
||||||
_tmpAuxHeat = entity.auxHeat;
|
_tmpAuxHeat = entity.auxHeat;
|
||||||
_tmpTargetHumidity = entity.targetHumidity;
|
_tmpTargetHumidity = entity.targetHumidity;
|
||||||
|
|
||||||
@ -116,11 +116,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setOperationMode(ClimateEntity entity, value) {
|
void _setHVACMode(ClimateEntity entity, value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tmpOperationMode = value;
|
_tmpHVACMode = value;
|
||||||
_changedHere = true;
|
_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);
|
_resetStateTimer(entity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -143,23 +143,23 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setAwayMode(ClimateEntity entity, value) {
|
void _setPresetMode(ClimateEntity entity, value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tmpAwayMode = value;
|
_tmpPresetMode = value;
|
||||||
_changedHere = true;
|
_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);
|
_resetStateTimer(entity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setOnOf(ClimateEntity entity, value) {
|
/*void _setOnOf(ClimateEntity entity, value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tmpIsOff = !value;
|
_tmpIsOff = !value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
|
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
|
||||||
_resetStateTimer(entity);
|
_resetStateTimer(entity);
|
||||||
});
|
});
|
||||||
}
|
}*/
|
||||||
|
|
||||||
void _setAuxHeat(ClimateEntity entity, value) {
|
void _setAuxHeat(ClimateEntity entity, value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -196,33 +196,34 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildOnOffControl(entity),
|
//_buildOnOffControl(entity),
|
||||||
_buildTemperatureControls(entity),
|
_buildTemperatureControls(entity),
|
||||||
_buildTargetTemperatureControls(entity),
|
_buildTargetTemperatureControls(entity),
|
||||||
_buildHumidityControls(entity),
|
_buildHumidityControls(entity),
|
||||||
_buildOperationControl(entity),
|
_buildOperationControl(entity),
|
||||||
_buildFanControl(entity),
|
_buildFanControl(entity),
|
||||||
_buildSwingControl(entity),
|
_buildSwingControl(entity),
|
||||||
_buildAwayModeControl(entity),
|
_buildPresetModeControl(entity),
|
||||||
_buildAuxHeatControl(entity)
|
_buildAuxHeatControl(entity)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAwayModeControl(ClimateEntity entity) {
|
Widget _buildPresetModeControl(ClimateEntity entity) {
|
||||||
if (entity.supportAwayMode) {
|
if (entity.supportPresetMode) {
|
||||||
return ModeSwitchWidget(
|
return ModeSelectorWidget(
|
||||||
caption: "Away mode",
|
options: entity.presetModes,
|
||||||
onChange: (value) => _setAwayMode(entity, value),
|
onChange: (mode) => _setPresetMode(entity, mode),
|
||||||
value: _tmpAwayMode,
|
caption: "Preset",
|
||||||
|
value: _tmpPresetMode,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOnOffControl(ClimateEntity entity) {
|
/*Widget _buildOnOffControl(ClimateEntity entity) {
|
||||||
if (entity.supportOnOff) {
|
if (entity.supportOnOff) {
|
||||||
return ModeSwitchWidget(
|
return ModeSwitchWidget(
|
||||||
onChange: (value) => _setOnOf(entity, value),
|
onChange: (value) => _setOnOf(entity, value),
|
||||||
@ -232,7 +233,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
} else {
|
} else {
|
||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
||||||
if (entity.supportAuxHeat ) {
|
if (entity.supportAuxHeat ) {
|
||||||
@ -247,12 +248,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOperationControl(ClimateEntity entity) {
|
Widget _buildOperationControl(ClimateEntity entity) {
|
||||||
if (entity.supportOperationMode) {
|
if (entity.hvacModes != null) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setOperationMode(entity, mode),
|
onChange: (mode) => _setHVACMode(entity, mode),
|
||||||
options: entity.operationList,
|
options: entity.hvacModes,
|
||||||
caption: "Operation",
|
caption: "Operation",
|
||||||
value: _tmpOperationMode,
|
value: _tmpHVACMode,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(height: 0.0, width: 0.0);
|
return Container(height: 0.0, width: 0.0);
|
||||||
@ -262,7 +263,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
Widget _buildFanControl(ClimateEntity entity) {
|
Widget _buildFanControl(ClimateEntity entity) {
|
||||||
if (entity.supportFanMode) {
|
if (entity.supportFanMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.fanList,
|
options: entity.fanModes,
|
||||||
onChange: (mode) => _setFanMode(entity, mode),
|
onChange: (mode) => _setFanMode(entity, mode),
|
||||||
caption: "Fan mode",
|
caption: "Fan mode",
|
||||||
value: _tmpFanMode,
|
value: _tmpFanMode,
|
||||||
@ -276,7 +277,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
if (entity.supportSwingMode) {
|
if (entity.supportSwingMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setSwingMode(entity, mode),
|
onChange: (mode) => _setSwingMode(entity, mode),
|
||||||
options: entity.swingList,
|
options: entity.swingModes,
|
||||||
value: _tmpSwingMode,
|
value: _tmpSwingMode,
|
||||||
caption: "Swing mode"
|
caption: "Swing mode"
|
||||||
);
|
);
|
||||||
@ -308,7 +309,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
|
|
||||||
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
||||||
List<Widget> controls = [];
|
List<Widget> controls = [];
|
||||||
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
|
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
||||||
controls.addAll(<Widget>[
|
controls.addAll(<Widget>[
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetLow,
|
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(
|
controls.add(
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetHigh,
|
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.createIconDataFromIconName(
|
|
||||||
'mdi:chevron-up')),
|
|
||||||
iconSize: 30.0,
|
|
||||||
onPressed: () => onInc(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
|
||||||
'mdi:chevron-down')),
|
|
||||||
iconSize: 30.0,
|
|
||||||
onPressed: () => onDec(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class ClimateStateWidget extends StatelessWidget {
|
class ClimateStateWidget extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
@ -8,13 +8,19 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
String targetTemp = "-";
|
String targetTemp = "-";
|
||||||
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||||
targetTemp = "${entity.temperature}";
|
targetTemp = "${entity.temperature}";
|
||||||
} else if ((entity.supportTargetTemperatureLow) &&
|
} else if ((entity.supportTargetTemperatureRange) &&
|
||||||
(entity.targetLow != null)) {
|
(entity.targetLow != null) &&
|
||||||
targetTemp = "${entity.targetLow}";
|
|
||||||
if ((entity.supportTargetTemperatureHigh) &&
|
|
||||||
(entity.targetHigh != 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(
|
return Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
@ -25,7 +31,7 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("${entity.state}",
|
Text("$displayState",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: new TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -38,8 +44,8 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
entity.attributes["current_temperature"] != null ?
|
entity.currentTemperature != null ?
|
||||||
Text("Currently: ${entity.attributes["current_temperature"]}",
|
Text("Currently: ${entity.currentTemperature}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: new TextStyle(
|
||||||
fontSize: Sizes.stateFontSize,
|
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 {
|
class CoverEntity extends Entity {
|
||||||
|
|
||||||
@ -11,29 +11,31 @@ class CoverEntity extends Entity {
|
|||||||
static const SUPPORT_STOP_TILT = 64;
|
static const SUPPORT_STOP_TILT = 64;
|
||||||
static const SUPPORT_SET_TILT_POSITION = 128;
|
static const SUPPORT_SET_TILT_POSITION = 128;
|
||||||
|
|
||||||
bool get supportOpen => ((attributes["supported_features"] &
|
CoverEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
bool get supportOpen => ((supportedFeatures &
|
||||||
CoverEntity.SUPPORT_OPEN) ==
|
CoverEntity.SUPPORT_OPEN) ==
|
||||||
CoverEntity.SUPPORT_OPEN);
|
CoverEntity.SUPPORT_OPEN);
|
||||||
bool get supportClose => ((attributes["supported_features"] &
|
bool get supportClose => ((supportedFeatures &
|
||||||
CoverEntity.SUPPORT_CLOSE) ==
|
CoverEntity.SUPPORT_CLOSE) ==
|
||||||
CoverEntity.SUPPORT_CLOSE);
|
CoverEntity.SUPPORT_CLOSE);
|
||||||
bool get supportSetPosition => ((attributes["supported_features"] &
|
bool get supportSetPosition => ((supportedFeatures &
|
||||||
CoverEntity.SUPPORT_SET_POSITION) ==
|
CoverEntity.SUPPORT_SET_POSITION) ==
|
||||||
CoverEntity.SUPPORT_SET_POSITION);
|
CoverEntity.SUPPORT_SET_POSITION);
|
||||||
bool get supportStop => ((attributes["supported_features"] &
|
bool get supportStop => ((supportedFeatures &
|
||||||
CoverEntity.SUPPORT_STOP) ==
|
CoverEntity.SUPPORT_STOP) ==
|
||||||
CoverEntity.SUPPORT_STOP);
|
CoverEntity.SUPPORT_STOP);
|
||||||
|
|
||||||
bool get supportOpenTilt => ((attributes["supported_features"] &
|
bool get supportOpenTilt => ((supportedFeatures &
|
||||||
CoverEntity.SUPPORT_OPEN_TILT) ==
|
CoverEntity.SUPPORT_OPEN_TILT) ==
|
||||||
CoverEntity.SUPPORT_OPEN_TILT);
|
CoverEntity.SUPPORT_OPEN_TILT);
|
||||||
bool get supportCloseTilt => ((attributes["supported_features"] &
|
bool get supportCloseTilt => ((supportedFeatures &
|
||||||
CoverEntity.SUPPORT_CLOSE_TILT) ==
|
CoverEntity.SUPPORT_CLOSE_TILT) ==
|
||||||
CoverEntity.SUPPORT_CLOSE_TILT);
|
CoverEntity.SUPPORT_CLOSE_TILT);
|
||||||
bool get supportStopTilt => ((attributes["supported_features"] &
|
bool get supportStopTilt => ((supportedFeatures &
|
||||||
CoverEntity.SUPPORT_STOP_TILT) ==
|
CoverEntity.SUPPORT_STOP_TILT) ==
|
||||||
CoverEntity.SUPPORT_STOP_TILT);
|
CoverEntity.SUPPORT_STOP_TILT);
|
||||||
bool get supportSetTiltPosition => ((attributes["supported_features"] &
|
bool get supportSetTiltPosition => ((supportedFeatures &
|
||||||
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
|
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
|
||||||
CoverEntity.SUPPORT_SET_TILT_POSITION);
|
CoverEntity.SUPPORT_SET_TILT_POSITION);
|
||||||
|
|
||||||
@ -45,8 +47,6 @@ class CoverEntity extends Entity {
|
|||||||
bool get canTiltBeOpened => currentTiltPosition < 100;
|
bool get canTiltBeOpened => currentTiltPosition < 100;
|
||||||
bool get canTiltBeClosed => currentTiltPosition > 0;
|
bool get canTiltBeClosed => currentTiltPosition > 0;
|
||||||
|
|
||||||
CoverEntity(Map rawData) : super(rawData);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
return CoverStateWidget();
|
return CoverStateWidget();
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class CoverControlWidget extends StatefulWidget {
|
class CoverControlWidget extends StatefulWidget {
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
|
|||||||
if (entity.supportOpenTilt) {
|
if (entity.supportOpenTilt) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName(
|
MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:arrow-top-right"),
|
"mdi:arrow-top-right"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
@ -170,7 +170,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
|
|||||||
if (entity.supportStopTilt) {
|
if (entity.supportStopTilt) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
onPressed: () => _stop(entity)));
|
onPressed: () => _stop(entity)));
|
||||||
@ -182,7 +182,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
|
|||||||
if (entity.supportCloseTilt) {
|
if (entity.supportCloseTilt) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName(
|
MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:arrow-bottom-left"),
|
"mdi:arrow-bottom-left"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class CoverStateWidget extends StatelessWidget {
|
class CoverStateWidget extends StatelessWidget {
|
||||||
void _open(CoverEntity entity) {
|
void _open(CoverEntity entity) {
|
||||||
@ -24,7 +24,7 @@ class CoverStateWidget extends StatelessWidget {
|
|||||||
if (entity.supportOpen) {
|
if (entity.supportOpen) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-up"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
onPressed: entity.canBeOpened ? () => _open(entity) : null));
|
onPressed: entity.canBeOpened ? () => _open(entity) : null));
|
||||||
@ -36,7 +36,7 @@ class CoverStateWidget extends StatelessWidget {
|
|||||||
if (entity.supportStop) {
|
if (entity.supportStop) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
onPressed: () => _stop(entity)));
|
onPressed: () => _stop(entity)));
|
||||||
@ -48,7 +48,7 @@ class CoverStateWidget extends StatelessWidget {
|
|||||||
if (entity.supportClose) {
|
if (entity.supportClose) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-down"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
onPressed: entity.canBeClosed ? () => _close(entity) : null));
|
onPressed: entity.canBeClosed ? () => _close(entity) : null));
|
@ -1,6 +1,8 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class DateTimeEntity extends Entity {
|
class DateTimeEntity extends Entity {
|
||||||
|
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get hasDate => attributes["has_date"] ?? false;
|
bool get hasDate => attributes["has_date"] ?? false;
|
||||||
bool get hasTime => attributes["has_time"] ?? false;
|
bool get hasTime => attributes["has_time"] ?? false;
|
||||||
int get year => attributes["year"] ?? 1970;
|
int get year => attributes["year"] ?? 1970;
|
||||||
@ -12,8 +14,6 @@ class DateTimeEntity extends Entity {
|
|||||||
String get formattedState => _getFormattedState();
|
String get formattedState => _getFormattedState();
|
||||||
DateTime get dateTimeState => _getDateTimeState();
|
DateTime get dateTimeState => _getDateTimeState();
|
||||||
|
|
||||||
DateTimeEntity(Map rawData) : super(rawData);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
return DateTimeStateWidget();
|
return DateTimeStateWidget();
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class DateTimeStateWidget extends StatelessWidget {
|
class DateTimeStateWidget extends StatelessWidget {
|
||||||
@override
|
@override
|
@ -1,5 +1,14 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class StatelessEntityType {
|
||||||
|
static const NONE = 0;
|
||||||
|
static const MISSED = 1;
|
||||||
|
static const DIVIDER = 2;
|
||||||
|
static const SECTION = 3;
|
||||||
|
static const CALL_SERVICE = 4;
|
||||||
|
static const WEBLINK = 5;
|
||||||
|
}
|
||||||
|
|
||||||
class Entity {
|
class Entity {
|
||||||
|
|
||||||
static List badgeDomains = [
|
static List badgeDomains = [
|
||||||
@ -18,7 +27,7 @@ class Entity {
|
|||||||
"cold.on": "Cold",
|
"cold.on": "Cold",
|
||||||
"cold.off": "Normal",
|
"cold.off": "Normal",
|
||||||
"connectivity.on": "Connected",
|
"connectivity.on": "Connected",
|
||||||
"connectivity.off": "Diconnected",
|
"connectivity.off": "Disconnected",
|
||||||
"door.on": "Open",
|
"door.on": "Open",
|
||||||
"door.off": "Closed",
|
"door.off": "Closed",
|
||||||
"garage_door.on": "Open",
|
"garage_door.on": "Open",
|
||||||
@ -64,12 +73,13 @@ class Entity {
|
|||||||
Map attributes;
|
Map attributes;
|
||||||
String domain;
|
String domain;
|
||||||
String entityId;
|
String entityId;
|
||||||
|
String entityPicture;
|
||||||
String state;
|
String state;
|
||||||
String displayState;
|
String displayState;
|
||||||
DateTime _lastUpdated;
|
DateTime _lastUpdated;
|
||||||
|
int statelessType = 0;
|
||||||
|
|
||||||
List<Entity> childEntities = [];
|
List<Entity> childEntities = [];
|
||||||
List<String> attributesToShow = ["all"];
|
|
||||||
String deviceClass;
|
String deviceClass;
|
||||||
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||||
chartType: EntityHistoryWidgetType.simple
|
chartType: EntityHistoryWidgetType.simple
|
||||||
@ -85,25 +95,68 @@ class Entity {
|
|||||||
bool get isBadge => Entity.badgeDomains.contains(domain);
|
bool get isBadge => Entity.badgeDomains.contains(domain);
|
||||||
String get icon => attributes["icon"] ?? "";
|
String get icon => attributes["icon"] ?? "";
|
||||||
bool get isOn => state == EntityState.on;
|
bool get isOn => state == EntityState.on;
|
||||||
String get entityPicture => attributes["entity_picture"];
|
|
||||||
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
||||||
List get childEntityIds => attributes["entity_id"] ?? [];
|
List get childEntityIds => attributes["entity_id"] ?? [];
|
||||||
String get lastUpdated => _getLastUpdatedFormatted();
|
String get lastUpdated => _getLastUpdatedFormatted();
|
||||||
bool get isHidden => attributes["hidden"] ?? false;
|
bool get isHidden => attributes["hidden"] ?? false;
|
||||||
double get doubleState => double.tryParse(state) ?? 0.0;
|
double get doubleState => double.tryParse(state) ?? 0.0;
|
||||||
|
int get supportedFeatures => attributes["supported_features"] ?? 0;
|
||||||
|
|
||||||
Entity(Map rawData) {
|
String _getEntityPictureUrl(String webHost) {
|
||||||
update(rawData);
|
String result = attributes["entity_picture"];
|
||||||
|
if (result == null) return result;
|
||||||
|
if (!result.startsWith("http")) {
|
||||||
|
if (result.startsWith("/")) {
|
||||||
|
result = "$webHost$result";
|
||||||
|
} else {
|
||||||
|
result = "$webHost/$result";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void update(Map rawData) {
|
Entity(Map rawData, String webHost) {
|
||||||
|
update(rawData, webHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.missed(String entityId) {
|
||||||
|
statelessType = StatelessEntityType.MISSED;
|
||||||
|
attributes = {"hidden": false};
|
||||||
|
this.entityId = entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.divider() {
|
||||||
|
statelessType = StatelessEntityType.DIVIDER;
|
||||||
|
attributes = {"hidden": false};
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.section(String label) {
|
||||||
|
statelessType = StatelessEntityType.SECTION;
|
||||||
|
attributes = {"hidden": false, "friendly_name": "$label"};
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.callService({String icon, String name, String service, String actionName}) {
|
||||||
|
statelessType = StatelessEntityType.CALL_SERVICE;
|
||||||
|
entityId = service;
|
||||||
|
displayState = actionName?.toUpperCase() ?? "RUN";
|
||||||
|
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.weblink({String url, String name, String icon}) {
|
||||||
|
statelessType = StatelessEntityType.WEBLINK;
|
||||||
|
entityId = "custom.custom"; //TODO wtf??
|
||||||
|
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(Map rawData, String webHost) {
|
||||||
attributes = rawData["attributes"] ?? {};
|
attributes = rawData["attributes"] ?? {};
|
||||||
domain = rawData["entity_id"].split(".")[0];
|
domain = rawData["entity_id"].split(".")[0];
|
||||||
entityId = rawData["entity_id"];
|
entityId = rawData["entity_id"];
|
||||||
deviceClass = attributes["device_class"];
|
deviceClass = attributes["device_class"];
|
||||||
state = rawData["state"];
|
state = rawData["state"];
|
||||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
|
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
||||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||||
|
entityPicture = _getEntityPictureUrl(webHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
double _getDoubleAttributeValue(String attributeName) {
|
double _getDoubleAttributeValue(String attributeName) {
|
||||||
@ -163,7 +216,7 @@ class Entity {
|
|||||||
entityWrapper: EntityWrapper(entity: this),
|
entityWrapper: EntityWrapper(entity: this),
|
||||||
child: EntityPageContainer(children: <Widget>[
|
child: EntityPageContainer(children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
||||||
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
||||||
),
|
),
|
||||||
LastUpdatedWidget(),
|
LastUpdatedWidget(),
|
@ -4,6 +4,8 @@ class EntityWrapper {
|
|||||||
|
|
||||||
String displayName;
|
String displayName;
|
||||||
String icon;
|
String icon;
|
||||||
|
String unitOfMeasurement;
|
||||||
|
String entityPicture;
|
||||||
EntityUIAction uiAction;
|
EntityUIAction uiAction;
|
||||||
Entity entity;
|
Entity entity;
|
||||||
|
|
||||||
@ -14,10 +16,16 @@ class EntityWrapper {
|
|||||||
String displayName,
|
String displayName,
|
||||||
this.uiAction
|
this.uiAction
|
||||||
}) {
|
}) {
|
||||||
|
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||||
this.icon = icon ?? entity.icon;
|
this.icon = icon ?? entity.icon;
|
||||||
|
if (icon == null) {
|
||||||
|
entityPicture = entity.entityPicture;
|
||||||
|
}
|
||||||
this.displayName = displayName ?? entity.displayName;
|
this.displayName = displayName ?? entity.displayName;
|
||||||
if (this.uiAction == null) {
|
if (uiAction == null) {
|
||||||
this.uiAction = EntityUIAction();
|
uiAction = EntityUIAction();
|
||||||
|
}
|
||||||
|
unitOfMeasurement = entity.unitOfMeasurement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +57,16 @@ class EntityWrapper {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case EntityUIAction.navigate: {
|
||||||
|
if (uiAction.tapService.startsWith("/")) {
|
||||||
|
//TODO handle local urls
|
||||||
|
Logger.w("Local urls is not supported yet");
|
||||||
|
} else {
|
||||||
|
Launcher.launchURL(uiAction.tapService);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -79,6 +97,16 @@ class EntityWrapper {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case EntityUIAction.navigate: {
|
||||||
|
if (uiAction.holdService.startsWith("/")) {
|
||||||
|
//TODO handle local urls
|
||||||
|
Logger.w("Local urls is not supported yet");
|
||||||
|
} else {
|
||||||
|
Launcher.launchURL(uiAction.holdService);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class FanEntity extends Entity {
|
class FanEntity extends Entity {
|
||||||
|
|
||||||
@ -6,15 +6,15 @@ class FanEntity extends Entity {
|
|||||||
static const SUPPORT_OSCILLATE = 2;
|
static const SUPPORT_OSCILLATE = 2;
|
||||||
static const SUPPORT_DIRECTION = 4;
|
static const SUPPORT_DIRECTION = 4;
|
||||||
|
|
||||||
FanEntity(Map rawData) : super(rawData);
|
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get supportSetSpeed => ((attributes["supported_features"] &
|
bool get supportSetSpeed => ((supportedFeatures &
|
||||||
FanEntity.SUPPORT_SET_SPEED) ==
|
FanEntity.SUPPORT_SET_SPEED) ==
|
||||||
FanEntity.SUPPORT_SET_SPEED);
|
FanEntity.SUPPORT_SET_SPEED);
|
||||||
bool get supportOscillate => ((attributes["supported_features"] &
|
bool get supportOscillate => ((supportedFeatures &
|
||||||
FanEntity.SUPPORT_OSCILLATE) ==
|
FanEntity.SUPPORT_OSCILLATE) ==
|
||||||
FanEntity.SUPPORT_OSCILLATE);
|
FanEntity.SUPPORT_OSCILLATE);
|
||||||
bool get supportDirection => ((attributes["supported_features"] &
|
bool get supportDirection => ((supportedFeatures &
|
||||||
FanEntity.SUPPORT_DIRECTION) ==
|
FanEntity.SUPPORT_DIRECTION) ==
|
||||||
FanEntity.SUPPORT_DIRECTION);
|
FanEntity.SUPPORT_DIRECTION);
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class FanControlsWidget extends StatefulWidget {
|
class FanControlsWidget extends StatefulWidget {
|
||||||
|
|
@ -1,12 +1,13 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class GroupEntity extends Entity {
|
class GroupEntity extends Entity {
|
||||||
GroupEntity(Map rawData) : super(rawData);
|
|
||||||
|
|
||||||
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
|
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
|
||||||
String mutualDomain;
|
String mutualDomain;
|
||||||
bool switchable = false;
|
bool switchable = false;
|
||||||
|
|
||||||
|
GroupEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
if (switchable) {
|
if (switchable) {
|
||||||
@ -19,8 +20,8 @@ class GroupEntity extends Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void update(Map rawData) {
|
void update(Map rawData, String webHost) {
|
||||||
super.update(rawData);
|
super.update(rawData, webHost);
|
||||||
if (_isOneDomain()) {
|
if (_isOneDomain()) {
|
||||||
mutualDomain = attributes['entity_id'][0].split(".")[0];
|
mutualDomain = attributes['entity_id'][0].split(".")[0];
|
||||||
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
|
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
|
@ -1,4 +1,4 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class LightEntity extends Entity {
|
class LightEntity extends Entity {
|
||||||
|
|
||||||
@ -10,46 +10,50 @@ class LightEntity extends Entity {
|
|||||||
static const SUPPORT_TRANSITION = 32;
|
static const SUPPORT_TRANSITION = 32;
|
||||||
static const SUPPORT_WHITE_VALUE = 128;
|
static const SUPPORT_WHITE_VALUE = 128;
|
||||||
|
|
||||||
bool get supportBrightness => ((attributes["supported_features"] &
|
bool get supportBrightness => ((supportedFeatures &
|
||||||
LightEntity.SUPPORT_BRIGHTNESS) ==
|
LightEntity.SUPPORT_BRIGHTNESS) ==
|
||||||
LightEntity.SUPPORT_BRIGHTNESS);
|
LightEntity.SUPPORT_BRIGHTNESS);
|
||||||
bool get supportColorTemp => ((attributes["supported_features"] &
|
bool get supportColorTemp => ((supportedFeatures &
|
||||||
LightEntity.SUPPORT_COLOR_TEMP) ==
|
LightEntity.SUPPORT_COLOR_TEMP) ==
|
||||||
LightEntity.SUPPORT_COLOR_TEMP);
|
LightEntity.SUPPORT_COLOR_TEMP);
|
||||||
bool get supportEffect => ((attributes["supported_features"] &
|
bool get supportEffect => ((supportedFeatures &
|
||||||
LightEntity.SUPPORT_EFFECT) ==
|
LightEntity.SUPPORT_EFFECT) ==
|
||||||
LightEntity.SUPPORT_EFFECT);
|
LightEntity.SUPPORT_EFFECT);
|
||||||
bool get supportFlash => ((attributes["supported_features"] &
|
bool get supportFlash => ((supportedFeatures &
|
||||||
LightEntity.SUPPORT_FLASH) ==
|
LightEntity.SUPPORT_FLASH) ==
|
||||||
LightEntity.SUPPORT_FLASH);
|
LightEntity.SUPPORT_FLASH);
|
||||||
bool get supportColor => ((attributes["supported_features"] &
|
bool get supportColor => ((supportedFeatures &
|
||||||
LightEntity.SUPPORT_COLOR) ==
|
LightEntity.SUPPORT_COLOR) ==
|
||||||
LightEntity.SUPPORT_COLOR);
|
LightEntity.SUPPORT_COLOR);
|
||||||
bool get supportTransition => ((attributes["supported_features"] &
|
bool get supportTransition => ((supportedFeatures &
|
||||||
LightEntity.SUPPORT_TRANSITION) ==
|
LightEntity.SUPPORT_TRANSITION) ==
|
||||||
LightEntity.SUPPORT_TRANSITION);
|
LightEntity.SUPPORT_TRANSITION);
|
||||||
bool get supportWhiteValue => ((attributes["supported_features"] &
|
bool get supportWhiteValue => ((supportedFeatures &
|
||||||
LightEntity.SUPPORT_WHITE_VALUE) ==
|
LightEntity.SUPPORT_WHITE_VALUE) ==
|
||||||
LightEntity.SUPPORT_WHITE_VALUE);
|
LightEntity.SUPPORT_WHITE_VALUE);
|
||||||
|
|
||||||
int get brightness => _getIntAttributeValue("brightness");
|
int get brightness => _getIntAttributeValue("brightness");
|
||||||
|
int get whiteValue => _getIntAttributeValue("white_value");
|
||||||
String get effect => attributes["effect"];
|
String get effect => attributes["effect"];
|
||||||
int get colorTemp => _getIntAttributeValue("color_temp");
|
int get colorTemp => _getIntAttributeValue("color_temp");
|
||||||
double get maxMireds => _getDoubleAttributeValue("max_mireds");
|
double get maxMireds => _getDoubleAttributeValue("max_mireds");
|
||||||
double get minMireds => _getDoubleAttributeValue("min_mireds");
|
double get minMireds => _getDoubleAttributeValue("min_mireds");
|
||||||
HSVColor get color => _getColor();
|
HSVColor get color => _getColor();
|
||||||
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
|
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
|
||||||
List<String> get effectList => getStringListAttributeValue("effect_list");
|
List<String> get effectList => getStringListAttributeValue("effect_list");
|
||||||
|
|
||||||
LightEntity(Map rawData) : super(rawData);
|
LightEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
HSVColor _getColor() {
|
HSVColor _getColor() {
|
||||||
List hs = attributes["hs_color"];
|
List hs = attributes["hs_color"];
|
||||||
|
List rgb = attributes["rgb_color"];
|
||||||
try {
|
try {
|
||||||
if ((hs != null) && (hs.length > 0)) {
|
if (hs != null && hs.isNotEmpty) {
|
||||||
double sat = hs[1]/100;
|
double sat = hs[1]/100;
|
||||||
String ssat = sat.toStringAsFixed(2);
|
String ssat = sat.toStringAsFixed(2);
|
||||||
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
|
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
|
||||||
|
} else if (rgb != null && rgb.isNotEmpty) {
|
||||||
|
return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2]));
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class LightControlsWidget extends StatefulWidget {
|
class LightControlsWidget extends StatefulWidget {
|
||||||
|
|
||||||
@ -10,13 +10,15 @@ class LightControlsWidget extends StatefulWidget {
|
|||||||
class _LightControlsWidgetState extends State<LightControlsWidget> {
|
class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||||
|
|
||||||
int _tmpBrightness;
|
int _tmpBrightness;
|
||||||
|
int _tmpWhiteValue;
|
||||||
int _tmpColorTemp = 0;
|
int _tmpColorTemp = 0;
|
||||||
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
|
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
|
||||||
bool _changedHere = false;
|
bool _changedHere = false;
|
||||||
String _tmpEffect;
|
String _tmpEffect;
|
||||||
|
|
||||||
void _resetState(LightEntity entity) {
|
void _resetState(LightEntity entity) {
|
||||||
_tmpBrightness = entity.brightness ?? 0;
|
_tmpBrightness = entity.brightness ?? 1;
|
||||||
|
_tmpWhiteValue = entity.whiteValue ?? 0;
|
||||||
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
|
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
|
||||||
_tmpColor = entity.color ?? _tmpColor;
|
_tmpColor = entity.color ?? _tmpColor;
|
||||||
_tmpEffect = entity.effect;
|
_tmpEffect = entity.effect;
|
||||||
@ -26,15 +28,20 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpBrightness = value.round();
|
_tmpBrightness = value.round();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
if (_tmpBrightness > 0) {
|
|
||||||
eventBus.fire(new ServiceCallEvent(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
entity.domain, "turn_on", entity.entityId,
|
entity.domain, "turn_on", entity.entityId,
|
||||||
{"brightness": _tmpBrightness}));
|
{"brightness": _tmpBrightness}));
|
||||||
} else {
|
});
|
||||||
eventBus.fire(new ServiceCallEvent(
|
|
||||||
entity.domain, "turn_off", entity.entityId,
|
|
||||||
null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setWhiteValue(LightEntity entity, double value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpWhiteValue = value.round();
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "turn_on", entity.entityId,
|
||||||
|
{"white_value": _tmpWhiteValue}));
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +91,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildBrightnessControl(entity),
|
_buildBrightnessControl(entity),
|
||||||
|
_buildWhiteValueControl(entity),
|
||||||
_buildColorTempControl(entity),
|
_buildColorTempControl(entity),
|
||||||
_buildColorControl(entity),
|
_buildColorControl(entity),
|
||||||
_buildEffectControl(entity)
|
_buildEffectControl(entity)
|
||||||
@ -100,10 +108,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpBrightness = value.round();
|
_tmpBrightness = value.round();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
min: 0.0,
|
min: 1.0,
|
||||||
max: 255.0,
|
max: 255.0,
|
||||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||||
value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(),
|
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(),
|
||||||
leading: Icon(Icons.brightness_5),
|
leading: Icon(Icons.brightness_5),
|
||||||
title: "Brightness",
|
title: "Brightness",
|
||||||
);
|
);
|
||||||
@ -112,6 +120,27 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildWhiteValueControl(LightEntity entity) {
|
||||||
|
if ((entity.supportWhiteValue) && (_tmpWhiteValue != null)) {
|
||||||
|
return UniversalSlider(
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_changedHere = true;
|
||||||
|
_tmpWhiteValue = value.round();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
min: 0.0,
|
||||||
|
max: 255.0,
|
||||||
|
onChangeEnd: (value) => _setWhiteValue(entity, value),
|
||||||
|
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
|
||||||
|
title: "White",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildColorTempControl(LightEntity entity) {
|
Widget _buildColorTempControl(LightEntity entity) {
|
||||||
if (entity.supportColorTemp) {
|
if (entity.supportColorTemp) {
|
||||||
return UniversalSlider(
|
return UniversalSlider(
|
||||||
@ -136,30 +165,42 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
Widget _buildColorControl(LightEntity entity) {
|
Widget _buildColorControl(LightEntity entity) {
|
||||||
if (entity.supportColor) {
|
if (entity.supportColor) {
|
||||||
return LightColorPicker(
|
HSVColor savedColor = HomeAssistant().savedColor;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
LightColorPicker(
|
||||||
color: _tmpColor,
|
color: _tmpColor,
|
||||||
onColorSelected: (color) => _setColor(entity, color),
|
onColorSelected: (color) => _setColor(entity, color),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
color: _tmpColor.toColor(),
|
||||||
|
child: Text('Copy color'),
|
||||||
|
onPressed: _tmpColor == null ? null : () {
|
||||||
|
setState(() {
|
||||||
|
HomeAssistant().savedColor = _tmpColor;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
color: savedColor?.toColor() ?? Colors.transparent,
|
||||||
|
child: Text('Paste color'),
|
||||||
|
onPressed: savedColor == null ? null : () {
|
||||||
|
_setColor(entity, savedColor);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0.0, height: 0.0);
|
return Container(width: 0.0, height: 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showColorPicker(LightEntity entity) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
titlePadding: EdgeInsets.all(0.0),
|
|
||||||
contentPadding: EdgeInsets.all(0.0),
|
|
||||||
content: LightColorPicker(
|
|
||||||
color: _tmpColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEffectControl(LightEntity entity) {
|
Widget _buildEffectControl(LightEntity entity) {
|
||||||
if ((entity.supportEffect) && (entity.effectList != null)) {
|
if ((entity.supportEffect) && (entity.effectList != null)) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
21
lib/entities/lock/lock_entity.class.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class LockEntity extends Entity {
|
||||||
|
LockEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
bool get isLocked => state == "locked";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return LockStateWidget(
|
||||||
|
assumedState: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePartForPage(BuildContext context) {
|
||||||
|
return LockStateWidget(
|
||||||
|
assumedState: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
lib/entities/lock/widgets/lock_state.dart
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class LockStateWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool assumedState;
|
||||||
|
|
||||||
|
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
|
||||||
|
|
||||||
|
void _lock(Entity entity) {
|
||||||
|
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unlock(Entity entity) {
|
||||||
|
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final LockEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
if (assumedState) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
height: 34.0,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: () => _unlock(entity),
|
||||||
|
child: Text("UNLOCK",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 34.0,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: () => _lock(entity),
|
||||||
|
child: Text("LOCK",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return SizedBox(
|
||||||
|
height: 34.0,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: (() {
|
||||||
|
entity.isLocked ? _unlock(entity) : _lock(entity);
|
||||||
|
}),
|
||||||
|
child: Text(
|
||||||
|
entity.isLocked ? "UNLOCK" : "LOCK",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class MediaPlayerEntity extends Entity {
|
class MediaPlayerEntity extends Entity {
|
||||||
|
|
||||||
@ -20,55 +20,55 @@ class MediaPlayerEntity extends Entity {
|
|||||||
static const SUPPORT_SHUFFLE_SET = 32768;
|
static const SUPPORT_SHUFFLE_SET = 32768;
|
||||||
static const SUPPORT_SELECT_SOUND_MODE = 65536;
|
static const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||||
|
|
||||||
MediaPlayerEntity(Map rawData) : super(rawData);
|
MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get supportPause => ((attributes["supported_features"] &
|
bool get supportPause => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_PAUSE) ==
|
MediaPlayerEntity.SUPPORT_PAUSE) ==
|
||||||
MediaPlayerEntity.SUPPORT_PAUSE);
|
MediaPlayerEntity.SUPPORT_PAUSE);
|
||||||
bool get supportSeek => ((attributes["supported_features"] &
|
bool get supportSeek => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_SEEK) ==
|
MediaPlayerEntity.SUPPORT_SEEK) ==
|
||||||
MediaPlayerEntity.SUPPORT_SEEK);
|
MediaPlayerEntity.SUPPORT_SEEK);
|
||||||
bool get supportVolumeSet => ((attributes["supported_features"] &
|
bool get supportVolumeSet => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_VOLUME_SET) ==
|
MediaPlayerEntity.SUPPORT_VOLUME_SET) ==
|
||||||
MediaPlayerEntity.SUPPORT_VOLUME_SET);
|
MediaPlayerEntity.SUPPORT_VOLUME_SET);
|
||||||
bool get supportVolumeMute => ((attributes["supported_features"] &
|
bool get supportVolumeMute => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_VOLUME_MUTE) ==
|
MediaPlayerEntity.SUPPORT_VOLUME_MUTE) ==
|
||||||
MediaPlayerEntity.SUPPORT_VOLUME_MUTE);
|
MediaPlayerEntity.SUPPORT_VOLUME_MUTE);
|
||||||
bool get supportPreviousTrack => ((attributes["supported_features"] &
|
bool get supportPreviousTrack => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) ==
|
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) ==
|
||||||
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK);
|
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK);
|
||||||
bool get supportNextTrack => ((attributes["supported_features"] &
|
bool get supportNextTrack => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_NEXT_TRACK) ==
|
MediaPlayerEntity.SUPPORT_NEXT_TRACK) ==
|
||||||
MediaPlayerEntity.SUPPORT_NEXT_TRACK);
|
MediaPlayerEntity.SUPPORT_NEXT_TRACK);
|
||||||
|
|
||||||
bool get supportTurnOn => ((attributes["supported_features"] &
|
bool get supportTurnOn => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_TURN_ON) ==
|
MediaPlayerEntity.SUPPORT_TURN_ON) ==
|
||||||
MediaPlayerEntity.SUPPORT_TURN_ON);
|
MediaPlayerEntity.SUPPORT_TURN_ON);
|
||||||
bool get supportTurnOff => ((attributes["supported_features"] &
|
bool get supportTurnOff => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_TURN_OFF) ==
|
MediaPlayerEntity.SUPPORT_TURN_OFF) ==
|
||||||
MediaPlayerEntity.SUPPORT_TURN_OFF);
|
MediaPlayerEntity.SUPPORT_TURN_OFF);
|
||||||
bool get supportPlayMedia => ((attributes["supported_features"] &
|
bool get supportPlayMedia => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_PLAY_MEDIA) ==
|
MediaPlayerEntity.SUPPORT_PLAY_MEDIA) ==
|
||||||
MediaPlayerEntity.SUPPORT_PLAY_MEDIA);
|
MediaPlayerEntity.SUPPORT_PLAY_MEDIA);
|
||||||
bool get supportVolumeStep => ((attributes["supported_features"] &
|
bool get supportVolumeStep => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_VOLUME_STEP) ==
|
MediaPlayerEntity.SUPPORT_VOLUME_STEP) ==
|
||||||
MediaPlayerEntity.SUPPORT_VOLUME_STEP);
|
MediaPlayerEntity.SUPPORT_VOLUME_STEP);
|
||||||
bool get supportSelectSource => ((attributes["supported_features"] &
|
bool get supportSelectSource => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_SELECT_SOURCE) ==
|
MediaPlayerEntity.SUPPORT_SELECT_SOURCE) ==
|
||||||
MediaPlayerEntity.SUPPORT_SELECT_SOURCE);
|
MediaPlayerEntity.SUPPORT_SELECT_SOURCE);
|
||||||
bool get supportStop => ((attributes["supported_features"] &
|
bool get supportStop => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_STOP) ==
|
MediaPlayerEntity.SUPPORT_STOP) ==
|
||||||
MediaPlayerEntity.SUPPORT_STOP);
|
MediaPlayerEntity.SUPPORT_STOP);
|
||||||
bool get supportClearPlaylist => ((attributes["supported_features"] &
|
bool get supportClearPlaylist => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) ==
|
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) ==
|
||||||
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST);
|
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST);
|
||||||
bool get supportPlay => ((attributes["supported_features"] &
|
bool get supportPlay => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_PLAY) ==
|
MediaPlayerEntity.SUPPORT_PLAY) ==
|
||||||
MediaPlayerEntity.SUPPORT_PLAY);
|
MediaPlayerEntity.SUPPORT_PLAY);
|
||||||
bool get supportShuffleSet => ((attributes["supported_features"] &
|
bool get supportShuffleSet => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_SHUFFLE_SET) ==
|
MediaPlayerEntity.SUPPORT_SHUFFLE_SET) ==
|
||||||
MediaPlayerEntity.SUPPORT_SHUFFLE_SET);
|
MediaPlayerEntity.SUPPORT_SHUFFLE_SET);
|
||||||
bool get supportSelectSoundMode => ((attributes["supported_features"] &
|
bool get supportSelectSoundMode => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) ==
|
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) ==
|
||||||
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE);
|
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE);
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class MediaPlayerWidget extends StatelessWidget {
|
class MediaPlayerWidget extends StatelessWidget {
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildImage(MediaPlayerEntity entity) {
|
Widget _buildImage(MediaPlayerEntity entity) {
|
||||||
String state = entity.state;
|
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(
|
return Container(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -81,7 +81,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Image(
|
child: Image(
|
||||||
image: CachedNetworkImageProvider("$homeAssistantWebHost${entity.entityPicture}"),
|
image: CachedNetworkImageProvider("${entity.entityPicture}"),
|
||||||
height: 240.0,
|
height: 240.0,
|
||||||
//width: 320.0,
|
//width: 320.0,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
@ -95,7 +95,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(
|
Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:movie"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
||||||
size: 150.0,
|
size: 150.0,
|
||||||
color: EntityColor.stateColor("$state"),
|
color: EntityColor.stateColor("$state"),
|
||||||
)
|
)
|
||||||
@ -227,7 +227,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
|||||||
if (showMenu) {
|
if (showMenu) {
|
||||||
result.add(
|
result.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:dots-vertical")),
|
"mdi:dots-vertical")),
|
||||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
|
||||||
)
|
)
|
||||||
@ -307,11 +307,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
||||||
Widget muteWidget;
|
Widget muteWidget;
|
||||||
Widget volumeStepWidget;
|
Widget volumeStepWidget;
|
||||||
if (entity.supportVolumeMute) {
|
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
|
||||||
bool isMuted = entity.attributes["is_volume_muted"] ?? false;
|
bool isMuted = entity.attributes["is_volume_muted"] ?? false;
|
||||||
muteWidget =
|
muteWidget =
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up),
|
icon: Icon(isMuted ? Icons.volume_up : Icons.volume_off),
|
||||||
onPressed: () => _setVolumeMute(!isMuted, entity.entityId)
|
onPressed: () => _setVolumeMute(!isMuted, entity.entityId)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -322,11 +322,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:plus")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||||
onPressed: () => _setVolumeUp(entity.entityId)
|
onPressed: () => _setVolumeUp(entity.entityId)
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:minus")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||||
onPressed: () => _setVolumeDown(entity.entityId)
|
onPressed: () => _setVolumeDown(entity.entityId)
|
||||||
)
|
)
|
||||||
],
|
],
|
@ -1,11 +1,11 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class SelectEntity extends Entity {
|
class SelectEntity extends Entity {
|
||||||
List<String> get listOptions => attributes["options"] != null
|
List<String> get listOptions => attributes["options"] != null
|
||||||
? (attributes["options"] as List).cast<String>()
|
? (attributes["options"] as List).cast<String>()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
SelectEntity(Map rawData) : super(rawData);
|
SelectEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class SelectStateWidget extends StatefulWidget {
|
class SelectStateWidget extends StatefulWidget {
|
||||||
|
|
@ -1,8 +1,4 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class SunEntity extends Entity {
|
|
||||||
SunEntity(Map rawData) : super(rawData);
|
|
||||||
}
|
|
||||||
|
|
||||||
class SensorEntity extends Entity {
|
class SensorEntity extends Entity {
|
||||||
|
|
||||||
@ -12,6 +8,6 @@ class SensorEntity extends Entity {
|
|||||||
numericState: true
|
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 {
|
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 minValue => _getDoubleAttributeValue("min") ?? 0.0;
|
||||||
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
|
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class SliderControlsWidget extends StatefulWidget {
|
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 {
|
class SwitchEntity extends Entity {
|
||||||
SwitchEntity(Map rawData) : super(rawData);
|
SwitchEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class SwitchStateWidget extends StatefulWidget {
|
class SwitchStateWidget extends StatefulWidget {
|
||||||
|
|
||||||
@ -71,13 +71,13 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _setNewState(false, entity),
|
onPressed: () => _setNewState(false, entity),
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash-off")),
|
||||||
color: newState == EntityState.on ? Colors.black : Colors.blue,
|
color: newState == EntityState.on ? Colors.black : Colors.blue,
|
||||||
iconSize: Sizes.iconSize,
|
iconSize: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _setNewState(true, entity),
|
onPressed: () => _setNewState(true, entity),
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash")),
|
||||||
color: newState == EntityState.on ? Colors.blue : Colors.black,
|
color: newState == EntityState.on ? Colors.blue : Colors.black,
|
||||||
iconSize: Sizes.iconSize
|
iconSize: Sizes.iconSize
|
||||||
)
|
)
|
@ -1,7 +1,7 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class TextEntity extends Entity {
|
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 valueMinLength => attributes["min"] ?? -1;
|
||||||
int get valueMaxLength => attributes["max"] ?? -1;
|
int get valueMaxLength => attributes["max"] ?? -1;
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class TextInputStateWidget extends StatefulWidget {
|
class TextInputStateWidget extends StatefulWidget {
|
||||||
|
|
||||||
@ -73,13 +73,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
obscureText: entity.isPasswordField,
|
obscureText: entity.isPasswordField,
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _tmpValue)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _tmpValue,
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _tmpValue.length)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_tmpValue = 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();
|
||||||
|
}
|
||||||
|
}
|
65
lib/entities/timer/widgets/timer_state.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class TimerState extends StatefulWidget {
|
||||||
|
//final bool expanded;
|
||||||
|
//final TextAlign textAlign;
|
||||||
|
//final EdgeInsetsGeometry padding;
|
||||||
|
//final int maxLines;
|
||||||
|
|
||||||
|
const TimerState({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TimerStateState createState() => _TimerStateState();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimerStateState extends State<TimerState> {
|
||||||
|
|
||||||
|
Timer timer;
|
||||||
|
Duration remaining = Duration(seconds: 0);
|
||||||
|
|
||||||
|
void checkState(TimerEntity entity) {
|
||||||
|
if (entity.state == EntityState.active) {
|
||||||
|
//Logger.d("Timer is active");
|
||||||
|
if (timer == null || !timer.isActive) {
|
||||||
|
timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||||
|
setState(() {
|
||||||
|
try {
|
||||||
|
int passed = DateTime
|
||||||
|
.now()
|
||||||
|
.difference(entity._lastUpdated)
|
||||||
|
.inSeconds;
|
||||||
|
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}");
|
||||||
|
remaining = Duration(seconds: 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timer?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
EntityModel model = EntityModel.of(context);
|
||||||
|
TimerEntity entity = model.entityWrapper.entity;
|
||||||
|
checkState(entity);
|
||||||
|
if (entity.state != EntityState.active) {
|
||||||
|
return SimpleEntityState();
|
||||||
|
} else {
|
||||||
|
return SimpleEntityState(
|
||||||
|
customValue: "${remaining.toString().split('.')[0]}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,19 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class CameraEntity extends Entity {
|
|
||||||
|
|
||||||
static const SUPPORT_ON_OFF = 1;
|
|
||||||
|
|
||||||
CameraEntity(Map rawData) : super(rawData);
|
|
||||||
|
|
||||||
bool get supportOnOff => ((attributes["supported_features"] &
|
|
||||||
CameraEntity.SUPPORT_ON_OFF) ==
|
|
||||||
CameraEntity.SUPPORT_ON_OFF);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
|
||||||
return CameraControlsWidget(
|
|
||||||
url: '$homeAssistantWebHost/api/camera_proxy_stream/camera.demo_camera?token=${this.attributes['access_token']}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,129 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class ClimateEntity extends Entity {
|
|
||||||
|
|
||||||
@override
|
|
||||||
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
|
||||||
chartType: EntityHistoryWidgetType.numericAttributes,
|
|
||||||
numericState: false,
|
|
||||||
numericAttributesToShow: ["current_temperature"]
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
bool get supportTargetTemperature => ((attributes["supported_features"] &
|
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
|
||||||
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] &
|
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
|
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
|
|
||||||
bool get supportTargetTemperatureLow => ((attributes["supported_features"] &
|
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
|
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
|
|
||||||
bool get supportTargetHumidity => ((attributes["supported_features"] &
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
|
||||||
bool get supportTargetHumidityHigh => ((attributes["supported_features"] &
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
|
|
||||||
bool get supportTargetHumidityLow => ((attributes["supported_features"] &
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
|
|
||||||
bool get supportFanMode =>
|
|
||||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) ==
|
|
||||||
ClimateEntity.SUPPORT_FAN_MODE);
|
|
||||||
bool get supportOperationMode => ((attributes["supported_features"] &
|
|
||||||
ClimateEntity.SUPPORT_OPERATION_MODE) ==
|
|
||||||
ClimateEntity.SUPPORT_OPERATION_MODE);
|
|
||||||
bool get supportHoldMode =>
|
|
||||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) ==
|
|
||||||
ClimateEntity.SUPPORT_HOLD_MODE);
|
|
||||||
bool get supportSwingMode =>
|
|
||||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) ==
|
|
||||||
ClimateEntity.SUPPORT_SWING_MODE);
|
|
||||||
bool get supportAwayMode =>
|
|
||||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
|
|
||||||
ClimateEntity.SUPPORT_AWAY_MODE);
|
|
||||||
bool get supportAuxHeat =>
|
|
||||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
|
||||||
ClimateEntity.SUPPORT_AUX_HEAT);
|
|
||||||
bool get supportOnOff =>
|
|
||||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) ==
|
|
||||||
ClimateEntity.SUPPORT_ON_OFF);
|
|
||||||
|
|
||||||
List<String> get operationList => attributes["operation_list"] != null
|
|
||||||
? (attributes["operation_list"] as List).cast<String>()
|
|
||||||
: null;
|
|
||||||
List<String> get fanList => attributes["fan_list"] != null
|
|
||||||
? (attributes["fan_list"] as List).cast<String>()
|
|
||||||
: null;
|
|
||||||
List<String> get swingList => attributes["swing_list"] != null
|
|
||||||
? (attributes["swing_list"] as List).cast<String>()
|
|
||||||
: null;
|
|
||||||
double get temperature => _getDoubleAttributeValue('temperature');
|
|
||||||
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
|
||||||
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
|
||||||
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
|
||||||
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
|
|
||||||
double get targetHumidity => _getDoubleAttributeValue('humidity');
|
|
||||||
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 fanMode => attributes['fan_mode'];
|
|
||||||
String get swingMode => attributes['swing_mode'];
|
|
||||||
bool get awayMode => attributes['away_mode'] == "on";
|
|
||||||
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);
|
|
||||||
if (supportTargetTemperature) {
|
|
||||||
historyConfig.numericAttributesToShow.add("temperature");
|
|
||||||
}
|
|
||||||
if (supportTargetTemperatureHigh) {
|
|
||||||
historyConfig.numericAttributesToShow.add("target_temp_high");
|
|
||||||
}
|
|
||||||
if (supportTargetTemperatureLow) {
|
|
||||||
historyConfig.numericAttributesToShow.add("target_temp_low");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildStatePart(BuildContext context) {
|
|
||||||
return ClimateStateWidget();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
|
||||||
return ClimateControlWidget();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double _getDoubleAttributeValue(String attributeName) {
|
|
||||||
var temp1 = attributes["$attributeName"];
|
|
||||||
if (temp1 is int) {
|
|
||||||
return temp1.toDouble();
|
|
||||||
} else if (temp1 is double) {
|
|
||||||
return temp1;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class LockEntity extends Entity {
|
|
||||||
LockEntity(Map rawData) : super(rawData);
|
|
||||||
|
|
||||||
bool get isLocked => state == "locked";
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildStatePart(BuildContext context) {
|
|
||||||
return LockStateWidget();
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,13 +2,15 @@ part of 'main.dart';
|
|||||||
|
|
||||||
class EntityCollection {
|
class EntityCollection {
|
||||||
|
|
||||||
|
final homeAssistantWebHost;
|
||||||
|
|
||||||
Map<String, Entity> _allEntities;
|
Map<String, Entity> _allEntities;
|
||||||
//Map<String, Entity> views;
|
//Map<String, Entity> views;
|
||||||
|
|
||||||
bool get isEmpty => _allEntities.isEmpty;
|
bool get isEmpty => _allEntities.isEmpty;
|
||||||
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
||||||
|
|
||||||
EntityCollection() {
|
EntityCollection(this.homeAssistantWebHost) {
|
||||||
_allEntities = {};
|
_allEntities = {};
|
||||||
//views = {};
|
//views = {};
|
||||||
}
|
}
|
||||||
@ -33,67 +35,74 @@ class EntityCollection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_allEntities.clear();
|
||||||
|
}
|
||||||
|
|
||||||
Entity _createEntityInstance(rawEntityData) {
|
Entity _createEntityInstance(rawEntityData) {
|
||||||
switch (rawEntityData["entity_id"].split(".")[0]) {
|
switch (rawEntityData["entity_id"].split(".")[0]) {
|
||||||
case 'sun': {
|
case 'sun': {
|
||||||
return SunEntity(rawEntityData);
|
return SunEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "media_player": {
|
case "media_player": {
|
||||||
return MediaPlayerEntity(rawEntityData);
|
return MediaPlayerEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case 'sensor': {
|
case 'sensor': {
|
||||||
return SensorEntity(rawEntityData);
|
return SensorEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case 'lock': {
|
case 'lock': {
|
||||||
return LockEntity(rawEntityData);
|
return LockEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "automation": {
|
case "automation": {
|
||||||
return AutomationEntity(rawEntityData);
|
return AutomationEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "input_boolean":
|
case "input_boolean":
|
||||||
case "switch": {
|
case "switch": {
|
||||||
return SwitchEntity(rawEntityData);
|
return SwitchEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "light": {
|
case "light": {
|
||||||
return LightEntity(rawEntityData);
|
return LightEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "group": {
|
case "group": {
|
||||||
return GroupEntity(rawEntityData);
|
return GroupEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "script":
|
case "script":
|
||||||
case "scene": {
|
case "scene": {
|
||||||
return ButtonEntity(rawEntityData);
|
return ButtonEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "input_datetime": {
|
case "input_datetime": {
|
||||||
return DateTimeEntity(rawEntityData);
|
return DateTimeEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "input_select": {
|
case "input_select": {
|
||||||
return SelectEntity(rawEntityData);
|
return SelectEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "input_number": {
|
case "input_number": {
|
||||||
return SliderEntity(rawEntityData);
|
return SliderEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "input_text": {
|
case "input_text": {
|
||||||
return TextEntity(rawEntityData);
|
return TextEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "climate": {
|
case "climate": {
|
||||||
return ClimateEntity(rawEntityData);
|
return ClimateEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "cover": {
|
case "cover": {
|
||||||
return CoverEntity(rawEntityData);
|
return CoverEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "fan": {
|
case "fan": {
|
||||||
return FanEntity(rawEntityData);
|
return FanEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "camera": {
|
case "camera": {
|
||||||
return CameraEntity(rawEntityData);
|
return CameraEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "alarm_control_panel": {
|
case "alarm_control_panel": {
|
||||||
return AlarmControlPanelEntity(rawEntityData);
|
return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "timer": {
|
||||||
|
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return Entity(rawEntityData);
|
return Entity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,7 +127,7 @@ class EntityCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateFromRaw(Map rawEntityData) {
|
void updateFromRaw(Map rawEntityData) {
|
||||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
|
get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity get(String entityId) {
|
Entity get(String entityId) {
|
||||||
@ -140,6 +149,17 @@ class EntityCollection {
|
|||||||
return _allEntities[entityId] != null;
|
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> filterEntitiesForDefaultView() {
|
||||||
List<Entity> result = [];
|
List<Entity> result = [];
|
||||||
List<Entity> groups = [];
|
List<Entity> groups = [];
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class ButtonEntityContainer extends StatelessWidget {
|
|
||||||
|
|
||||||
ButtonEntityContainer({
|
|
||||||
Key key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 0.4,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.fitHeight,
|
|
||||||
child: EntityIcon(
|
|
||||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
|
||||||
iconSize: Sizes.iconSize,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildName()
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildName() {
|
|
||||||
return EntityName(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 3,
|
|
||||||
wordsWrap: true,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
fontSize: Sizes.nameFontSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,11 +14,11 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
{
|
{
|
||||||
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
||||||
? Icon(
|
? Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
|
MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
)
|
)
|
||||||
: Icon(
|
: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
|
MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -27,27 +27,44 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
case "media_player":
|
case "media_player":
|
||||||
case "binary_sensor":
|
case "binary_sensor":
|
||||||
{
|
{
|
||||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
badgeIcon = EntityIcon(
|
||||||
entityModel.entityWrapper, iconSize, Colors.black);
|
padding: EdgeInsets.all(0.0),
|
||||||
|
size: iconSize,
|
||||||
|
color: Colors.black
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "device_tracker":
|
case "device_tracker":
|
||||||
|
case "person":
|
||||||
{
|
{
|
||||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
badgeIcon = EntityIcon(
|
||||||
entityModel.entityWrapper, iconSize, Colors.black);
|
padding: EdgeInsets.all(0.0),
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.state;
|
size: iconSize,
|
||||||
|
color: Colors.black
|
||||||
|
);
|
||||||
|
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
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(
|
badgeIcon = Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"${entityModel.entityWrapper.entity.state}",
|
"${entityModel.entityWrapper.entity.displayState}",
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 17.0),
|
style: TextStyle(fontSize: stateFontSize),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
58
lib/entity_widgets/common/camera_stream_view.dart
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class CameraStreamView extends StatefulWidget {
|
||||||
|
|
||||||
|
CameraStreamView({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CameraStreamViewState createState() => _CameraStreamViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CameraStreamViewState extends State<CameraStreamView> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
CameraEntity _entity;
|
||||||
|
bool started = false;
|
||||||
|
String streamUrl = "";
|
||||||
|
|
||||||
|
launchStream() {
|
||||||
|
Launcher.launchURLInCustomTab(
|
||||||
|
context: context,
|
||||||
|
url: streamUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!started) {
|
||||||
|
_entity = EntityModel
|
||||||
|
.of(context)
|
||||||
|
.entityWrapper
|
||||||
|
.entity;
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
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: IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
||||||
|
iconSize: 50.0,
|
||||||
|
onPressed: () => launchStream(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -7,20 +7,9 @@ class EntityAttributesList extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final entityModel = EntityModel.of(context);
|
final entityModel = EntityModel.of(context);
|
||||||
List<Widget> attrs = [];
|
List<Widget> attrs = [];
|
||||||
if ((entityModel.entityWrapper.entity.attributesToShow == null) ||
|
|
||||||
(entityModel.entityWrapper.entity.attributesToShow.contains("all"))) {
|
|
||||||
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
|
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
|
||||||
attrs.add(_buildSingleAttribute("$name", "$value"));
|
attrs.add(_buildSingleAttribute("$name", "${value ?? '-'}"));
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
entityModel.entityWrapper.entity.attributesToShow.forEach((String attr) {
|
|
||||||
String attrValue = entityModel.entityWrapper.entity.getAttribute("$attr");
|
|
||||||
if (attrValue != null) {
|
|
||||||
attrs.add(
|
|
||||||
_buildSingleAttribute("$attr", "$attrValue"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -49,7 +38,7 @@ class EntityAttributesList extends StatelessWidget {
|
|||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"$value",
|
"${value}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
59
lib/entity_widgets/common/simple_state.dart
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class SimpleEntityState extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool expanded;
|
||||||
|
final TextAlign textAlign;
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
final int maxLines;
|
||||||
|
final String customValue;
|
||||||
|
final double fontSize;
|
||||||
|
final bool bold;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
String state;
|
||||||
|
if (customValue == null) {
|
||||||
|
state = entityModel.entityWrapper.entity.displayState ?? "";
|
||||||
|
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
|
||||||
|
} else {
|
||||||
|
state = customValue;
|
||||||
|
}
|
||||||
|
TextStyle textStyle = TextStyle(
|
||||||
|
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.unitOfMeasurement}",
|
||||||
|
textAlign: textAlign,
|
||||||
|
maxLines: maxLines,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
softWrap: true,
|
||||||
|
style: textStyle
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (expanded) {
|
||||||
|
return Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
flex: 2,
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,115 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class CameraControlsWidget extends StatefulWidget {
|
|
||||||
|
|
||||||
final String url;
|
|
||||||
|
|
||||||
CameraControlsWidget({Key key, @required this.url}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_CameraControlsWidgetState createState() => _CameraControlsWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CameraControlsWidgetState extends State<CameraControlsWidget> {
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_getData();
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Client client;
|
|
||||||
http.StreamedResponse response;
|
|
||||||
List<int> binaryImage = [];
|
|
||||||
String cameraState = "Connecting...";
|
|
||||||
bool timeToStop = false;
|
|
||||||
Completer streamCompleter;
|
|
||||||
|
|
||||||
void _getData() async {
|
|
||||||
client = new http.Client(); // create a client to make api calls
|
|
||||||
http.Request request = new http.Request("GET", Uri.parse(widget.url)); // create get request
|
|
||||||
Logger.d("[Sending] ==> ${widget.url}");
|
|
||||||
response = await client.send(request);
|
|
||||||
setState(() {
|
|
||||||
cameraState = "Starting...";
|
|
||||||
});
|
|
||||||
Logger.d("[Received] <== ${response.headers}");
|
|
||||||
List<int> primaryBuffer=[];
|
|
||||||
final int imageSizeStart = 59;
|
|
||||||
int imageSizeEnd = 0;
|
|
||||||
int imageStart = 0;
|
|
||||||
int imageSize = 0;
|
|
||||||
String strBuffer = "";
|
|
||||||
streamCompleter = Completer();
|
|
||||||
response.stream.transform(
|
|
||||||
StreamTransformer.fromHandlers(
|
|
||||||
handleData: (data, sink) {
|
|
||||||
primaryBuffer.addAll(data);
|
|
||||||
if (primaryBuffer.length >= 66) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
imageSize = int.tryParse(utf8.decode(
|
|
||||||
primaryBuffer.sublist(imageSizeStart, imageSizeEnd),
|
|
||||||
allowMalformed: true));
|
|
||||||
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) {
|
|
||||||
sink?.close();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
).listen((d) {
|
|
||||||
setState(() {
|
|
||||||
binaryImage = d;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (binaryImage.isEmpty) {
|
|
||||||
return Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Text("$cameraState")
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Image.memory(Uint8List.fromList(binaryImage), gaplessPlayback: true),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
timeToStop = true;
|
|
||||||
if (streamCompleter != null && !streamCompleter.isCompleted) {
|
|
||||||
streamCompleter.future.then((_) {
|
|
||||||
client?.close();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
client?.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,6 +11,45 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
}
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
||||||
|
return Divider(
|
||||||
|
color: Colors.black45,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Divider(
|
||||||
|
color: Colors.black45,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${entityModel.entityWrapper.entity.displayName}",
|
||||||
|
style: TextStyle(color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Widget result = Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
EntityIcon(),
|
||||||
|
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
flex: 3,
|
||||||
|
child: EntityName(
|
||||||
|
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
state
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (entityModel.handleTap) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
if (entityModel.handleTap) {
|
if (entityModel.handleTap) {
|
||||||
@ -22,19 +61,10 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
entityModel.entityWrapper.handleTap();
|
entityModel.entityWrapper.handleTap();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Row(
|
child: result,
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
EntityIcon(),
|
|
||||||
|
|
||||||
Flexible(
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
flex: 3,
|
|
||||||
child: EntityName(),
|
|
||||||
),
|
|
||||||
state
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,6 +2,8 @@ part of '../main.dart';
|
|||||||
|
|
||||||
class EntityColor {
|
class EntityColor {
|
||||||
|
|
||||||
|
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
|
||||||
|
|
||||||
static const badgeColors = {
|
static const badgeColors = {
|
||||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||||
@ -10,15 +12,17 @@ class EntityColor {
|
|||||||
static const _stateColors = {
|
static const _stateColors = {
|
||||||
EntityState.on: Colors.amber,
|
EntityState.on: Colors.amber,
|
||||||
"auto": Colors.amber,
|
"auto": Colors.amber,
|
||||||
EntityState.idle: Colors.amber,
|
EntityState.active: Colors.amber,
|
||||||
EntityState.playing: Colors.amber,
|
EntityState.playing: Colors.amber,
|
||||||
|
EntityState.paused: Colors.amber,
|
||||||
"above_horizon": Colors.amber,
|
"above_horizon": Colors.amber,
|
||||||
EntityState.home: Colors.amber,
|
EntityState.home: Colors.amber,
|
||||||
EntityState.open: Colors.amber,
|
EntityState.open: Colors.amber,
|
||||||
EntityState.off: Color.fromRGBO(68, 115, 158, 1.0),
|
EntityState.off: defaultStateColor,
|
||||||
EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0),
|
EntityState.closed: defaultStateColor,
|
||||||
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0),
|
"below_horizon": defaultStateColor,
|
||||||
"default": Color.fromRGBO(68, 115, 158, 1.0),
|
"default": defaultStateColor,
|
||||||
|
EntityState.idle: defaultStateColor,
|
||||||
"heat": Colors.redAccent,
|
"heat": Colors.redAccent,
|
||||||
"cool": Colors.lightBlue,
|
"cool": Colors.lightBlue,
|
||||||
EntityState.unavailable: Colors.black26,
|
EntityState.unavailable: Colors.black26,
|
||||||
|
@ -3,20 +3,71 @@ part of '../main.dart';
|
|||||||
class EntityIcon extends StatelessWidget {
|
class EntityIcon extends StatelessWidget {
|
||||||
|
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final double iconSize;
|
final double size;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB(
|
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
|
||||||
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key);
|
|
||||||
|
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||||
|
String domain = entityId.split(".")[0];
|
||||||
|
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
|
||||||
|
String iconNameByDeviceClass;
|
||||||
|
if (deviceClass != null) {
|
||||||
|
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
|
||||||
|
}
|
||||||
|
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
|
||||||
|
if (iconName != null) {
|
||||||
|
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildIcon(EntityWrapper data, Color color) {
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (data.entityPicture != null) {
|
||||||
|
return Container(
|
||||||
|
height: size+12,
|
||||||
|
width: size+12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
image: DecorationImage(
|
||||||
|
fit:BoxFit.cover,
|
||||||
|
image: CachedNetworkImageProvider(
|
||||||
|
"${data.entityPicture}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
String iconName = data.icon;
|
||||||
|
int iconCode = 0;
|
||||||
|
if (iconName.length > 0) {
|
||||||
|
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||||
|
} else {
|
||||||
|
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||||
|
data.entity.deviceClass, data.entity.state); //
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
|
||||||
|
child: Icon(
|
||||||
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
|
size: size,
|
||||||
|
color: color,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
child: buildIcon(
|
||||||
entityWrapper,
|
entityWrapper,
|
||||||
iconSize,
|
color ?? EntityColor.stateColor(entityWrapper.entity.state)
|
||||||
EntityColor.stateColor(entityWrapper.entity.state)
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,10 @@ class EntityName extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
TextStyle textStyle = TextStyle(fontSize: fontSize);
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||||
|
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
|
||||||
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -21,7 +25,7 @@ class EntityName extends StatelessWidget {
|
|||||||
overflow: textOverflow,
|
overflow: textOverflow,
|
||||||
softWrap: wordsWrap,
|
softWrap: wordsWrap,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
style: TextStyle(fontSize: fontSize),
|
style: textStyle,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -148,7 +148,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
});
|
});
|
||||||
|
|
||||||
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
|
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
|
||||||
_selectedId = 0;
|
_selectedId = numericDataLists.length -1;
|
||||||
}
|
}
|
||||||
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
||||||
numericDataLists.forEach((attrName, dataList) {
|
numericDataLists.forEach((attrName, dataList) {
|
||||||
@ -202,6 +202,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
_selectedId -= 1;
|
_selectedId -= 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = _parsedHistory.first.data.length - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectNext() {
|
void _selectNext() {
|
||||||
@ -210,6 +215,12 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
_selectedId += 1;
|
_selectedId += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(charts.SelectionModel model) {
|
void _onSelectionChanged(charts.SelectionModel model) {
|
||||||
|
@ -32,6 +32,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
List _history;
|
List _history;
|
||||||
bool _needToUpdateHistory;
|
bool _needToUpdateHistory;
|
||||||
DateTime _historyLastUpdated;
|
DateTime _historyLastUpdated;
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -39,37 +40,40 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
_needToUpdateHistory = true;
|
_needToUpdateHistory = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadHistory(HomeAssistant ha, String entityId) {
|
void _loadHistory(String entityId) {
|
||||||
DateTime now = DateTime.now();
|
DateTime now = DateTime.now();
|
||||||
if (_historyLastUpdated != null) {
|
if (_historyLastUpdated != null) {
|
||||||
Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
|
Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
|
||||||
}
|
}
|
||||||
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
||||||
_historyLastUpdated = now;
|
_historyLastUpdated = now;
|
||||||
ha.getHistory(entityId).then((history){
|
ConnectionManager().getHistory(entityId).then((history){
|
||||||
|
if (!_disposed) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_history = history.isNotEmpty ? history[0] : [];
|
_history = history.isNotEmpty ? history[0] : [];
|
||||||
_needToUpdateHistory = false;
|
_needToUpdateHistory = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
Logger.e("Error loading $entityId history: $e");
|
Logger.e("Error loading $entityId history: $e");
|
||||||
|
if (!_disposed) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_history = [];
|
_history = [];
|
||||||
_needToUpdateHistory = false;
|
_needToUpdateHistory = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
|
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
final Entity entity = entityModel.entityWrapper.entity;
|
final Entity entity = entityModel.entityWrapper.entity;
|
||||||
if (!_needToUpdateHistory) {
|
if (!_needToUpdateHistory) {
|
||||||
_needToUpdateHistory = true;
|
_needToUpdateHistory = true;
|
||||||
} else {
|
} else {
|
||||||
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
|
_loadHistory(entity.entityId);
|
||||||
}
|
}
|
||||||
return _buildChart();
|
return _buildChart();
|
||||||
}
|
}
|
||||||
@ -131,4 +135,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposed = true;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -103,7 +103,7 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
id: widget.rawHistory.length
|
id: widget.rawHistory.length
|
||||||
));
|
));
|
||||||
if (_selectedId == -1) {
|
if (_selectedId == -1) {
|
||||||
_selectedId = 0;
|
_selectedId = data.length - 1;
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
@ -132,6 +132,11 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
_selectedId -= 1;
|
_selectedId -= 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = _parsedHistory.first.data.length - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectNext() {
|
void _selectNext() {
|
||||||
@ -140,6 +145,12 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
_selectedId += 1;
|
_selectedId += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(charts.SelectionModel model) {
|
void _onSelectionChanged(charts.SelectionModel model) {
|
||||||
|
@ -101,7 +101,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
colorId: data.last.colorId
|
colorId: data.last.colorId
|
||||||
));
|
));
|
||||||
if (_selectedId == -1) {
|
if (_selectedId == -1) {
|
||||||
_selectedId = 0;
|
_selectedId = data.length - 1;
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
@ -137,14 +137,25 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
_selectedId -= 1;
|
_selectedId -= 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = _parsedHistory.first.data.length - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectNext() {
|
void _selectNext() {
|
||||||
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
|
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedId += 1;
|
_selectedId += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(charts.SelectionModel model) {
|
void _onSelectionChanged(charts.SelectionModel model) {
|
||||||
|
19
lib/entity_widgets/missed_entity.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class MissedEntityWidget extends StatelessWidget {
|
||||||
|
MissedEntityWidget({
|
||||||
|
Key key
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
return Container(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(5.0),
|
||||||
|
child: Text("Entity not available: ${entityModel.entityWrapper.entity.entityId}"),
|
||||||
|
),
|
||||||
|
color: Colors.amber[100],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -20,23 +20,3 @@ class EntityModel extends InheritedWidget {
|
|||||||
return true;
|
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,32 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class LockStateWidget extends StatelessWidget {
|
|
||||||
|
|
||||||
void _lock(Entity entity) {
|
|
||||||
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _unlock(Entity entity) {
|
|
||||||
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final entityModel = EntityModel.of(context);
|
|
||||||
final LockEntity entity = entityModel.entityWrapper.entity;
|
|
||||||
return SizedBox(
|
|
||||||
height: 34.0,
|
|
||||||
child: FlatButton(
|
|
||||||
onPressed: (() {
|
|
||||||
entity.isLocked ? _unlock(entity) : _lock(entity);
|
|
||||||
}),
|
|
||||||
child: Text(
|
|
||||||
entity.isLocked ? "UNLOCK" : "LOCK",
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
style:
|
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class SimpleEntityState extends StatelessWidget {
|
|
||||||
|
|
||||||
final bool expanded;
|
|
||||||
final TextAlign textAlign;
|
|
||||||
final EdgeInsetsGeometry padding;
|
|
||||||
final int maxLines;
|
|
||||||
|
|
||||||
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)}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final entityModel = EntityModel.of(context);
|
|
||||||
String state = entityModel.entityWrapper.entity.displayState ?? "";
|
|
||||||
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
|
|
||||||
while (state.contains(" ")){
|
|
||||||
state = state.replaceAll(" ", " ");
|
|
||||||
}
|
|
||||||
Widget result = Padding(
|
|
||||||
padding: padding,
|
|
||||||
child: Text(
|
|
||||||
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
|
||||||
textAlign: textAlign,
|
|
||||||
maxLines: maxLines,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
softWrap: true,
|
|
||||||
style: new TextStyle(
|
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (expanded) {
|
|
||||||
return Flexible(
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
flex: 2,
|
|
||||||
child: result,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +1,30 @@
|
|||||||
part of 'main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class HomeAssistant {
|
class HomeAssistant {
|
||||||
String _webSocketAPIEndpoint;
|
|
||||||
String _password;
|
|
||||||
bool _useLovelace = false;
|
|
||||||
|
|
||||||
IOWebSocketChannel _hassioChannel;
|
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||||
SendMessageQueue _messageQueue;
|
|
||||||
|
factory HomeAssistant() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
int _currentMessageId = 0;
|
|
||||||
int _statesMessageId = 0;
|
|
||||||
int _servicesMessageId = 0;
|
|
||||||
int _subscriptionMessageId = 0;
|
|
||||||
int _configMessageId = 0;
|
|
||||||
int _userInfoMessageId = 0;
|
|
||||||
int _lovelaceMessageId = 0;
|
|
||||||
EntityCollection entities;
|
EntityCollection entities;
|
||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
|
Map services;
|
||||||
String _userName;
|
String _userName;
|
||||||
|
HSVColor savedColor;
|
||||||
|
|
||||||
|
String fcmToken;
|
||||||
|
|
||||||
Map _rawLovelaceData;
|
Map _rawLovelaceData;
|
||||||
|
|
||||||
Completer _fetchCompleter;
|
List<Panel> panels = [];
|
||||||
Completer _statesCompleter;
|
|
||||||
Completer _servicesCompleter;
|
|
||||||
Completer _lovelaceCompleter;
|
|
||||||
Completer _configCompleter;
|
|
||||||
Completer _connectionCompleter;
|
|
||||||
Completer _userInfoCompleter;
|
|
||||||
Timer _connectionTimer;
|
|
||||||
Timer _fetchTimer;
|
|
||||||
bool autoReconnect = false;
|
|
||||||
|
|
||||||
StreamSubscription _socketSubscription;
|
|
||||||
|
|
||||||
int messageExpirationTime = 30; //seconds
|
|
||||||
Duration fetchTimeout = Duration(seconds: 30);
|
Duration fetchTimeout = Duration(seconds: 30);
|
||||||
Duration connectTimeout = Duration(seconds: 15);
|
|
||||||
|
|
||||||
String get locationName {
|
String get locationName {
|
||||||
if (_useLovelace) {
|
if (ConnectionManager().useLovelace) {
|
||||||
return ui?.title ?? "";
|
return ui?.title ?? "";
|
||||||
} else {
|
} else {
|
||||||
return _instanceConfig["location_name"] ?? "";
|
return _instanceConfig["location_name"] ?? "";
|
||||||
@ -48,360 +32,128 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
String get userName => _userName ?? locationName;
|
String get userName => _userName ?? locationName;
|
||||||
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
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() {
|
HomeAssistant._internal() {
|
||||||
entities = EntityCollection();
|
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||||
_messageQueue = SendMessageQueue(messageExpirationTime);
|
DeviceInfoManager().loadDeviceInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSettings(String url, String password, bool useLovelace) {
|
Completer _fetchCompleter;
|
||||||
_webSocketAPIEndpoint = url;
|
|
||||||
_password = password;
|
|
||||||
_useLovelace = useLovelace;
|
|
||||||
Logger.d( "Use lovelace is $_useLovelace");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future fetch() {
|
Future fetchData() {
|
||||||
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
|
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||||
Logger.w("Previous fetch is not complited");
|
Logger.w("Previous data fetch is not completed yet");
|
||||||
} 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _fetchCompleter.future;
|
return _fetchCompleter.future;
|
||||||
}
|
}
|
||||||
|
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||||
disconnect() async {
|
_fetchCompleter = Completer();
|
||||||
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 {
|
|
||||||
List<Future> futures = [];
|
List<Future> futures = [];
|
||||||
futures.add(_getStates());
|
futures.add(_getStates());
|
||||||
if (_useLovelace) {
|
if (ConnectionManager().useLovelace) {
|
||||||
futures.add(_getLovelace());
|
futures.add(_getLovelace());
|
||||||
}
|
}
|
||||||
futures.add(_getConfig());
|
futures.add(_getConfig());
|
||||||
futures.add(_getServices());
|
futures.add(_getServices());
|
||||||
futures.add(_getUserInfo());
|
futures.add(_getUserInfo());
|
||||||
try {
|
futures.add(_getPanels());
|
||||||
await Future.wait(futures);
|
futures.add(ConnectionManager().sendSocketMessage(
|
||||||
|
type: "subscribe_events",
|
||||||
|
additionalData: {"event_type": "state_changed"},
|
||||||
|
));
|
||||||
|
Future.wait(futures).then((_) {
|
||||||
|
if (isMobileAppEnabled) {
|
||||||
_createUI();
|
_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();
|
_fetchCompleter.complete();
|
||||||
}
|
MobileAppIntegrationManager.checkAppRegistration();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _completeConnecting(error) {
|
|
||||||
_connectionTimer.cancel();
|
|
||||||
if (!_connectionCompleter.isCompleted) {
|
|
||||||
if (error != null) {
|
|
||||||
_connectionCompleter.completeError(error);
|
|
||||||
} else {
|
} else {
|
||||||
_connectionCompleter.complete();
|
_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")]));
|
||||||
}
|
}
|
||||||
} else if (error != null) {
|
}).catchError((e) {
|
||||||
if (error is Error) {
|
_fetchCompleter.completeError(e);
|
||||||
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") {
|
|
||||||
_sendAuthMessageRaw('{"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'}");
|
|
||||||
if (data["id"] == _configMessageId) {
|
|
||||||
_parseConfig(data);
|
|
||||||
} else if (data["id"] == _statesMessageId) {
|
|
||||||
_parseEntities(data);
|
|
||||||
} else if (data["id"] == _lovelaceMessageId) {
|
|
||||||
_handleLovelace(data);
|
|
||||||
} else if (data["id"] == _servicesMessageId) {
|
|
||||||
_parseServices(data);
|
|
||||||
} else if (data["id"] == _userInfoMessageId) {
|
|
||||||
_parseUserInfo(data);
|
|
||||||
}
|
|
||||||
} 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;
|
|
||||||
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getConfig() {
|
|
||||||
_configCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_configMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
|
|
||||||
|
|
||||||
return _configCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getStates() {
|
|
||||||
_statesCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_statesMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
|
|
||||||
|
|
||||||
return _statesCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getLovelace() {
|
|
||||||
_lovelaceCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_lovelaceMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false);
|
|
||||||
|
|
||||||
return _lovelaceCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getUserInfo() {
|
|
||||||
_userInfoCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_userInfoMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
|
|
||||||
|
|
||||||
return _userInfoCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getServices() {
|
|
||||||
_servicesCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_servicesMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
|
|
||||||
|
|
||||||
return _servicesCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
_incrementMessageId() {
|
|
||||||
_currentMessageId += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendAuthMessageRaw(String message) {
|
|
||||||
Logger.d( "[Sending] ==> auth request");
|
|
||||||
_hassioChannel.sink.add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendMessageRaw(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) {
|
return _fetchCompleter.future;
|
||||||
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) {
|
Future logout() async {
|
||||||
_incrementMessageId();
|
Logger.d("Logging out...");
|
||||||
String message = "";
|
await ConnectionManager().logout().then((_) {
|
||||||
if (entityId != null) {
|
ui?.clear();
|
||||||
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
|
entities?.clear();
|
||||||
if (additionalParams != null) {
|
panels?.clear();
|
||||||
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 += '}';
|
Future _getConfig() async {
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||||
|
_instanceConfig = Map.from(data);
|
||||||
|
}).catchError((e) {
|
||||||
|
throw HAError("Error getting config: ${e}");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
message += '}';
|
|
||||||
|
Future _getStates() async {
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||||
|
(data) => entities.parse(data)
|
||||||
|
).catchError((e) {
|
||||||
|
throw HAError("Error getting states: $e");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return _sendMessageRaw(message, true);
|
|
||||||
|
Future _getLovelace() async {
|
||||||
|
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 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 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 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,
|
||||||
|
type: v["component_name"],
|
||||||
|
title: title,
|
||||||
|
urlPath: v["url_path"],
|
||||||
|
config: v["config"],
|
||||||
|
icon: v["icon"]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
throw HAError("Error getting panels list: $e");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleEntityStateChange(Map eventData) {
|
void _handleEntityStateChange(Map eventData) {
|
||||||
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||||
|
if (_fetchCompleter.isCompleted) {
|
||||||
Map data = Map.from(eventData);
|
Map data = Map.from(eventData);
|
||||||
eventBus.fire(new StateChangedEvent(
|
eventBus.fire(new StateChangedEvent(
|
||||||
entityId: data["entity_id"],
|
entityId: data["entity_id"],
|
||||||
needToRebuildUI: entities.updateState(data)
|
needToRebuildUI: entities.updateState(data)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseConfig(Map data) {
|
|
||||||
if (data["success"] == true) {
|
|
||||||
_instanceConfig = Map.from(data["result"]);
|
|
||||||
_configCompleter.complete();
|
|
||||||
} else {
|
|
||||||
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseUserInfo(Map data) {
|
|
||||||
if (data["success"] == true) {
|
|
||||||
_userName = data["result"]["name"];
|
|
||||||
} else {
|
|
||||||
_userName = null;
|
|
||||||
Logger.w("There was an error getting current user: $data");
|
|
||||||
}
|
|
||||||
_userInfoCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseServices(response) {
|
|
||||||
_servicesCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleLovelace(response) {
|
|
||||||
if (response["success"] == true) {
|
|
||||||
_rawLovelaceData = response["result"];
|
|
||||||
} else {
|
|
||||||
Logger.e("There was an error getting Lovelace config: $response");
|
|
||||||
_rawLovelaceData = null;
|
|
||||||
}
|
|
||||||
_lovelaceCompleter.complete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseLovelace() {
|
void _parseLovelace() {
|
||||||
@ -415,7 +167,8 @@ class HomeAssistant {
|
|||||||
count: viewCounter,
|
count: viewCounter,
|
||||||
id: "${rawView['id']}",
|
id: "${rawView['id']}",
|
||||||
name: rawView['title'],
|
name: rawView['title'],
|
||||||
iconName: rawView['icon']
|
iconName: rawView['icon'],
|
||||||
|
panel: rawView['panel'] ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rawView['badges'] != null && rawView['badges'] is List) {
|
if (rawView['badges'] != null && rawView['badges'] is List) {
|
||||||
@ -439,37 +192,77 @@ class HomeAssistant {
|
|||||||
List<HACard> result = [];
|
List<HACard> result = [];
|
||||||
rawCards.forEach((rawCard){
|
rawCards.forEach((rawCard){
|
||||||
try {
|
try {
|
||||||
bool isThereCardOptionsInside = rawCard["card"] != null;
|
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||||
|
var rawCardInfo = rawCard["card"] ?? rawCard;
|
||||||
HACard card = HACard(
|
HACard card = HACard(
|
||||||
id: "card",
|
id: "card",
|
||||||
name: isThereCardOptionsInside ? rawCard["card"]["title"] ??
|
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||||
rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
|
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||||
type: isThereCardOptionsInside
|
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||||
? rawCard["card"]['type']
|
showName: rawCardInfo['show_name'] ?? true,
|
||||||
: rawCard['type'],
|
showState: rawCardInfo['show_state'] ?? true,
|
||||||
columnsCount: isThereCardOptionsInside
|
showEmpty: rawCardInfo['show_empty'] ?? true,
|
||||||
? rawCard["card"]['columns'] ?? 4
|
stateFilter: rawCardInfo['state_filter'] ?? [],
|
||||||
: rawCard['columns'] ?? 4,
|
states: rawCardInfo['states'],
|
||||||
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ??
|
conditions: rawCard['conditions'] ?? [],
|
||||||
true : rawCard['show_name'] ?? true,
|
content: rawCardInfo['content'],
|
||||||
showState: isThereCardOptionsInside
|
min: rawCardInfo['min'] ?? 0,
|
||||||
? rawCard["card"]['show_state'] ?? true
|
max: rawCardInfo['max'] ?? 100,
|
||||||
: rawCard['show_state'] ?? true,
|
unit: rawCardInfo['unit'],
|
||||||
showEmpty: rawCard['show_empty'] ?? true,
|
severity: rawCardInfo['severity']
|
||||||
stateFilter: rawCard['state_filter'] ?? [],
|
|
||||||
states: rawCard['states'],
|
|
||||||
content: rawCard['content']
|
|
||||||
);
|
);
|
||||||
if (rawCard["cards"] != null) {
|
if (rawCardInfo["cards"] != null) {
|
||||||
card.childCards = _createLovelaceCards(rawCard["cards"]);
|
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||||
}
|
}
|
||||||
rawCard["entities"]?.forEach((rawEntity) {
|
rawCardInfo["entities"]?.forEach((rawEntity) {
|
||||||
if (rawEntity is String) {
|
if (rawEntity is String) {
|
||||||
if (entities.isExist(rawEntity)) {
|
if (entities.isExist(rawEntity)) {
|
||||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||||
|
} else {
|
||||||
|
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (entities.isExist(rawEntity["entity"])) {
|
if (rawEntity["type"] == "divider") {
|
||||||
|
card.entities.add(EntityWrapper(entity: Entity.divider()));
|
||||||
|
} else if (rawEntity["type"] == "section") {
|
||||||
|
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
||||||
|
} else if (rawEntity["type"] == "call-service") {
|
||||||
|
Map uiActionData = {
|
||||||
|
"tap_action": {
|
||||||
|
"action": EntityUIAction.callService,
|
||||||
|
"service": rawEntity["service"],
|
||||||
|
"service_data": rawEntity["service_data"]
|
||||||
|
},
|
||||||
|
"hold_action": EntityUIAction.none
|
||||||
|
};
|
||||||
|
card.entities.add(EntityWrapper(
|
||||||
|
entity: Entity.callService(
|
||||||
|
icon: rawEntity["icon"],
|
||||||
|
name: rawEntity["name"],
|
||||||
|
service: rawEntity["service"],
|
||||||
|
actionName: rawEntity["action_name"]
|
||||||
|
),
|
||||||
|
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (rawEntity["type"] == "weblink") {
|
||||||
|
Map uiActionData = {
|
||||||
|
"tap_action": {
|
||||||
|
"action": EntityUIAction.navigate,
|
||||||
|
"service": rawEntity["url"]
|
||||||
|
},
|
||||||
|
"hold_action": EntityUIAction.none
|
||||||
|
};
|
||||||
|
card.entities.add(EntityWrapper(
|
||||||
|
entity: Entity.weblink(
|
||||||
|
icon: rawEntity["icon"],
|
||||||
|
name: rawEntity["name"],
|
||||||
|
url: rawEntity["url"]
|
||||||
|
),
|
||||||
|
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (entities.isExist(rawEntity["entity"])) {
|
||||||
Entity e = entities.get(rawEntity["entity"]);
|
Entity e = entities.get(rawEntity["entity"]);
|
||||||
card.entities.add(
|
card.entities.add(
|
||||||
EntityWrapper(
|
EntityWrapper(
|
||||||
@ -479,20 +272,24 @@ class HomeAssistant {
|
|||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (rawCard["entity"] != null) {
|
if (rawCardInfo["entity"] != null) {
|
||||||
var en = rawCard["entity"];
|
var en = rawCardInfo["entity"];
|
||||||
if (en is String) {
|
if (en is String) {
|
||||||
if (entities.isExist(en)) {
|
if (entities.isExist(en)) {
|
||||||
Entity e = entities.get(en);
|
Entity e = entities.get(en);
|
||||||
card.linkedEntityWrapper = EntityWrapper(
|
card.linkedEntityWrapper = EntityWrapper(
|
||||||
entity: e,
|
entity: e,
|
||||||
icon: rawCard["icon"],
|
icon: rawCardInfo["icon"],
|
||||||
displayName: rawCard["name"],
|
displayName: rawCardInfo["name"],
|
||||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (entities.isExist(en["entity"])) {
|
if (entities.isExist(en["entity"])) {
|
||||||
@ -503,6 +300,8 @@ class HomeAssistant {
|
|||||||
displayName: en["name"],
|
displayName: en["name"],
|
||||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -514,18 +313,9 @@ class HomeAssistant {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseEntities(response) async {
|
|
||||||
if (response["success"] == false) {
|
|
||||||
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entities.parse(response["result"]);
|
|
||||||
_statesCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _createUI() {
|
void _createUI() {
|
||||||
ui = HomeAssistantUI();
|
ui = HomeAssistantUI();
|
||||||
if ((_useLovelace) && (_rawLovelaceData != null)) {
|
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
||||||
Logger.d("Creating Lovelace UI");
|
Logger.d("Creating Lovelace UI");
|
||||||
_parseLovelace();
|
_parseLovelace();
|
||||||
} else {
|
} else {
|
||||||
@ -559,31 +349,12 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildViews(BuildContext context, bool lovelace) {
|
Widget buildViews(BuildContext context, TabController tabController) {
|
||||||
return ui.build(context);
|
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 {
|
class SendMessageQueue {
|
||||||
int _messageTimeout;
|
int _messageTimeout;
|
||||||
List<HAMessage> _queue = [];
|
List<HAMessage> _queue = [];
|
||||||
@ -622,4 +393,4 @@ class HAMessage {
|
|||||||
bool isExpired() {
|
bool isExpired() {
|
||||||
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
702
lib/main.dart
@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:math';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -8,39 +8,55 @@ import 'package:web_socket_channel/io.dart';
|
|||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:date_format/date_format.dart';
|
import 'package:date_format/date_format.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:charts_flutter/flutter.dart' as charts;
|
import 'package:charts_flutter/flutter.dart' as charts;
|
||||||
import 'package:progress_indicators/progress_indicators.dart';
|
import 'package:progress_indicators/progress_indicators.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.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';
|
import 'utils/logger.dart';
|
||||||
part 'entity_class/entity.class.dart';
|
|
||||||
part 'entity_class/entity_wrapper.class.dart';
|
part 'const.dart';
|
||||||
part 'entity_class/switch_entity.class.dart';
|
part 'utils/launcher.dart';
|
||||||
part 'entity_class/button_entity.class.dart';
|
part 'entities/entity.class.dart';
|
||||||
part 'entity_class/text_entity.class.dart';
|
part 'entities/entity_wrapper.class.dart';
|
||||||
part 'entity_class/climate_entity.class.dart';
|
part 'entities/timer/timer_entity.class.dart';
|
||||||
part 'entity_class/cover_entity.class.dart';
|
part 'entities/switch/switch_entity.class.dart';
|
||||||
part 'entity_class/date_time_entity.class.dart';
|
part 'entities/button/button_entity.class.dart';
|
||||||
part 'entity_class/light_entity.class.dart';
|
part 'entities/text/text_entity.class.dart';
|
||||||
part 'entity_class/select_entity.class.dart';
|
part 'entities/climate/climate_entity.class.dart';
|
||||||
part 'entity_class/other_entity.class.dart';
|
part 'entities/cover/cover_entity.class.dart';
|
||||||
part 'entity_class/slider_entity.dart';
|
part 'entities/date_time/date_time_entity.class.dart';
|
||||||
part 'entity_class/media_player_entity.class.dart';
|
part 'entities/light/light_entity.class.dart';
|
||||||
part 'entity_class/lock_entity.class.dart';
|
part 'entities/select/select_entity.class.dart';
|
||||||
part 'entity_class/group_entity.class.dart';
|
part 'entities/sun/sun_entity.class.dart';
|
||||||
part 'entity_class/fan_entity.class.dart';
|
part 'entities/sensor/sensor_entity.class.dart';
|
||||||
part 'entity_class/automation_entity.dart';
|
part 'entities/slider/slider_entity.dart';
|
||||||
part 'entity_class/camera_entity.class.dart';
|
part 'entities/media_player/media_player_entity.class.dart';
|
||||||
part 'entity_class/alarm_control_panel.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/common/badge.dart';
|
||||||
part 'entity_widgets/model_widgets.dart';
|
part 'entity_widgets/model_widgets.dart';
|
||||||
part 'entity_widgets/default_entity_container.dart';
|
part 'entity_widgets/default_entity_container.dart';
|
||||||
part 'entity_widgets/glance_entity_container.dart';
|
part 'entity_widgets/missed_entity.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/common/entity_attributes_list.dart';
|
||||||
part 'entity_widgets/entity_icon.dart';
|
part 'entity_widgets/entity_icon.dart';
|
||||||
part 'entity_widgets/entity_name.dart';
|
part 'entity_widgets/entity_name.dart';
|
||||||
@ -50,6 +66,7 @@ part 'entity_widgets/common/mode_selector.dart';
|
|||||||
part 'entity_widgets/common/universal_slider.dart';
|
part 'entity_widgets/common/universal_slider.dart';
|
||||||
part 'entity_widgets/common/flat_service_button.dart';
|
part 'entity_widgets/common/flat_service_button.dart';
|
||||||
part 'entity_widgets/common/light_color_picker.dart';
|
part 'entity_widgets/common/light_color_picker.dart';
|
||||||
|
part 'entity_widgets/common/camera_stream_view.dart';
|
||||||
part 'entity_widgets/entity_colors.class.dart';
|
part 'entity_widgets/entity_colors.class.dart';
|
||||||
part 'entity_widgets/entity_page_container.dart';
|
part 'entity_widgets/entity_page_container.dart';
|
||||||
part 'entity_widgets/history_chart/entity_history.dart';
|
part 'entity_widgets/history_chart/entity_history.dart';
|
||||||
@ -58,47 +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/combined_history_chart.dart';
|
||||||
part 'entity_widgets/history_chart/history_control_widget.dart';
|
part 'entity_widgets/history_chart/history_control_widget.dart';
|
||||||
part 'entity_widgets/history_chart/entity_history_moment.dart';
|
part 'entity_widgets/history_chart/entity_history_moment.dart';
|
||||||
part 'entity_widgets/state/switch_state.dart';
|
part 'entities/switch/widget/switch_state.dart';
|
||||||
part 'entity_widgets/controls/slider_controls.dart';
|
part 'entities/slider/widgets/slider_controls.dart';
|
||||||
part 'entity_widgets/state/text_input_state.dart';
|
part 'entities/text/widgets/text_input_state.dart';
|
||||||
part 'entity_widgets/state/select_state.dart';
|
part 'entities/select/widgets/select_state.dart';
|
||||||
part 'entity_widgets/state/simple_state.dart';
|
part 'entity_widgets/common/simple_state.dart';
|
||||||
part 'entity_widgets/state/climate_state.dart';
|
part 'entities/timer/widgets/timer_state.dart';
|
||||||
part 'entity_widgets/state/cover_state.dart';
|
part 'entities/climate/widgets/climate_state.widget.dart';
|
||||||
part 'entity_widgets/state/date_time_state.dart';
|
part 'entities/cover/widgets/cover_state.dart';
|
||||||
part 'entity_widgets/state/lock_state.dart';
|
part 'entities/date_time/widgets/date_time_state.dart';
|
||||||
part 'entity_widgets/controls/climate_controls.dart';
|
part 'entities/lock/widgets/lock_state.dart';
|
||||||
part 'entity_widgets/controls/cover_controls.dart';
|
part 'entities/climate/widgets/climate_controls.dart';
|
||||||
part 'entity_widgets/controls/light_controls.dart';
|
part 'entities/climate/widgets/temperature_control_widget.dart';
|
||||||
part 'entity_widgets/controls/media_player_widgets.dart';
|
part 'entities/cover/widgets/cover_controls.widget.dart';
|
||||||
part 'entity_widgets/controls/fan_controls.dart';
|
part 'entities/light/widgets/light_controls.dart';
|
||||||
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
|
part 'entities/media_player/widgets/media_player_widgets.dart';
|
||||||
part 'entity_widgets/controls/camera_controls.dart';
|
part 'entities/fan/widgets/fan_controls.dart';
|
||||||
part 'settings.page.dart';
|
part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.dart';
|
||||||
part 'configuration.page.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 'home_assistant.class.dart';
|
||||||
part 'log.page.dart';
|
part 'pages/log.page.dart';
|
||||||
part 'entity.page.dart';
|
part 'pages/entity.page.dart';
|
||||||
part 'utils.class.dart';
|
|
||||||
part 'mdi.class.dart';
|
part 'mdi.class.dart';
|
||||||
part 'entity_collection.class.dart';
|
part 'entity_collection.class.dart';
|
||||||
part 'ui_class/ui.dart';
|
part 'managers/auth_manager.class.dart';
|
||||||
part 'ui_class/view.class.dart';
|
part 'managers/location_manager.class.dart';
|
||||||
part 'ui_class/card.class.dart';
|
part 'managers/mobile_app_integration_manager.class.dart';
|
||||||
part 'ui_class/sizes_class.dart';
|
part 'managers/connection_manager.class.dart';
|
||||||
part 'ui_widgets/view.dart';
|
part 'managers/device_info_manager.class.dart';
|
||||||
part 'ui_widgets/card_widget.dart';
|
part 'managers/startup_user_messages_manager.class.dart';
|
||||||
part 'ui_widgets/card_header_widget.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();
|
EventBus eventBus = new EventBus();
|
||||||
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||||
|
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersion = "0.4.0";
|
const appVersion = "0.6.7";
|
||||||
const appBuild = "91";
|
|
||||||
|
|
||||||
String homeAssistantWebHost;
|
void main() async {
|
||||||
|
|
||||||
void main() {
|
|
||||||
FlutterError.onError = (errorDetails) {
|
FlutterError.onError = (errorDetails) {
|
||||||
Logger.e( "${errorDetails.exception}");
|
Logger.e( "${errorDetails.exception}");
|
||||||
if (Logger.isInDebugMode) {
|
if (Logger.isInDebugMode) {
|
||||||
@ -107,7 +141,11 @@ void main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
runZoned(() {
|
runZoned(() {
|
||||||
|
//AndroidAlarmManager.initialize().then((_) {
|
||||||
runApp(new HAClientApp());
|
runApp(new HAClientApp());
|
||||||
|
// print("Running MAIN isolate ${Isolate.current.hashCode}");
|
||||||
|
//});
|
||||||
|
|
||||||
}, onError: (error, stack) {
|
}, onError: (error, stack) {
|
||||||
Logger.e("$error");
|
Logger.e("$error");
|
||||||
Logger.e("$stack");
|
Logger.e("$stack");
|
||||||
@ -118,6 +156,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class HAClientApp extends StatelessWidget {
|
class HAClientApp extends StatelessWidget {
|
||||||
|
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -130,541 +169,38 @@ class HAClientApp extends StatelessWidget {
|
|||||||
routes: {
|
routes: {
|
||||||
"/": (context) => MainPage(title: 'HA Client'),
|
"/": (context) => MainPage(title: 'HA Client'),
|
||||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||||
"/configuration": (context) => ConfigurationPage(title: "Configuration"),
|
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
||||||
"/log-view": (context) => LogViewPage(title: "Log")
|
"/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(
|
||||||
|
|
||||||
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 {
|
|
||||||
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;
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}).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 {
|
|
||||||
menuItems.addAll([
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(Icons.settings),
|
|
||||||
title: Text("Configuration"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.of(context).pushNamed('/configuration');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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.createIconDataFromIconName("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,
|
|
||||||
applicationLegalese: "build $appBuild",
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
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.createIconDataFromIconName(
|
|
||||||
"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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
)
|
|
||||||
],
|
|
||||||
leading: IconButton(
|
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: () {
|
onPressed: () {
|
||||||
_scaffoldKey.currentState.openDrawer();
|
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
|
||||||
setState(() {
|
|
||||||
_accountMenuExpanded = false;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
|
||||||
bottom: empty ? null : TabBar(
|
|
||||||
tabs: buildUIViewTabs(),
|
|
||||||
isScrollable: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
];
|
|
||||||
},
|
|
||||||
body: empty ?
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
|
|
||||||
size: 100.0,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
:
|
|
||||||
_homeAssistant.buildViews(context, _useLovelaceUI),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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: DefaultTabController(
|
|
||||||
length: _homeAssistant.ui?.views?.length ?? 0,
|
|
||||||
child: _buildScaffoldBody(false),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|