Compare commits
	
		
			26 Commits
		
	
	
		
			0.5.0
			...
			0.6.0-alph
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 885a516676 | ||
|  | 921b0e09b0 | ||
|  | 277c67fc6f | ||
|  | 2a01ff8a03 | ||
|  | b246b7bc1d | ||
|  | e1868b9a14 | ||
|  | 125f3ac16c | ||
|  | be502b5668 | ||
|  | 6f33fdca9f | ||
|  | 4e96b9adbb | ||
|  | b9581d3762 | ||
|  | 7c010359c3 | ||
|  | 4a75243994 | ||
|  | d29d7e5b3b | ||
|  | 5ebd25e0d1 | ||
|  | b7d5a53e86 | ||
|  | 20d3498bfd | ||
|  | 67d7bb45f5 | ||
|  | 6a03105d01 | ||
|  | 5ae580ecf1 | ||
|  | 0efef33e53 | ||
|  | ccb88884a7 | ||
|  | d70ba0a55a | ||
|  | 5140840d3a | ||
|  | 14759fd3c9 | ||
|  | fed35be517 | 
| @@ -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" | ||||||
|  | } | ||||||
| @@ -15,7 +15,8 @@ | |||||||
|     <application |     <application | ||||||
|         android:name="io.flutter.app.FlutterApplication" |         android:name="io.flutter.app.FlutterApplication" | ||||||
|         android:label="HA Client" |         android:label="HA Client" | ||||||
|         android:icon="@mipmap/ic_launcher"> |         android:icon="@mipmap/ic_launcher" | ||||||
|  |         android:usesCleartextTraffic="true"> | ||||||
|         <activity |         <activity | ||||||
|             android:name=".MainActivity" |             android:name=".MainActivity" | ||||||
|             android:launchMode="singleTop" |             android:launchMode="singleTop" | ||||||
| @@ -30,10 +31,14 @@ | |||||||
|             <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> | ||||||
|     </application> |     </application> | ||||||
| </manifest> | </manifest> | ||||||
|   | |||||||
| 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' | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx2g | |||||||
| org.gradle.daemon=true | org.gradle.daemon=true | ||||||
| org.gradle.caching=true | org.gradle.caching=true | ||||||
| android.useAndroidX=true | android.useAndroidX=true | ||||||
| android.enableJetifier=false | 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 | ||||||
|   | |||||||
| Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										44
									
								
								lib/auth_manager.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | |||||||
|  | part of 'main.dart'; | ||||||
|  |  | ||||||
|  | class AuthManager { | ||||||
|  |  | ||||||
|  |   static final AuthManager _instance = AuthManager._internal(); | ||||||
|  |  | ||||||
|  |   factory AuthManager() { | ||||||
|  |     return _instance; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   AuthManager._internal(); | ||||||
|  |  | ||||||
|  |   Future getTempToken({String httpWebHost, 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..."); | ||||||
|  |         Connection().sendHTTPPost( | ||||||
|  |             host: httpWebHost, | ||||||
|  |             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("Gottemp token"); | ||||||
|  |           String tempToken = json.decode(response)['access_token']; | ||||||
|  |           Logger.d("Closing webview..."); | ||||||
|  |           flutterWebviewPlugin.close(); | ||||||
|  |           completer.complete(tempToken); | ||||||
|  |         }).catchError((e) { | ||||||
|  |           flutterWebviewPlugin.close(); | ||||||
|  |           completer.completeError({"errorCode": 61, "errorMessage": "Error getting temp token"}); | ||||||
|  |           Logger.e("Error getting temp token: ${e.toString()}"); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     Logger.d("Launching OAuth..."); | ||||||
|  |     eventBus.fire(StartAuthEvent(oauthUrl)); | ||||||
|  |     return completer.future; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										343
									
								
								lib/connection.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,343 @@ | |||||||
|  | part of 'main.dart'; | ||||||
|  |  | ||||||
|  | class Connection { | ||||||
|  |  | ||||||
|  |   static final Connection _instance = Connection._internal(); | ||||||
|  |  | ||||||
|  |   factory Connection() { | ||||||
|  |     return _instance; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Connection._internal(); | ||||||
|  |  | ||||||
|  |   String displayHostname; | ||||||
|  |   String _webSocketAPIEndpoint; | ||||||
|  |   String httpWebHost; | ||||||
|  |   String _token; | ||||||
|  |   String _tempToken; | ||||||
|  |   String oauthUrl; | ||||||
|  |   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(onStateChange) async { | ||||||
|  |     Completer completer = Completer(); | ||||||
|  |     onStateChangeCallback = onStateChange; | ||||||
|  |     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|  |     String domain = prefs.getString('hassio-domain'); | ||||||
|  |     String port = prefs.getString('hassio-port'); | ||||||
|  |     displayHostname = "$domain:$port"; | ||||||
|  |     _webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; | ||||||
|  |     httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; | ||||||
|  |     //_token = prefs.getString('hassio-token'); | ||||||
|  |     final storage = new FlutterSecureStorage(); | ||||||
|  |     try { | ||||||
|  |       _token = await storage.read(key: "hacl_llt"); | ||||||
|  |     } catch (e) { | ||||||
|  |       Logger.e("Cannt read secure storage. Need to relogin."); | ||||||
|  |       _token = null; | ||||||
|  |       await storage.delete(key: "hacl_llt"); | ||||||
|  |     } | ||||||
|  |     if ((domain == null) || (port == null) || | ||||||
|  |         (domain.length == 0) || (port.length == 0)) { | ||||||
|  |       completer.completeError({"errorCode": 5, "errorMessage": "Check connection settings"}); | ||||||
|  |     } else { | ||||||
|  |       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')}"; | ||||||
|  |       if (_token == null) { | ||||||
|  |         await AuthManager().getTempToken( | ||||||
|  |             httpWebHost: httpWebHost, | ||||||
|  |             oauthUrl: oauthUrl | ||||||
|  |         ).then((token) { | ||||||
|  |           Logger.d("Token from AuthManager recived"); | ||||||
|  |           _tempToken = token; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       _connect().timeout(connectTimeout, onTimeout: () { | ||||||
|  |         _disconnect().then((_) { | ||||||
|  |           completer.completeError( | ||||||
|  |               {"errorCode": 1, "errorMessage": "Connection timeout"}); | ||||||
|  |         }); | ||||||
|  |       }).then((_) => completer.complete()).catchError((e) { | ||||||
|  |         completer.completeError(e); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     return completer.future; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Completer connecting; | ||||||
|  |  | ||||||
|  |   Future _connect() async { | ||||||
|  |     if (connecting != null && !connecting.isCompleted) { | ||||||
|  |       Logger.w(""); | ||||||
|  |       return connecting.future; | ||||||
|  |     } | ||||||
|  |     connecting = Completer(); | ||||||
|  |     await _disconnect(); | ||||||
|  |     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((_) => 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 (!connecting.isCompleted) connecting.complete(sendSocketMessage( | ||||||
|  |               type: "subscribe_events", | ||||||
|  |               additionalData: {"event_type": "state_changed"}, | ||||||
|  |             )); | ||||||
|  |           } else if (data["type"] == "auth_invalid") { | ||||||
|  |             Logger.d("[Received] <== ${data.toString()}"); | ||||||
|  |             _messageResolver["auth"]?.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"}); | ||||||
|  |             _messageResolver.remove("auth"); | ||||||
|  |             logout().then((_) { | ||||||
|  |               if (!connecting.isCompleted) connecting.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"}); | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             _handleMessage(data); | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         cancelOnError: true, | ||||||
|  |         onDone: () => _handleSocketClose(connecting), | ||||||
|  |         onError: (e) => _handleSocketError(e, connecting) | ||||||
|  |     ); | ||||||
|  |     return connecting.future; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future _disconnect() async { | ||||||
|  |     Logger.d( "Socket disconnecting..."); | ||||||
|  |     await _socketSubscription?.cancel(); | ||||||
|  |     await _socket?.sink?.close()?.timeout(Duration(seconds: 4), | ||||||
|  |         onTimeout: () => Logger.d( "Socket sink close timeout") | ||||||
|  |     ); | ||||||
|  |     Logger.d( "..Disconnected"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   _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({"errorMessage": "${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) { | ||||||
|  |     isConnected = false; | ||||||
|  |     Logger.d("Socket disconnected."); | ||||||
|  |     if (!connectionCompleter.isCompleted) { | ||||||
|  |       connectionCompleter.completeError({"errorCode": 82, "errorMessage": "Disconnected"}); | ||||||
|  |     } else { | ||||||
|  |       _disconnect().then((_) { | ||||||
|  |         Timer(Duration(seconds: 5), () { | ||||||
|  |           Logger.d("Trying to reconnect..."); | ||||||
|  |           _connect(); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _handleSocketError(e, Completer connectionCompleter) { | ||||||
|  |     isConnected = false; | ||||||
|  |     Logger.e("Socket stream Error: $e"); | ||||||
|  |     if (!connectionCompleter.isCompleted) { | ||||||
|  |       connectionCompleter.completeError({"errorCode": 81, "errorMessage": "Unable to connect to Home Assistant"}); | ||||||
|  |     } else { | ||||||
|  |       _disconnect().then((_) { | ||||||
|  |         Timer(Duration(seconds: 5), () { | ||||||
|  |           Logger.d("Trying to reconnect..."); | ||||||
|  |           _connect(); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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((_) { | ||||||
|  |           completer.complete(); | ||||||
|  |         }).catchError((e) { | ||||||
|  |           Logger.e("Can't get long-lived token: $e"); | ||||||
|  |           throw e; | ||||||
|  |         }); | ||||||
|  |       }).catchError((e) => completer.completeError(e)); | ||||||
|  |     } else { | ||||||
|  |       completer.completeError({"errorCode": 63, "errorMessage": "General login error"}); | ||||||
|  |     } | ||||||
|  |     return completer.future; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future logout() { | ||||||
|  |     _token = null; | ||||||
|  |     _tempToken = null; | ||||||
|  |     final storage = new FlutterSecureStorage(); | ||||||
|  |     return storage.delete(key: "hacl_llt"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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((_) { | ||||||
|  |         completer.complete(); | ||||||
|  |       }).catchError((e) { | ||||||
|  |         throw e; | ||||||
|  |       }); | ||||||
|  |     }).catchError((e) { | ||||||
|  |       logout(); | ||||||
|  |       completer.completeError({"errorCode": 63, "errorMessage": "Authentication error: $e"}); | ||||||
|  |     }); | ||||||
|  |     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); | ||||||
|  |     Logger.d("[Sending] ==> $rawMessage"); | ||||||
|  |     if (!isConnected) { | ||||||
|  |       _connect().timeout(connectTimeout, onTimeout: (){ | ||||||
|  |         _completer.completeError({"errorCode": 8, "errorMessage": "No connection to Home Assistant"}); | ||||||
|  |       }).then((_) { | ||||||
|  |         _socket.sink.add(rawMessage); | ||||||
|  |       }).catchError((e) { | ||||||
|  |         _completer.completeError(e); | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       _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] ==> $url"); | ||||||
|  |     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] <== ${history.first.length} history recors"); | ||||||
|  |       return history; | ||||||
|  |     } else { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future sendHTTPPost({String host, String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true, String authToken}) async { | ||||||
|  |     Completer completer = Completer(); | ||||||
|  |     String url = "$host$endPoint"; | ||||||
|  |     Logger.d("[Sending] ==> $url"); | ||||||
|  |     Map<String, String> headers = {}; | ||||||
|  |     if (contentType != null) { | ||||||
|  |       headers["Content-Type"] = contentType; | ||||||
|  |     } | ||||||
|  |     if (includeAuthHeader) { | ||||||
|  |       headers["authorization"] = "Bearer $authToken"; | ||||||
|  |     } | ||||||
|  |     http.post( | ||||||
|  |         url, | ||||||
|  |         headers: headers, | ||||||
|  |         body: data | ||||||
|  |     ).then((response) { | ||||||
|  |       Logger.d("[Received] <== ${response.statusCode}, ${response.body}"); | ||||||
|  |       if (response.statusCode == 200) { | ||||||
|  |         completer.complete(response.body); | ||||||
|  |       } else { | ||||||
|  |         completer.completeError({"code": response.statusCode, "message": "${response.body}"}); | ||||||
|  |       } | ||||||
|  |     }).catchError((e) { | ||||||
|  |       completer.completeError(e); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return completer.future; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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,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) { | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ class CameraEntity extends Entity { | |||||||
|  |  | ||||||
|   static const SUPPORT_ON_OFF = 1; |   static const SUPPORT_ON_OFF = 1; | ||||||
|  |  | ||||||
|   CameraEntity(Map rawData) : super(rawData); |   CameraEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|   bool get supportOnOff => ((supportedFeatures & |   bool get supportOnOff => ((supportedFeatures & | ||||||
|   CameraEntity.SUPPORT_ON_OFF) == |   CameraEntity.SUPPORT_ON_OFF) == | ||||||
|   | |||||||
| @@ -23,6 +23,8 @@ class ClimateEntity extends Entity { | |||||||
|   static const SUPPORT_AUX_HEAT = 2048; |   static const SUPPORT_AUX_HEAT = 2048; | ||||||
|   static const SUPPORT_ON_OFF = 4096; |   static const SUPPORT_ON_OFF = 4096; | ||||||
|  |  | ||||||
|  |   ClimateEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|   bool get supportTargetTemperature => ((supportedFeatures & |   bool get supportTargetTemperature => ((supportedFeatures & | ||||||
|   ClimateEntity.SUPPORT_TARGET_TEMPERATURE) == |   ClimateEntity.SUPPORT_TARGET_TEMPERATURE) == | ||||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE); |       ClimateEntity.SUPPORT_TARGET_TEMPERATURE); | ||||||
| @@ -88,11 +90,9 @@ class ClimateEntity extends Entity { | |||||||
|   bool get isOff => state == EntityState.off; |   bool get isOff => state == EntityState.off; | ||||||
|   bool get auxHeat => attributes['aux_heat'] == "on"; |   bool get auxHeat => attributes['aux_heat'] == "on"; | ||||||
|  |  | ||||||
|   ClimateEntity(Map rawData) : super(rawData); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void update(Map rawData) { |   void update(Map rawData, String webHost) { | ||||||
|     super.update(rawData); |     super.update(rawData, webHost); | ||||||
|     if (supportTargetTemperature) { |     if (supportTargetTemperature) { | ||||||
|       historyConfig.numericAttributesToShow.add("temperature"); |       historyConfig.numericAttributesToShow.add("temperature"); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -11,6 +11,8 @@ 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; | ||||||
|  |  | ||||||
|  |   CoverEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|   bool get supportOpen => ((supportedFeatures & |   bool get supportOpen => ((supportedFeatures & | ||||||
|   CoverEntity.SUPPORT_OPEN) == |   CoverEntity.SUPPORT_OPEN) == | ||||||
|       CoverEntity.SUPPORT_OPEN); |       CoverEntity.SUPPORT_OPEN); | ||||||
| @@ -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,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(); | ||||||
|   | |||||||
| @@ -73,6 +73,7 @@ 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; | ||||||
| @@ -94,7 +95,6 @@ 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 => _getEntityPictureUrl(); |  | ||||||
|   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(); | ||||||
| @@ -102,21 +102,21 @@ class Entity { | |||||||
|   double get doubleState => double.tryParse(state) ?? 0.0; |   double get doubleState => double.tryParse(state) ?? 0.0; | ||||||
|   int get supportedFeatures => attributes["supported_features"] ?? 0; |   int get supportedFeatures => attributes["supported_features"] ?? 0; | ||||||
|  |  | ||||||
|   String _getEntityPictureUrl() { |   String _getEntityPictureUrl(String webHost) { | ||||||
|     String result = attributes["entity_picture"]; |     String result = attributes["entity_picture"]; | ||||||
|     if (result == null) return result; |     if (result == null) return result; | ||||||
|     if (!result.startsWith("http")) { |     if (!result.startsWith("http")) { | ||||||
|       if (result.startsWith("/")) { |       if (result.startsWith("/")) { | ||||||
|         result = "$homeAssistantWebHost$result"; |         result = "$webHost$result"; | ||||||
|       } else { |       } else { | ||||||
|         result = "$homeAssistantWebHost/$result"; |         result = "$webHost/$result"; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Entity(Map rawData) { |   Entity(Map rawData, String webHost) { | ||||||
|     update(rawData); |     update(rawData, webHost); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Entity.missed(String entityId) { |   Entity.missed(String entityId) { | ||||||
| @@ -148,7 +148,7 @@ class Entity { | |||||||
|     attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; |     attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void update(Map rawData) { |   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"]; | ||||||
| @@ -156,6 +156,7 @@ class Entity { | |||||||
|     state = rawData["state"]; |     state = rawData["state"]; | ||||||
|     displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state; |     displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state; | ||||||
|     _lastUpdated = DateTime.tryParse(rawData["last_updated"]); |     _lastUpdated = DateTime.tryParse(rawData["last_updated"]); | ||||||
|  |     entityPicture = _getEntityPictureUrl(webHost); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   double _getDoubleAttributeValue(String attributeName) { |   double _getDoubleAttributeValue(String attributeName) { | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ 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 => ((supportedFeatures & |   bool get supportSetSpeed => ((supportedFeatures & | ||||||
|   FanEntity.SUPPORT_SET_SPEED) == |   FanEntity.SUPPORT_SET_SPEED) == | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ class LightEntity extends Entity { | |||||||
|   bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 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"]; | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| part of '../main.dart'; | part of '../main.dart'; | ||||||
|  |  | ||||||
| class LockEntity extends Entity { | class LockEntity extends Entity { | ||||||
|   LockEntity(Map rawData) : super(rawData); |   LockEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|   bool get isLocked => state == "locked"; |   bool get isLocked => state == "locked"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ 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 => ((supportedFeatures & |   bool get supportPause => ((supportedFeatures & | ||||||
|   MediaPlayerEntity.SUPPORT_PAUSE) == |   MediaPlayerEntity.SUPPORT_PAUSE) == | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| part of '../main.dart'; | part of '../main.dart'; | ||||||
|  |  | ||||||
| class SunEntity extends Entity { | class SunEntity extends Entity { | ||||||
|   SunEntity(Map rawData) : super(rawData); |   SunEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
| } | } | ||||||
|  |  | ||||||
| class SensorEntity extends Entity { | class SensorEntity extends Entity { | ||||||
| @@ -12,6 +12,6 @@ class SensorEntity extends Entity { | |||||||
|       numericState: true |       numericState: true | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   SensorEntity(Map rawData) : super(rawData); |   SensorEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -5,7 +5,7 @@ class SelectEntity extends Entity { | |||||||
|       ? (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,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,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,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,30 +1,39 @@ | |||||||
| part of '../main.dart'; | part of '../main.dart'; | ||||||
|  |  | ||||||
| class TimerEntity extends Entity { | class TimerEntity extends Entity { | ||||||
|   TimerEntity(Map rawData) : super(rawData); |   TimerEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|   Duration duration; |   Duration duration; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void update(Map rawData) { |   void update(Map rawData, String webHost) { | ||||||
|     super.update(rawData); |     super.update(rawData, webHost); | ||||||
|     String durationSource = "${attributes["duration"]}"; |     String durationSource = "${attributes["duration"]}"; | ||||||
|     List<String> durationList = durationSource.split(":"); |     if (durationSource != null && durationSource.isNotEmpty) { | ||||||
|     if (durationList.length == 1) { |       try { | ||||||
|       duration = Duration(seconds: int.tryParse(durationList[0] ?? 0)); |         List<String> durationList = durationSource.split(":"); | ||||||
|     } else if (durationList.length == 2) { |         if (durationList.length == 1) { | ||||||
|       duration = Duration( |           duration = Duration(seconds: int.tryParse(durationList[0] ?? 0)); | ||||||
|           hours: int.tryParse(durationList[0]) ?? 0, |         } else if (durationList.length == 2) { | ||||||
|           minutes: int.tryParse(durationList[1]) ?? 0 |           duration = Duration( | ||||||
|       ); |               hours: int.tryParse(durationList[0]) ?? 0, | ||||||
|     } else if (durationList.length == 3) { |               minutes: int.tryParse(durationList[1]) ?? 0 | ||||||
|       duration = Duration( |           ); | ||||||
|           hours: int.tryParse(durationList[0]) ?? 0, |         } else if (durationList.length == 3) { | ||||||
|           minutes: int.tryParse(durationList[1]) ?? 0, |           duration = Duration( | ||||||
|         seconds: int.tryParse(durationList[2]) ?? 0 |               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 { |     } else { | ||||||
|       Logger.e("Cann't parse $entityId duration: $durationSource"); |  | ||||||
|       duration = Duration(seconds: 0); |       duration = Duration(seconds: 0); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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,70 +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": { |       case "timer": { | ||||||
|         return TimerEntity(rawEntityData); |         return TimerEntity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|       default: { |       default: { | ||||||
|         return Entity(rawEntityData); |         return Entity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -121,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) { | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ class _CameraStreamViewState extends State<CameraStreamView> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   CameraEntity _entity; |   CameraEntity _entity; | ||||||
|  |   String _webHost; | ||||||
|  |  | ||||||
|   http.Client client; |   http.Client client; | ||||||
|   http.StreamedResponse response; |   http.StreamedResponse response; | ||||||
| @@ -28,7 +29,7 @@ class _CameraStreamViewState extends State<CameraStreamView> { | |||||||
|   void _connect() async { |   void _connect() async { | ||||||
|     started = true; |     started = true; | ||||||
|     timeToStop = false; |     timeToStop = false; | ||||||
|     String streamUrl = '$homeAssistantWebHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}'; |     String streamUrl = '$_webHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}'; | ||||||
|     client = new http.Client(); // create a client to make api calls |     client = new http.Client(); // create a client to make api calls | ||||||
|     http.Request request = new http.Request("GET", Uri.parse(streamUrl));  // create get request |     http.Request request = new http.Request("GET", Uri.parse(streamUrl));  // create get request | ||||||
|     Logger.d("[Sending] ==> $streamUrl"); |     Logger.d("[Sending] ==> $streamUrl"); | ||||||
| @@ -130,6 +131,7 @@ class _CameraStreamViewState extends State<CameraStreamView> { | |||||||
|           .of(context) |           .of(context) | ||||||
|           .entityWrapper |           .entityWrapper | ||||||
|           .entity; |           .entity; | ||||||
|  |       _webHost = HomeAssistantModel.of(context).homeAssistant.connection.httpWebHost; | ||||||
|       _connect(); |       _connect(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> { | |||||||
|     } |     } | ||||||
|     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){ |       ha.connection.getHistory(entityId).then((history){ | ||||||
|         if (!_disposed) { |         if (!_disposed) { | ||||||
|           setState(() { |           setState(() { | ||||||
|             _history = history.isNotEmpty ? history[0] : []; |             _history = history.isNotEmpty ? history[0] : []; | ||||||
|   | |||||||
| @@ -1,37 +1,27 @@ | |||||||
| part of 'main.dart'; | part of 'main.dart'; | ||||||
|  |  | ||||||
| class HomeAssistant { | class HomeAssistant { | ||||||
|   String _webSocketAPIEndpoint; |  | ||||||
|   String _password; |   final Connection connection = Connection(); | ||||||
|  |  | ||||||
|   bool _useLovelace = false; |   bool _useLovelace = false; | ||||||
|  |   //bool isSettingsLoaded = false; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   IOWebSocketChannel _hassioChannel; |  | ||||||
|   SendMessageQueue _messageQueue; |  | ||||||
|  |  | ||||||
|   int _currentMessageId = 0; |  | ||||||
|   int _subscriptionMessageId = 0; |  | ||||||
|   Map<int, Completer> _messageResolver = {}; |  | ||||||
|   EntityCollection entities; |   EntityCollection entities; | ||||||
|   HomeAssistantUI ui; |   HomeAssistantUI ui; | ||||||
|   Map _instanceConfig = {}; |   Map _instanceConfig = {}; | ||||||
|   String _userName; |   String _userName; | ||||||
|  |   String hostname; | ||||||
|   HSVColor savedColor; |   HSVColor savedColor; | ||||||
|  |  | ||||||
|   Map _rawLovelaceData; |   Map _rawLovelaceData; | ||||||
|  |  | ||||||
|   List<Panel> panels = []; |   List<Panel> panels = []; | ||||||
|  |  | ||||||
|   Completer _fetchCompleter; |  | ||||||
|   Completer _connectionCompleter; |  | ||||||
|   Timer _connectionTimer; |  | ||||||
|   Timer _fetchTimer; |  | ||||||
|   bool autoReconnect = false; |  | ||||||
|  |  | ||||||
|   StreamSubscription _socketSubscription; |  | ||||||
|  |  | ||||||
|   int messageExpirationTime = 30; //seconds |  | ||||||
|   Duration fetchTimeout = Duration(seconds: 30); |   Duration fetchTimeout = Duration(seconds: 30); | ||||||
|   Duration connectTimeout = Duration(seconds: 15); |  | ||||||
|  |  | ||||||
|   String get locationName { |   String get locationName { | ||||||
|     if (_useLovelace) { |     if (_useLovelace) { | ||||||
| @@ -42,117 +32,39 @@ 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] : ""; | ||||||
|  |   bool get isNoEntities => entities == null || entities.isEmpty; | ||||||
|  |   bool get isNoViews => ui == null || ui.isEmpty; | ||||||
|   //int get viewsCount => entities.views.length ?? 0; |   //int get viewsCount => entities.views.length ?? 0; | ||||||
|  |  | ||||||
|   HomeAssistant() { |   HomeAssistant(); | ||||||
|     entities = EntityCollection(); |  | ||||||
|     _messageQueue = SendMessageQueue(messageExpirationTime); |   Completer _connectCompleter; | ||||||
|  |  | ||||||
|  |   Future init() { | ||||||
|  |     if (_connectCompleter != null && !_connectCompleter.isCompleted) { | ||||||
|  |       Logger.w("Previous connection pending..."); | ||||||
|  |       return _connectCompleter.future; | ||||||
|  |     } | ||||||
|  |     Logger.d("init..."); | ||||||
|  |     _connectCompleter = Completer(); | ||||||
|  |     connection.init(_handleEntityStateChange).then((_) { | ||||||
|  |       SharedPreferences.getInstance().then((prefs) { | ||||||
|  |         if (entities == null) entities = EntityCollection(connection.httpWebHost); | ||||||
|  |         _useLovelace = prefs.getBool('use-lovelace') ?? true; | ||||||
|  |         _connectCompleter.complete(); | ||||||
|  |       }).catchError((e) => _connectCompleter.completeError(e)); | ||||||
|  |     }).catchError((e) => _connectCompleter.completeError(e)); | ||||||
|  |     return _connectCompleter.future; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   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 fetch() { | ||||||
|     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 { |       return _fetchCompleter.future; | ||||||
|       _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; |     _fetchCompleter = Completer(); | ||||||
|   } |  | ||||||
|  |  | ||||||
|   disconnect() async { |  | ||||||
|     if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) { |  | ||||||
|       await _hassioChannel.sink.close().timeout(Duration(seconds: 3), |  | ||||||
|         onTimeout: () => Logger.d( "Socket sink closed") |  | ||||||
|       ); |  | ||||||
|       await _socketSubscription.cancel(); |  | ||||||
|       _hassioChannel = null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future _connection() { |  | ||||||
|     if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) { |  | ||||||
|       Logger.d("Previous connection is not complited"); |  | ||||||
|     } else { |  | ||||||
|       if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { |  | ||||||
|         _connectionCompleter = new Completer(); |  | ||||||
|         autoReconnect = false; |  | ||||||
|         disconnect().then((_){ |  | ||||||
|           Logger.d( "Socket connecting..."); |  | ||||||
|           _connectionTimer = Timer(connectTimeout, () { |  | ||||||
|             Logger.e( "Socket connection timeout"); |  | ||||||
|             _handleSocketError(null); |  | ||||||
|           }); |  | ||||||
|           if (_socketSubscription != null) { |  | ||||||
|             _socketSubscription.cancel(); |  | ||||||
|           } |  | ||||||
|           _hassioChannel = IOWebSocketChannel.connect( |  | ||||||
|               _webSocketAPIEndpoint, pingInterval: Duration(seconds: 30)); |  | ||||||
|           _socketSubscription = _hassioChannel.stream.listen( |  | ||||||
|                   (message) => _handleMessage(message), |  | ||||||
|               cancelOnError: true, |  | ||||||
|               onDone: () => _handleSocketClose(), |  | ||||||
|               onError: (e) => _handleSocketError(e) |  | ||||||
|           ); |  | ||||||
|         }); |  | ||||||
|       } else { |  | ||||||
|         _completeConnecting(null); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return _connectionCompleter.future; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _handleSocketClose() { |  | ||||||
|     Logger.d("Socket disconnected. Automatic reconnect is $autoReconnect"); |  | ||||||
|     if (autoReconnect) { |  | ||||||
|       _reconnect(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _handleSocketError(e) { |  | ||||||
|     Logger.e("Socket stream Error: $e"); |  | ||||||
|     Logger.d("Automatic reconnect is $autoReconnect"); |  | ||||||
|     if (autoReconnect) { |  | ||||||
|       _reconnect(); |  | ||||||
|     } else { |  | ||||||
|       disconnect().then((_) { |  | ||||||
|         _completeConnecting({ |  | ||||||
|           "errorCode": 1, |  | ||||||
|           "errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings." |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _reconnect() { |  | ||||||
|     disconnect().then((_) { |  | ||||||
|       _connection().catchError((e){ |  | ||||||
|         _completeConnecting(e); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _getData() async { |  | ||||||
|     List<Future> futures = []; |     List<Future> futures = []; | ||||||
|     futures.add(_getStates()); |     futures.add(_getStates()); | ||||||
|     if (_useLovelace) { |     if (_useLovelace) { | ||||||
| @@ -162,194 +74,76 @@ class HomeAssistant { | |||||||
|     futures.add(_getServices()); |     futures.add(_getServices()); | ||||||
|     futures.add(_getUserInfo()); |     futures.add(_getUserInfo()); | ||||||
|     futures.add(_getPanels()); |     futures.add(_getPanels()); | ||||||
|     try { |     Future.wait(futures).then((_) { | ||||||
|       await Future.wait(futures); |  | ||||||
|       _createUI(); |       _createUI(); | ||||||
|       _completeFetching(null); |       _fetchCompleter.complete(); | ||||||
|     } catch (error) { |     }).catchError((e) { | ||||||
|       _completeFetching(error); |       _fetchCompleter.completeError(e); | ||||||
|     } |     }); | ||||||
|  |     return _fetchCompleter.future; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _completeFetching(error) { |   Future logout() async { | ||||||
|     _fetchTimer.cancel(); |     Logger.d("Logging out..."); | ||||||
|     _completeConnecting(error); |     await connection.logout().then((_) { | ||||||
|     if (!_fetchCompleter.isCompleted) { |       ui?.clear(); | ||||||
|       if (error != null) { |       entities?.clear(); | ||||||
|         _fetchCompleter.completeError(error); |     }); | ||||||
|       } else { |  | ||||||
|         autoReconnect = true; |  | ||||||
|         Logger.d( "Fetch complete successful"); |  | ||||||
|         _fetchCompleter.complete(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _completeConnecting(error) { |  | ||||||
|     _connectionTimer.cancel(); |  | ||||||
|     if (!_connectionCompleter.isCompleted) { |  | ||||||
|       if (error != null) { |  | ||||||
|         _connectionCompleter.completeError(error); |  | ||||||
|       } else { |  | ||||||
|         _connectionCompleter.complete(); |  | ||||||
|       } |  | ||||||
|     } else if (error != null) { |  | ||||||
|       if (error is Error) { |  | ||||||
|         eventBus.fire(ShowErrorEvent(error.toString(), 12)); |  | ||||||
|       } else { |  | ||||||
|         eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"])); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _handleMessage(String message) { |  | ||||||
|     var data = json.decode(message); |  | ||||||
|     if (data["type"] == "auth_required") { |  | ||||||
|       _sendAuthMessage('{"type": "auth","access_token": "$_password"}'); |  | ||||||
|     } else if (data["type"] == "auth_ok") { |  | ||||||
|       _completeConnecting(null); |  | ||||||
|       _sendSubscribe(); |  | ||||||
|     } else if (data["type"] == "auth_invalid") { |  | ||||||
|       _completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"}); |  | ||||||
|     } else if (data["type"] == "result") { |  | ||||||
|       Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}"); |  | ||||||
|       _messageResolver[data["id"]]?.complete(data); |  | ||||||
|       _messageResolver.remove(data["id"]); |  | ||||||
|     } else if (data["type"] == "event") { |  | ||||||
|       if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) { |  | ||||||
|         Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}"); |  | ||||||
|         _handleEntityStateChange(data["event"]["data"]); |  | ||||||
|       } else if (data["event"] != null) { |  | ||||||
|         Logger.w("Unhandled event type: ${data["event"]["event_type"]}"); |  | ||||||
|       } else { |  | ||||||
|         Logger.e("Event is null: $message"); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       Logger.w("Unknown message type: $message"); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _sendSubscribe() { |  | ||||||
|     _incrementMessageId(); |  | ||||||
|     _subscriptionMessageId = _currentMessageId; |  | ||||||
|     _send('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future _getConfig() async { |   Future _getConfig() async { | ||||||
|     await _sendInitialMessage("get_config").then((data) => _instanceConfig = Map.from(data["result"])); |     await connection.sendSocketMessage(type: "get_config").then((data) { | ||||||
|  |       _instanceConfig = Map.from(data); | ||||||
|  |     }).catchError((e) { | ||||||
|  |       throw {"errorCode": 1, "errorMessage": "Error getting config: $e"}; | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future _getStates() async { |   Future _getStates() async { | ||||||
|     await _sendInitialMessage("get_states").then((data) => entities.parse(data["result"])); |     await connection.sendSocketMessage(type: "get_states").then( | ||||||
|  |             (data) => entities.parse(data) | ||||||
|  |     ).catchError((e) { | ||||||
|  |       throw {"errorCode": 1, "errorMessage": "Error getting states: $e"}; | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future _getLovelace() async { |   Future _getLovelace() async { | ||||||
|     await _sendInitialMessage("lovelace/config").then((data) => _rawLovelaceData = data["result"]); |     await connection.sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) { | ||||||
|  |       throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"}; | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future _getUserInfo() async { |   Future _getUserInfo() async { | ||||||
|     _userName = null; |     _userName = null; | ||||||
|     await _sendInitialMessage("auth/current_user").then((data) => _userName = data["result"]["name"]); |     await connection.sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) { | ||||||
|  |       Logger.w("Can't get user info: ${e}"); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future _getServices() async { |   Future _getServices() async { | ||||||
|     await _sendInitialMessage("get_services").then((data) => Logger.d("We actually don`t need the list of servcies for now")); |     await connection.sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) { | ||||||
|  |       Logger.w("Can't get services: ${e}"); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future _getPanels() async { |   Future _getPanels() async { | ||||||
|     panels.clear(); |     panels.clear(); | ||||||
|     await _sendInitialMessage("get_panels").then((data) { |     await connection.sendSocketMessage(type: "get_panels").then((data) { | ||||||
|       if (data["success"]) { |       data.forEach((k,v) { | ||||||
|         data["result"].forEach((k,v) { |         String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}"; | ||||||
|             String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}"; |         panels.add(Panel( | ||||||
|             panels.add(Panel( |             id: k, | ||||||
|                 id: k, |             type: v["component_name"], | ||||||
|                 type: v["component_name"], |             title: title, | ||||||
|                 title: title, |             urlPath: v["url_path"], | ||||||
|                 urlPath: v["url_path"], |             config: v["config"], | ||||||
|                 config: v["config"], |             icon: v["icon"] | ||||||
|                 icon: v["icon"] |         ) | ||||||
|             ) |         ); | ||||||
|             ); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _incrementMessageId() { |  | ||||||
|     _currentMessageId += 1; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _sendAuthMessage(String message) { |  | ||||||
|     Logger.d( "[Sending] ==> auth request"); |  | ||||||
|     _hassioChannel.sink.add(message); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future _sendInitialMessage(String type) { |  | ||||||
|     Completer _completer = Completer(); |  | ||||||
|     _incrementMessageId(); |  | ||||||
|     _messageResolver[_currentMessageId] = _completer; |  | ||||||
|     _send('{"id": $_currentMessageId, "type": "$type"}', false); |  | ||||||
|     return _completer.future; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _send(String message, bool queued) { |  | ||||||
|     var sendCompleter = Completer(); |  | ||||||
|     if (queued) _messageQueue.add(message); |  | ||||||
|     _connection().then((r) { |  | ||||||
|       _messageQueue.getActualMessages().forEach((message){ |  | ||||||
|         Logger.d( "[Sending queued] ==> $message"); |  | ||||||
|         _hassioChannel.sink.add(message); |  | ||||||
|       }); |       }); | ||||||
|       if (!queued) { |     }).catchError((e) { | ||||||
|         Logger.d( "[Sending] ==> $message"); |       throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"}; | ||||||
|         _hassioChannel.sink.add(message); |  | ||||||
|       } |  | ||||||
|       sendCompleter.complete(); |  | ||||||
|     }).catchError((e){ |  | ||||||
|       sendCompleter.completeError(e); |  | ||||||
|     }); |     }); | ||||||
|     return sendCompleter.future; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) { |  | ||||||
|     _incrementMessageId(); |  | ||||||
|     String message = ""; |  | ||||||
|     if (entityId != null) { |  | ||||||
|       message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"'; |  | ||||||
|       if (additionalParams != null) { |  | ||||||
|         additionalParams.forEach((name, value) { |  | ||||||
|           if ((value is double) || (value is int) || (value is List)) { |  | ||||||
|             message += ', "$name" : $value'; |  | ||||||
|           } else { |  | ||||||
|             message += ', "$name" : "$value"'; |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|       message += '}}'; |  | ||||||
|     } else { |  | ||||||
|       message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"'; |  | ||||||
|       if (additionalParams != null && additionalParams.isNotEmpty) { |  | ||||||
|         message += ', "service_data": {'; |  | ||||||
|         bool first = true; |  | ||||||
|         additionalParams.forEach((name, value) { |  | ||||||
|           if (!first) { |  | ||||||
|             message += ', '; |  | ||||||
|           } |  | ||||||
|           if ((value is double) || (value is int) || (value is List)) { |  | ||||||
|             message += '"$name" : $value'; |  | ||||||
|           } else { |  | ||||||
|             message += '"$name" : "$value"'; |  | ||||||
|           } |  | ||||||
|           first = false; |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         message += '}'; |  | ||||||
|       } |  | ||||||
|       message += '}'; |  | ||||||
|     } |  | ||||||
|     return _send(message, true); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _handleEntityStateChange(Map eventData) { |   void _handleEntityStateChange(Map eventData) { | ||||||
| @@ -555,31 +349,12 @@ class HomeAssistant { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget buildViews(BuildContext context, bool lovelace, TabController tabController) { |   Widget buildViews(BuildContext context, TabController tabController) { | ||||||
|     return ui.build(context, tabController); |     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 = []; | ||||||
| @@ -618,4 +393,4 @@ class HAMessage { | |||||||
|   bool isExpired() { |   bool isExpired() { | ||||||
|     return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout; |     return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout; | ||||||
|   } |   } | ||||||
| } | }*/ | ||||||
|   | |||||||
							
								
								
									
										379
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						| @@ -17,6 +17,9 @@ import 'package:progress_indicators/progress_indicators.dart'; | |||||||
| import 'package:flutter_markdown/flutter_markdown.dart'; | import 'package:flutter_markdown/flutter_markdown.dart'; | ||||||
| import 'package:flutter_svg/flutter_svg.dart'; | import 'package:flutter_svg/flutter_svg.dart'; | ||||||
| import 'package:flutter_custom_tabs/flutter_custom_tabs.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'; | ||||||
|  |  | ||||||
| part 'entity_class/const.dart'; | part 'entity_class/const.dart'; | ||||||
| part 'entity_class/entity.class.dart'; | part 'entity_class/entity.class.dart'; | ||||||
| @@ -87,6 +90,8 @@ part 'entity.page.dart'; | |||||||
| part 'utils.class.dart'; | part 'utils.class.dart'; | ||||||
| part 'mdi.class.dart'; | part 'mdi.class.dart'; | ||||||
| part 'entity_collection.class.dart'; | part 'entity_collection.class.dart'; | ||||||
|  | part 'auth_manager.class.dart'; | ||||||
|  | part 'connection.class.dart'; | ||||||
| part 'ui_class/ui.dart'; | part 'ui_class/ui.dart'; | ||||||
| part 'ui_class/view.class.dart'; | part 'ui_class/view.class.dart'; | ||||||
| part 'ui_class/card.class.dart'; | part 'ui_class/card.class.dart'; | ||||||
| @@ -100,9 +105,7 @@ part 'ui_widgets/config_panel_widget.dart'; | |||||||
|  |  | ||||||
| EventBus eventBus = new EventBus(); | EventBus eventBus = new EventBus(); | ||||||
| const String appName = "HA Client"; | const String appName = "HA Client"; | ||||||
| const appVersion = "0.5.0"; | const appVersion = "0.6.0-alpha2"; | ||||||
|  |  | ||||||
| String homeAssistantWebHost; |  | ||||||
|  |  | ||||||
| void main() { | void main() { | ||||||
|   FlutterError.onError = (errorDetails) { |   FlutterError.onError = (errorDetails) { | ||||||
| @@ -124,6 +127,8 @@ void main() { | |||||||
| } | } | ||||||
|  |  | ||||||
| class HAClientApp extends StatelessWidget { | class HAClientApp extends StatelessWidget { | ||||||
|  |  | ||||||
|  |   final HomeAssistant homeAssistant = HomeAssistant(); | ||||||
|   // 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) { | ||||||
| @@ -134,7 +139,7 @@ class HAClientApp extends StatelessWidget { | |||||||
|       ), |       ), | ||||||
|       initialRoute: "/", |       initialRoute: "/", | ||||||
|       routes: { |       routes: { | ||||||
|         "/": (context) => MainPage(title: 'HA Client'), |         "/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,), | ||||||
|         "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), |         "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), | ||||||
|         "/configuration": (context) => PanelPage(title: "Configuration"), |         "/configuration": (context) => PanelPage(title: "Configuration"), | ||||||
|         "/log-view": (context) => LogViewPage(title: "Log") |         "/log-view": (context) => LogViewPage(title: "Log") | ||||||
| @@ -144,82 +149,84 @@ class HAClientApp extends StatelessWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class MainPage extends StatefulWidget { | class MainPage extends StatefulWidget { | ||||||
|   MainPage({Key key, this.title}) : super(key: key); |   MainPage({Key key, this.title, this.homeAssistant}) : super(key: key); | ||||||
|  |  | ||||||
|   final String title; |   final String title; | ||||||
|  |   final HomeAssistant homeAssistant; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   _MainPageState createState() => new _MainPageState(); |   _MainPageState createState() => new _MainPageState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin { | class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin { | ||||||
|   HomeAssistant _homeAssistant; |  | ||||||
|   //Map _instanceConfig; |  | ||||||
|   String _webSocketApiEndpoint; |  | ||||||
|   String _password; |  | ||||||
|   //int _uiViewsCount = 0; |  | ||||||
|   String _instanceHost; |  | ||||||
|   StreamSubscription _stateSubscription; |   StreamSubscription _stateSubscription; | ||||||
|   StreamSubscription _settingsSubscription; |   StreamSubscription _settingsSubscription; | ||||||
|   StreamSubscription _serviceCallSubscription; |   StreamSubscription _serviceCallSubscription; | ||||||
|   StreamSubscription _showEntityPageSubscription; |   StreamSubscription _showEntityPageSubscription; | ||||||
|   StreamSubscription _showErrorSubscription; |   StreamSubscription _showErrorSubscription; | ||||||
|   bool _settingsLoaded = false; |   StreamSubscription _startAuthSubscription; | ||||||
|   bool _accountMenuExpanded = false; |   StreamSubscription _reloadUISubscription; | ||||||
|   bool _useLovelaceUI; |  | ||||||
|   int _previousViewCount; |   int _previousViewCount; | ||||||
|  |   //final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _settingsLoaded = false; |     //widget.homeAssistant = HomeAssistant(); | ||||||
|  |     //_settingsLoaded = false; | ||||||
|     WidgetsBinding.instance.addObserver(this); |     WidgetsBinding.instance.addObserver(this); | ||||||
|  |  | ||||||
|     Logger.d("<!!!> Creating new HomeAssistant instance"); |  | ||||||
|     _homeAssistant = HomeAssistant(); |  | ||||||
|  |  | ||||||
|     _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { |     _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { | ||||||
|       Logger.d("Settings change event: reconnect=${event.reconnect}"); |       Logger.d("Settings change event: reconnect=${event.reconnect}"); | ||||||
|       if (event.reconnect) { |       if (event.reconnect) { | ||||||
|         _homeAssistant.disconnect().then((_){ |         _reLoad(); | ||||||
|           _initialLoad(); |  | ||||||
|         }); |  | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     _initialLoad(); |     _initialLoad(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _initialLoad() { |   void _initialLoad() { | ||||||
|     _loadConnectionSettings().then((_){ |     _showInfoBottomBar(progress: true,); | ||||||
|       _subscribe(); |     _subscribe(); | ||||||
|       _refreshData(); |     widget.homeAssistant.init().then((_){ | ||||||
|     }, onError: (_) { |       _fetchData(); | ||||||
|       _showErrorBottomBar(message: _, errorCode: 5); |     }, onError: (e) { | ||||||
|  |       _setErrorState(e); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void _reLoad() { | ||||||
|  |     _hideBottomBar(); | ||||||
|  |     _showInfoBottomBar(progress: true,); | ||||||
|  |     widget.homeAssistant.init().then((_){ | ||||||
|  |       _fetchData(); | ||||||
|  |     }, onError: (e) { | ||||||
|  |       _setErrorState(e); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   _fetchData() async { | ||||||
|  |     await widget.homeAssistant.fetch().then((_) { | ||||||
|  |       _hideBottomBar(); | ||||||
|  |       int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0; | ||||||
|  |       if (_previousViewCount != currentViewCount) { | ||||||
|  |         Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller."); | ||||||
|  |         _viewsTabController = TabController(vsync: this, length: currentViewCount); | ||||||
|  |         _previousViewCount = currentViewCount; | ||||||
|  |       } | ||||||
|  |     }).catchError((e) { | ||||||
|  |       _setErrorState(e); | ||||||
|  |     }); | ||||||
|  |     eventBus.fire(RefreshDataFinishedEvent()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { |   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||||
|     Logger.d("$state"); |     Logger.d("$state"); | ||||||
|     if (state == AppLifecycleState.resumed && _settingsLoaded) { |     if (state == AppLifecycleState.resumed) { | ||||||
|       _refreshData(); |       _reLoad(); | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _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; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -228,12 +235,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|       _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { |       _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { | ||||||
|         if (event.needToRebuildUI) { |         if (event.needToRebuildUI) { | ||||||
|           Logger.d("New entity. Need to rebuild UI"); |           Logger.d("New entity. Need to rebuild UI"); | ||||||
|           _refreshData(); |           _reLoad(); | ||||||
|         } else { |         } else { | ||||||
|           setState(() {}); |           setState(() {}); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |     if (_reloadUISubscription == null) { | ||||||
|  |       _reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){ | ||||||
|  |         _reLoad(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|     if (_serviceCallSubscription == null) { |     if (_serviceCallSubscription == null) { | ||||||
|       _serviceCallSubscription = |       _serviceCallSubscription = | ||||||
|           eventBus.on<ServiceCallEvent>().listen((event) { |           eventBus.on<ServiceCallEvent>().listen((event) { | ||||||
| @@ -254,24 +266,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|         _showErrorBottomBar(message: event.text, errorCode: event.errorCode); |         _showErrorBottomBar(message: event.text, errorCode: event.errorCode); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (_startAuthSubscription == null) { | ||||||
|  |       _startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){ | ||||||
|  |         _showOAuth(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /*_firebaseMessaging.getToken().then((String token) { | ||||||
|  |       //Logger.d("FCM token: $token"); | ||||||
|  |       widget.homeAssistant.sendHTTPPost( | ||||||
|  |           endPoint: '/api/notify.fcm-android', | ||||||
|  |           jsonData:  '{"token": "$token"}' | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |     _firebaseMessaging.configure( | ||||||
|  |         onLaunch: (data) { | ||||||
|  |           Logger.d("Notification [onLaunch]: $data"); | ||||||
|  |         }, | ||||||
|  |         onMessage: (data) { | ||||||
|  |           Logger.d("Notification [onMessage]: $data"); | ||||||
|  |         }, | ||||||
|  |         onResume: (data) { | ||||||
|  |           Logger.d("Notification [onResume]: $data"); | ||||||
|  |         } | ||||||
|  |     );*/ | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   _refreshData() async { |   void _showOAuth() { | ||||||
|     _homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI); |     Navigator.push( | ||||||
|     _hideBottomBar(); |         context, | ||||||
|     _showInfoBottomBar(progress: true,); |         MaterialPageRoute( | ||||||
|     await _homeAssistant.fetch().then((result) { |           builder: (context) => WebviewScaffold( | ||||||
|       _hideBottomBar(); |             url: "${widget.homeAssistant.connection.oauthUrl}", | ||||||
|       int currentViewCount = _homeAssistant.ui?.views?.length ?? 0; |             appBar: new AppBar( | ||||||
|       if (_previousViewCount != currentViewCount) { |               leading: IconButton( | ||||||
|         Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller."); |                   icon: Icon(Icons.help), | ||||||
|         _viewsTabController = TabController(vsync: this, length: currentViewCount); |                   onPressed: () => HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/docs#authentication") | ||||||
|         _previousViewCount = currentViewCount; |               ), | ||||||
|       } |               title: new Text("Login to your Home Assistant"), | ||||||
|     }).catchError((e) { |             ), | ||||||
|       _setErrorState(e); |           ), | ||||||
|     }); |         ) | ||||||
|     eventBus.fire(RefreshDataFinishedEvent()); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   _setErrorState(e) { |   _setErrorState(e) { | ||||||
| @@ -279,7 +318,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|       Logger.e(e.toString()); |       Logger.e(e.toString()); | ||||||
|       Logger.e("${e.stackTrace}"); |       Logger.e("${e.stackTrace}"); | ||||||
|       _showErrorBottomBar( |       _showErrorBottomBar( | ||||||
|           message: "There was some error", |           message: "Unknown error", | ||||||
|           errorCode: 13 |           errorCode: 13 | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
| @@ -290,19 +329,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) { |   void _callService(String domain, String service, String entityId, Map additionalParams) { | ||||||
|     _showInfoBottomBar( |     _showInfoBottomBar( | ||||||
|       message: "Calling $domain.$service", |       message: "Calling $domain.$service", | ||||||
|       duration: Duration(seconds: 3) |       duration: Duration(seconds: 3) | ||||||
|     ); |     ); | ||||||
|     _homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e)); |     widget.homeAssistant.connection.callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _showEntityPage(String entityId) { |   void _showEntityPage(String entityId) { | ||||||
|     Navigator.push( |     Navigator.push( | ||||||
|         context, |         context, | ||||||
|         MaterialPageRoute( |         MaterialPageRoute( | ||||||
|           builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant), |           builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant), | ||||||
|         ) |         ) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| @@ -310,8 +349,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|   List<Tab> buildUIViewTabs() { |   List<Tab> buildUIViewTabs() { | ||||||
|     List<Tab> result = []; |     List<Tab> result = []; | ||||||
|  |  | ||||||
|       if (_homeAssistant.ui.views.isNotEmpty) { |       if (widget.homeAssistant.ui.views.isNotEmpty) { | ||||||
|         _homeAssistant.ui.views.forEach((HAView view) { |         widget.homeAssistant.ui.views.forEach((HAView view) { | ||||||
|           result.add(view.buildTab()); |           result.add(view.buildTab()); | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
| @@ -323,16 +362,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|     List<Widget> menuItems = []; |     List<Widget> menuItems = []; | ||||||
|     menuItems.add( |     menuItems.add( | ||||||
|         UserAccountsDrawerHeader( |         UserAccountsDrawerHeader( | ||||||
|           accountName: Text(_homeAssistant.userName), |           accountName: Text(widget.homeAssistant.userName), | ||||||
|           accountEmail: Text(_instanceHost ?? "Not configured"), |           accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"), | ||||||
|           onDetailsPressed: () { |           /*onDetailsPressed: () { | ||||||
|             setState(() { |             setState(() { | ||||||
|               _accountMenuExpanded = !_accountMenuExpanded; |               _accountMenuExpanded = !_accountMenuExpanded; | ||||||
|             }); |             }); | ||||||
|           }, |           },*/ | ||||||
|           currentAccountPicture: CircleAvatar( |           currentAccountPicture: CircleAvatar( | ||||||
|             child: Text( |             child: Text( | ||||||
|               _homeAssistant.userAvatarText, |               widget.homeAssistant.userAvatarText, | ||||||
|               style: TextStyle( |               style: TextStyle( | ||||||
|                   fontSize: 32.0 |                   fontSize: 32.0 | ||||||
|               ), |               ), | ||||||
| @@ -340,21 +379,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|           ), |           ), | ||||||
|         ) |         ) | ||||||
|     ); |     ); | ||||||
|     if (_accountMenuExpanded) { |       if (widget.homeAssistant.panels.isNotEmpty) { | ||||||
|       menuItems.addAll([ |         widget.homeAssistant.panels.forEach((Panel panel) { | ||||||
|         ListTile( |  | ||||||
|           leading: Icon(Icons.settings), |  | ||||||
|           title: Text("Settings"), |  | ||||||
|           onTap: () { |  | ||||||
|             Navigator.of(context).pop(); |  | ||||||
|             Navigator.of(context).pushNamed('/connection-settings'); |  | ||||||
|           }, |  | ||||||
|         ), |  | ||||||
|         Divider(), |  | ||||||
|       ]); |  | ||||||
|     } else { |  | ||||||
|       if (_homeAssistant != null && _homeAssistant.panels.isNotEmpty) { |  | ||||||
|         _homeAssistant.panels.forEach((Panel panel) { |  | ||||||
|           if (!panel.isHidden) { |           if (!panel.isHidden) { | ||||||
|             menuItems.add( |             menuItems.add( | ||||||
|                 new ListTile( |                 new ListTile( | ||||||
| @@ -365,16 +391,28 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|             ); |             ); | ||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
|         menuItems.addAll([ |       } | ||||||
|  |       //TODO check for loaded | ||||||
|  |       menuItems.add( | ||||||
|           new ListTile( |           new ListTile( | ||||||
|             leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")), |             leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")), | ||||||
|             title: Text("Open Web UI"), |             title: Text("Open Web UI"), | ||||||
|             onTap: () => HAUtils.launchURL(homeAssistantWebHost), |             onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost), | ||||||
|           ), |           ) | ||||||
|           Divider() |       ); | ||||||
|         ]); |  | ||||||
|       } |  | ||||||
|       menuItems.addAll([ |       menuItems.addAll([ | ||||||
|  |         Divider(), | ||||||
|  |         ListTile( | ||||||
|  |           leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")), | ||||||
|  |           title: Text("Connection settings"), | ||||||
|  |           onTap: () { | ||||||
|  |             Navigator.of(context).pop(); | ||||||
|  |             Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant}); | ||||||
|  |           }, | ||||||
|  |         ) | ||||||
|  |       ]); | ||||||
|  |       menuItems.addAll([ | ||||||
|  |         Divider(), | ||||||
|         new ListTile( |         new ListTile( | ||||||
|           leading: Icon(Icons.insert_drive_file), |           leading: Icon(Icons.insert_drive_file), | ||||||
|           title: Text("Log"), |           title: Text("Log"), | ||||||
| @@ -392,6 +430,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|           }, |           }, | ||||||
|         ), |         ), | ||||||
|         Divider(), |         Divider(), | ||||||
|  |         new ListTile( | ||||||
|  |           leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")), | ||||||
|  |           title: Text("Join Discord server"), | ||||||
|  |           onTap: () { | ||||||
|  |             Navigator.of(context).pop(); | ||||||
|  |             HAUtils.launchURL("https://discord.gg/AUzEvwn"); | ||||||
|  |           }, | ||||||
|  |         ), | ||||||
|         new AboutListTile( |         new AboutListTile( | ||||||
|           aboutBoxChildren: <Widget>[ |           aboutBoxChildren: <Widget>[ | ||||||
|             GestureDetector( |             GestureDetector( | ||||||
| @@ -406,13 +452,44 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|                   decoration: TextDecoration.underline |                   decoration: TextDecoration.underline | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|  |             ), | ||||||
|  |             Container( | ||||||
|  |               height: 10.0, | ||||||
|  |             ), | ||||||
|  |             GestureDetector( | ||||||
|  |               onTap: () { | ||||||
|  |                 Navigator.of(context).pop(); | ||||||
|  |                 HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/terms_and_conditions"); | ||||||
|  |               }, | ||||||
|  |               child: Text( | ||||||
|  |                 "Terms and Conditions", | ||||||
|  |                 style: TextStyle( | ||||||
|  |                     color: Colors.blue, | ||||||
|  |                     decoration: TextDecoration.underline | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             Container( | ||||||
|  |               height: 10.0, | ||||||
|  |             ), | ||||||
|  |             GestureDetector( | ||||||
|  |               onTap: () { | ||||||
|  |                 Navigator.of(context).pop(); | ||||||
|  |                 HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/privacy_policy"); | ||||||
|  |               }, | ||||||
|  |               child: Text( | ||||||
|  |                 "Privacy Policy", | ||||||
|  |                 style: TextStyle( | ||||||
|  |                     color: Colors.blue, | ||||||
|  |                     decoration: TextDecoration.underline | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|             ) |             ) | ||||||
|           ], |           ], | ||||||
|           applicationName: appName, |           applicationName: appName, | ||||||
|           applicationVersion: appVersion |           applicationVersion: appVersion | ||||||
|         ) |         ) | ||||||
|       ]); |       ]); | ||||||
|     } |  | ||||||
|     return new Drawer( |     return new Drawer( | ||||||
|       child: ListView( |       child: ListView( | ||||||
|         children: menuItems, |         children: menuItems, | ||||||
| @@ -465,7 +542,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|                 child: Text("Retry", style: textStyle), |                 child: Text("Retry", style: textStyle), | ||||||
|                 onPressed: () { |                 onPressed: () { | ||||||
|                   //_scaffoldKey?.currentState?.hideCurrentSnackBar(); |                   //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||||
|                   _refreshData(); |                   _reLoad(); | ||||||
|                 }, |                 }, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
| @@ -483,12 +560,32 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         case 6: { |         case 60: { | ||||||
|           _bottomBarAction = FlatButton( |           _bottomBarAction = FlatButton( | ||||||
|               child: Text("Settings", style: textStyle), |               child: Text("Login", style: textStyle), | ||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); |               _reLoad(); | ||||||
|               Navigator.pushNamed(context, '/connection-settings'); |             }, | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         case 63: | ||||||
|  |         case 61: { | ||||||
|  |           _bottomBarAction = FlatButton( | ||||||
|  |             child: Text("Try again", style: textStyle), | ||||||
|  |             onPressed: () { | ||||||
|  |               _reLoad(); | ||||||
|  |             }, | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         case 62: { | ||||||
|  |           _bottomBarAction = FlatButton( | ||||||
|  |             child: Text("Login again", style: textStyle), | ||||||
|  |             onPressed: () { | ||||||
|  |               _reLoad(); | ||||||
|             }, |             }, | ||||||
|           ); |           ); | ||||||
|           break; |           break; | ||||||
| @@ -499,52 +596,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|               child: Text("Refresh", style: textStyle), |               child: Text("Refresh", style: textStyle), | ||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); |               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||||
|               _refreshData(); |               _reLoad(); | ||||||
|             }, |             }, | ||||||
|           ); |           ); | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         case 82: | ||||||
|  |         case 81: | ||||||
|         case 8: { |         case 8: { | ||||||
|           _bottomBarAction = FlatButton( |           _bottomBarAction = FlatButton( | ||||||
|               child: Text("Reconnect", style: textStyle), |               child: Text("Reconnect", style: textStyle), | ||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); |               _reLoad(); | ||||||
|               _refreshData(); |  | ||||||
|             }, |             }, | ||||||
|           ); |           ); | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         default: { |         default: { | ||||||
|           _bottomBarAction = FlatButton( |           _bottomBarAction = Container(width: 0.0, height: 0.0,); | ||||||
|               child: Text("Reload", style: textStyle), |  | ||||||
|             onPressed: () { |  | ||||||
|               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); |  | ||||||
|               _refreshData(); |  | ||||||
|             }, |  | ||||||
|           ); |  | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       setState(() { |       setState(() { | ||||||
|         _bottomBarProgress = false; |         _bottomBarProgress = false; | ||||||
|         _bottomBarText = "$message (code: $errorCode)"; |         _bottomBarText = "$message"; | ||||||
|         _showBottomBar = true; |         _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>(); |   final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); | ||||||
|  |  | ||||||
|   Widget _buildScaffoldBody(bool empty) { |   Widget _buildScaffoldBody(bool empty) { | ||||||
|  |     List<PopupMenuItem<String>> popupMenuItems = []; | ||||||
|  |     popupMenuItems.add(PopupMenuItem<String>( | ||||||
|  |       child: new Text("Reload"), | ||||||
|  |       value: "reload", | ||||||
|  |     )); | ||||||
|  |     if (widget.homeAssistant.connection.isAuthenticated) { | ||||||
|  |       popupMenuItems.add( | ||||||
|  |           PopupMenuItem<String>( | ||||||
|  |             child: new Text("Logout"), | ||||||
|  |             value: "logout", | ||||||
|  |           )); | ||||||
|  |     } | ||||||
|     return NestedScrollView( |     return NestedScrollView( | ||||||
|       headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { |       headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||||
|         return <Widget>[ |         return <Widget>[ | ||||||
| @@ -552,7 +648,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|             floating: true, |             floating: true, | ||||||
|             pinned: true, |             pinned: true, | ||||||
|             primary: true, |             primary: true, | ||||||
|             title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""), |             title: Text(widget.homeAssistant.locationName ?? ""), | ||||||
|             actions: <Widget>[ |             actions: <Widget>[ | ||||||
|               IconButton( |               IconButton( | ||||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName( |                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||||
| @@ -561,13 +657,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|                   showMenu( |                   showMenu( | ||||||
|                     position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0), |                     position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0), | ||||||
|                     context: context, |                     context: context, | ||||||
|                     items: [PopupMenuItem<String>( |                     items: popupMenuItems | ||||||
|                       child: new Text("Reload"), |  | ||||||
|                       value: "reload", |  | ||||||
|                     )] |  | ||||||
|                   ).then((String val) { |                   ).then((String val) { | ||||||
|                     if (val == "reload") { |                     if (val == "reload") { | ||||||
|                       _refreshData(); |                       _reLoad(); | ||||||
|  |                     } else if (val == "logout") { | ||||||
|  |                       widget.homeAssistant.logout().then((_) { | ||||||
|  |                         _reLoad(); | ||||||
|  |                       }); | ||||||
|                     } |                     } | ||||||
|                   }); |                   }); | ||||||
|                 } |                 } | ||||||
| @@ -577,9 +674,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|               icon: Icon(Icons.menu), |               icon: Icon(Icons.menu), | ||||||
|               onPressed: () { |               onPressed: () { | ||||||
|                 _scaffoldKey.currentState.openDrawer(); |                 _scaffoldKey.currentState.openDrawer(); | ||||||
|                 setState(() { |  | ||||||
|                   _accountMenuExpanded = false; |  | ||||||
|                 }); |  | ||||||
|               }, |               }, | ||||||
|             ), |             ), | ||||||
|             bottom: empty ? null : TabBar( |             bottom: empty ? null : TabBar( | ||||||
| @@ -597,15 +691,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|             mainAxisAlignment: MainAxisAlignment.center, |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|             children: [ |             children: [ | ||||||
|               Icon( |               Icon( | ||||||
|                 MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant"), |                 MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"), | ||||||
|                 size: 100.0, |                 size: 100.0, | ||||||
|                 color: Colors.blue, |                 color: Colors.black26, | ||||||
|               ), |               ), | ||||||
|             ] |             ] | ||||||
|         ), |         ), | ||||||
|       ) |       ) | ||||||
|           : |           : | ||||||
|       _homeAssistant.buildViews(context, _useLovelaceUI, _viewsTabController), |       widget.homeAssistant.buildViews(context, _viewsTabController), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -662,7 +756,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     // This method is rerun every time setState is called. |     // This method is rerun every time setState is called. | ||||||
|     if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) { |     if (widget.homeAssistant.isNoViews) { | ||||||
|       return Scaffold( |       return Scaffold( | ||||||
|         key: _scaffoldKey, |         key: _scaffoldKey, | ||||||
|         primary: false, |         primary: false, | ||||||
| @@ -678,7 +772,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|         bottomNavigationBar: bottomBar, |         bottomNavigationBar: bottomBar, | ||||||
|         body: HomeAssistantModel( |         body: HomeAssistantModel( | ||||||
|           child: _buildScaffoldBody(false), |           child: _buildScaffoldBody(false), | ||||||
|           homeAssistant: _homeAssistant |           homeAssistant: widget.homeAssistant | ||||||
|         ), |         ), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| @@ -686,14 +780,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void dispose() { |   void dispose() { | ||||||
|  |     final flutterWebviewPlugin = new FlutterWebviewPlugin(); | ||||||
|  |     flutterWebviewPlugin.dispose(); | ||||||
|     WidgetsBinding.instance.removeObserver(this); |     WidgetsBinding.instance.removeObserver(this); | ||||||
|     _viewsTabController.dispose(); |     _viewsTabController?.dispose(); | ||||||
|     if (_stateSubscription != null) _stateSubscription.cancel(); |     _stateSubscription?.cancel(); | ||||||
|     if (_settingsSubscription != null) _settingsSubscription.cancel(); |     _settingsSubscription?.cancel(); | ||||||
|     if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); |     _serviceCallSubscription?.cancel(); | ||||||
|     if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); |     _showEntityPageSubscription?.cancel(); | ||||||
|     if (_showErrorSubscription != null) _showErrorSubscription.cancel(); |     _showErrorSubscription?.cancel(); | ||||||
|     _homeAssistant.disconnect(); |     _startAuthSubscription?.cancel(); | ||||||
|  |     _reloadUISubscription?.cancel(); | ||||||
|  |     //TODO disconnect | ||||||
|  |     //widget.homeAssistant?.disconnect(); | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,17 +14,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|   String _newHassioDomain = ""; |   String _newHassioDomain = ""; | ||||||
|   String _hassioPort = ""; |   String _hassioPort = ""; | ||||||
|   String _newHassioPort = ""; |   String _newHassioPort = ""; | ||||||
|   String _hassioPassword = ""; |  | ||||||
|   String _newHassioPassword = ""; |  | ||||||
|   String _socketProtocol = "wss"; |   String _socketProtocol = "wss"; | ||||||
|   String _newSocketProtocol = "wss"; |   String _newSocketProtocol = "wss"; | ||||||
|   bool _useLovelace = true; |   bool _useLovelace = true; | ||||||
|   bool _newUseLovelace = true; |   bool _newUseLovelace = true; | ||||||
|  |  | ||||||
|  |   String oauthUrl; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _loadSettings(); |     _loadSettings(); | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   _loadSettings() async { |   _loadSettings() async { | ||||||
| @@ -33,7 +34,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|     setState(() { |     setState(() { | ||||||
|       _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; |       _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; | ||||||
|       _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; |       _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; | ||||||
|       _hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? ""; |  | ||||||
|       _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; |       _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; | ||||||
|       try { |       try { | ||||||
|         _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true; |         _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true; | ||||||
| @@ -44,7 +44,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool _checkConfigChanged() { |   bool _checkConfigChanged() { | ||||||
|     return ((_newHassioPassword != _hassioPassword) || |     return ( | ||||||
|       (_newHassioPort != _hassioPort) || |       (_newHassioPort != _hassioPort) || | ||||||
|       (_newHassioDomain != _hassioDomain) || |       (_newHassioDomain != _hassioDomain) || | ||||||
|       (_newSocketProtocol != _socketProtocol) || |       (_newSocketProtocol != _socketProtocol) || | ||||||
| @@ -59,7 +59,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); |     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|     prefs.setString("hassio-domain", _newHassioDomain); |     prefs.setString("hassio-domain", _newHassioDomain); | ||||||
|     prefs.setString("hassio-port", _newHassioPort); |     prefs.setString("hassio-port", _newHassioPort); | ||||||
|     prefs.setString("hassio-password", _newHassioPassword); |  | ||||||
|     prefs.setString("hassio-protocol", _newSocketProtocol); |     prefs.setString("hassio-protocol", _newSocketProtocol); | ||||||
|     prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); |     prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); | ||||||
|     prefs.setBool("use-lovelace", _newUseLovelace); |     prefs.setBool("use-lovelace", _newUseLovelace); | ||||||
| @@ -149,21 +148,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|             "Try ports 80 and 443 if default is not working and you don't know why.", |             "Try ports 80 and 443 if default is not working and you don't know why.", | ||||||
|             style: TextStyle(color: Colors.grey), |             style: TextStyle(color: Colors.grey), | ||||||
|           ), |           ), | ||||||
|           new TextField( |  | ||||||
|             decoration: InputDecoration( |  | ||||||
|                 labelText: "Access token" |  | ||||||
|             ), |  | ||||||
|             controller: new TextEditingController.fromValue( |  | ||||||
|                 new TextEditingValue( |  | ||||||
|                     text: _newHassioPassword, |  | ||||||
|                     selection: |  | ||||||
|                     new TextSelection.collapsed(offset: _newHassioPassword.length) |  | ||||||
|                 ) |  | ||||||
|             ), |  | ||||||
|             onChanged: (value) { |  | ||||||
|               _newHassioPassword = value; |  | ||||||
|             } |  | ||||||
|           ), |  | ||||||
|           Padding( |           Padding( | ||||||
|             padding: EdgeInsets.only(top: 20.0), |             padding: EdgeInsets.only(top: 20.0), | ||||||
|             child: Text( |             child: Text( | ||||||
|   | |||||||
| @@ -36,7 +36,8 @@ class Panel { | |||||||
|           ) |           ) | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       String url = "$homeAssistantWebHost/$urlPath"; |       HomeAssistantModel haModel = HomeAssistantModel.of(context); | ||||||
|  |       String url = "${haModel.homeAssistant.connection.httpWebHost}/$urlPath"; | ||||||
|       Logger.d("Launching custom tab with $url"); |       Logger.d("Launching custom tab with $url"); | ||||||
|       HAUtils.launchURLInCustomTab(context, url); |       HAUtils.launchURLInCustomTab(context, url); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ class HomeAssistantUI { | |||||||
|   List<HAView> views; |   List<HAView> views; | ||||||
|   String title; |   String title; | ||||||
|  |  | ||||||
|  |   bool get isEmpty => views == null || views.isEmpty; | ||||||
|  |  | ||||||
|   HomeAssistantUI() { |   HomeAssistantUI() { | ||||||
|     views = []; |     views = []; | ||||||
|   } |   } | ||||||
| @@ -25,4 +27,8 @@ class HomeAssistantUI { | |||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void clear() { | ||||||
|  |     views.clear(); | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -109,6 +109,16 @@ class RefreshDataFinishedEvent { | |||||||
|   RefreshDataFinishedEvent(); |   RefreshDataFinishedEvent(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class ReloadUIEvent { | ||||||
|  |   ReloadUIEvent(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class StartAuthEvent { | ||||||
|  |   String oauthUrl; | ||||||
|  |  | ||||||
|  |   StartAuthEvent(this.oauthUrl); | ||||||
|  | } | ||||||
|  |  | ||||||
| class ServiceCallEvent { | class ServiceCallEvent { | ||||||
|   String domain; |   String domain; | ||||||
|   String service; |   String service; | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						| @@ -101,6 +101,13 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.3" |     version: "1.0.3" | ||||||
|  |   firebase_messaging: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: firebase_messaging | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "4.0.0+1" | ||||||
|   flutter: |   flutter: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -134,6 +141,13 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.2.0" |     version: "0.2.0" | ||||||
|  |   flutter_secure_storage: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: flutter_secure_storage | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.2.0" | ||||||
|   flutter_svg: |   flutter_svg: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -146,6 +160,13 @@ packages: | |||||||
|     description: flutter |     description: flutter | ||||||
|     source: sdk |     source: sdk | ||||||
|     version: "0.0.0" |     version: "0.0.0" | ||||||
|  |   flutter_webview_plugin: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: flutter_webview_plugin | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.3.1" | ||||||
|   http: |   http: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -244,6 +265,13 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.1" |     version: "2.1.1" | ||||||
|  |   platform: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: platform | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "2.2.0" | ||||||
|   progress_indicators: |   progress_indicators: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -264,7 +292,7 @@ packages: | |||||||
|       name: shared_preferences |       name: shared_preferences | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.5.1+1" |     version: "0.5.1+2" | ||||||
|   sky_engine: |   sky_engine: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: flutter |     description: flutter | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| name: hass_client | name: hass_client | ||||||
| description: Home Assistant Android Client | description: Home Assistant Android Client | ||||||
|  |  | ||||||
| version: 0.5.0+97 | version: 0.6.0+601 | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: ">=2.0.0-dev.68.0 <3.0.0" |   sdk: ">=2.0.0-dev.68.0 <3.0.0" | ||||||
| @@ -20,6 +20,9 @@ dependencies: | |||||||
|   flutter_markdown: any |   flutter_markdown: any | ||||||
|   flutter_svg: ^0.10.3 |   flutter_svg: ^0.10.3 | ||||||
|   flutter_custom_tabs: ^0.6.0 |   flutter_custom_tabs: ^0.6.0 | ||||||
|  |   firebase_messaging: ^4.0.0+1 | ||||||
|  |   flutter_webview_plugin: ^0.3.1 | ||||||
|  |   flutter_secure_storage: ^3.2.0 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||