Compare commits
	
		
			138 Commits
		
	
	
		
			0.3.10.1
			...
			0.6.0-alph
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 4e96b9adbb | ||
|  | b9581d3762 | ||
|  | 7c010359c3 | ||
|  | 4a75243994 | ||
|  | d29d7e5b3b | ||
|  | 5ebd25e0d1 | ||
|  | b7d5a53e86 | ||
|  | 20d3498bfd | ||
|  | 67d7bb45f5 | ||
|  | 6a03105d01 | ||
|  | 5ae580ecf1 | ||
|  | 0efef33e53 | ||
|  | ccb88884a7 | ||
|  | d70ba0a55a | ||
|  | 5140840d3a | ||
|  | 14759fd3c9 | ||
|  | fed35be517 | ||
|  | db77cc43aa | ||
|  | b2269cc96d | ||
|  | 8b28bb2e9e | ||
|  | fb456878bc | ||
|  | 8b961ebd69 | ||
|  | 9bd3a41cf5 | ||
|  | 491ae55a2a | ||
|  | e1d2981782 | ||
|  | 74572168ae | ||
|  | 92d0b5c055 | ||
|  | 3504d3276c | ||
|  | 736b38b64c | ||
|  | cb118b599a | ||
|  | a08a056cff | ||
|  | 0ef2ebfe31 | ||
|  | 4f4ac3b574 | ||
|  | 7064cb0e30 | ||
|  | 91a99e17e0 | ||
|  | 2e9b7d20b9 | ||
|  | b8aa808de4 | ||
|  | 2cfa92a42b | ||
|  | 146efef72d | ||
|  | 8c9804e16f | ||
|  | a4736bfb5a | ||
|  | 15c54df629 | ||
|  | 32ffef21e9 | ||
|  | 848d3cb510 | ||
|  | 8a4caeebba | ||
|  | aa923f0fba | ||
|  | 4d8f50ddd5 | ||
|  | fe06b21a6c | ||
|  | efed7fb1b5 | ||
|  | df2cbb7d13 | ||
|  | 03edaa9ca2 | ||
|  | 1a7457abf9 | ||
|  | 00889b13e0 | ||
|  | 0615073ec4 | ||
|  | eb7d17d147 | ||
|  | 24f80feeee | ||
|  | 4b6dda5a9c | ||
|  | 4099fa0c83 | ||
|  | 76057e8797 | ||
|  | 538d3603dc | ||
|  | bc0e72ca52 | ||
|  | f25a47beb2 | ||
|  | cc3c6b0087 | ||
|  | 6cf80c0bfd | ||
|  | 8ce9bdb7a5 | ||
|  | 31e50150b1 | ||
|  | e359150d97 | ||
|  | 93680c981c | ||
|  | e06b66c523 | ||
|  | 3dea844e1e | ||
|  | 62b1af30e0 | ||
|  | e006c4e403 | ||
|  | 983573388e | ||
|  | bdd1dc7e17 | ||
|  | 9c1970ee14 | ||
|  | d0e0bf3571 | ||
|  | b399357517 | ||
|  | 0290cd3a32 | ||
|  | d8a1d03179 | ||
|  | 216fad3cb9 | ||
|  | fead6ea348 | ||
|  | 8814687be6 | ||
|  | 71c0e2caa0 | ||
|  | 1531c41542 | ||
|  | bc90d013e8 | ||
|  | 2adfaca0c4 | ||
|  | 6cc1a37d9d | ||
|  | 4bb616b327 | ||
|  | 38219618ba | ||
|  | 6774b53758 | ||
|  | 29a94c882f | ||
|  | 5897fa3a99 | ||
|  | 7af92c2dc9 | ||
|  | 1094177a42 | ||
|  | 5e814e8109 | ||
|  | 24c7675fa4 | ||
|  | dc3ca38c78 | ||
|  | 96b528e055 | ||
|  | 3858036631 | ||
|  | 19d42ceeb3 | ||
|  | a2836a3603 | ||
|  | 2a45758a6d | ||
|  | dc1bf4d878 | ||
|  | e82ba60c4e | ||
|  | 09199d30e8 | ||
|  | 724d32dbe2 | ||
|  | 949c8ee44e | ||
|  | 1a446d34c7 | ||
|  | 22a5847285 | ||
|  | 1c8f770f10 | ||
|  | be5ea55f6b | ||
|  | c65ade9827 | ||
|  | d3c1422b9e | ||
|  | b6ac9f985f | ||
|  | a59de4b6dc | ||
|  | f507d5df0c | ||
|  | f77e46de37 | ||
|  | cda17b1217 | ||
|  | be560769ef | ||
|  | 3815800e32 | ||
|  | a3226311a2 | ||
|  | 79669243c2 | ||
|  | fdc81f6ea4 | ||
|  | 7fe44459e7 | ||
|  | a8500d44e1 | ||
|  | b4d4c5abec | ||
|  | c19a3f272a | ||
|  | b264534858 | ||
|  | ab53f77f9e | ||
|  | c73956720c | ||
|  | 051041e794 | ||
|  | 5c83be9fee | ||
|  | 4bece42693 | ||
|  | 4ae107fe4c | ||
|  | 9523ed2562 | ||
|  | 9c403480e2 | ||
|  | 20b1b90e39 | ||
|  | 5633e30448 | 
							
								
								
									
										76
									
								
								CODE_OF_CONDUCT.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,76 @@ | ||||
| # Contributor Covenant Code of Conduct | ||||
|  | ||||
| ## Our Pledge | ||||
|  | ||||
| In the interest of fostering an open and welcoming environment, we as | ||||
| contributors and maintainers pledge to making participation in our project and | ||||
| our community a harassment-free experience for everyone, regardless of age, body | ||||
| size, disability, ethnicity, sex characteristics, gender identity and expression, | ||||
| level of experience, education, socio-economic status, nationality, personal | ||||
| appearance, race, religion, or sexual identity and orientation. | ||||
|  | ||||
| ## Our Standards | ||||
|  | ||||
| Examples of behavior that contributes to creating a positive environment | ||||
| include: | ||||
|  | ||||
| * Using welcoming and inclusive language | ||||
| * Being respectful of differing viewpoints and experiences | ||||
| * Gracefully accepting constructive criticism | ||||
| * Focusing on what is best for the community | ||||
| * Showing empathy towards other community members | ||||
|  | ||||
| Examples of unacceptable behavior by participants include: | ||||
|  | ||||
| * The use of sexualized language or imagery and unwelcome sexual attention or | ||||
|  advances | ||||
| * Trolling, insulting/derogatory comments, and personal or political attacks | ||||
| * Public or private harassment | ||||
| * Publishing others' private information, such as a physical or electronic | ||||
|  address, without explicit permission | ||||
| * Other conduct which could reasonably be considered inappropriate in a | ||||
|  professional setting | ||||
|  | ||||
| ## Our Responsibilities | ||||
|  | ||||
| Project maintainers are responsible for clarifying the standards of acceptable | ||||
| behavior and are expected to take appropriate and fair corrective action in | ||||
| response to any instances of unacceptable behavior. | ||||
|  | ||||
| Project maintainers have the right and responsibility to remove, edit, or | ||||
| reject comments, commits, code, wiki edits, issues, and other contributions | ||||
| that are not aligned to this Code of Conduct, or to ban temporarily or | ||||
| permanently any contributor for other behaviors that they deem inappropriate, | ||||
| threatening, offensive, or harmful. | ||||
|  | ||||
| ## Scope | ||||
|  | ||||
| This Code of Conduct applies both within project spaces and in public spaces | ||||
| when an individual is representing the project or its community. Examples of | ||||
| representing a project or community include using an official project e-mail | ||||
| address, posting via an official social media account, or acting as an appointed | ||||
| representative at an online or offline event. Representation of a project may be | ||||
| further defined and clarified by project maintainers. | ||||
|  | ||||
| ## Enforcement | ||||
|  | ||||
| Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||||
| reported by contacting the project team at vyalov.egor@gmail.com. All | ||||
| complaints will be reviewed and investigated and will result in a response that | ||||
| is deemed necessary and appropriate to the circumstances. The project team is | ||||
| obligated to maintain confidentiality with regard to the reporter of an incident. | ||||
| Further details of specific enforcement policies may be posted separately. | ||||
|  | ||||
| Project maintainers who do not follow or enforce the Code of Conduct in good | ||||
| faith may face temporary or permanent repercussions as determined by other | ||||
| members of the project's leadership. | ||||
|  | ||||
| ## Attribution | ||||
|  | ||||
| This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, | ||||
| available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html | ||||
|  | ||||
| [homepage]: https://www.contributor-covenant.org | ||||
|  | ||||
| For answers to common questions about this code of conduct, see | ||||
| https://www.contributor-covenant.org/faq | ||||
| @@ -1,13 +1,12 @@ | ||||
| [](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart) | ||||
| # HA Client | ||||
| ## Native Android client for Home Assistant | ||||
| ### With Lovelace UI support | ||||
|  | ||||
| Home Assistant Android client on Dart with Flutter. | ||||
|  | ||||
| Visit [www.keyboardcrumbs.io](http://www.keyboardcrumbs.io/ha-client) for more info. | ||||
| Visit [homemade.systems](http://ha-client.homemade.systems/) for more info. | ||||
|  | ||||
| Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester | ||||
|  | ||||
| Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group | ||||
|  | ||||
| Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or in [Discord](https://discord.gg/NSaQEQ8) | ||||
| Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) | ||||
|   | ||||
| @@ -29,7 +29,12 @@ def keystoreProperties = new Properties() | ||||
| keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) | ||||
|  | ||||
| android { | ||||
|     compileSdkVersion 27 | ||||
|     compileSdkVersion 28 | ||||
|  | ||||
|     compileOptions { | ||||
|         sourceCompatibility JavaVersion.VERSION_1_8 | ||||
|         targetCompatibility JavaVersion.VERSION_1_8 | ||||
|     } | ||||
|  | ||||
|     lintOptions { | ||||
|         disable 'InvalidPackage' | ||||
| @@ -38,7 +43,7 @@ android { | ||||
|     defaultConfig { | ||||
|         applicationId "com.keyboardcrumbs.haclient" | ||||
|         minSdkVersion 21 | ||||
|         targetSdkVersion 27 | ||||
|         targetSdkVersion 28 | ||||
|         versionCode flutterVersionCode.toInteger() | ||||
|         versionName flutterVersionName | ||||
|         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | ||||
| @@ -65,7 +70,10 @@ flutter { | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation 'com.google.firebase:firebase-core:16.0.8' | ||||
|     testImplementation 'junit:junit:4.12' | ||||
|     androidTestImplementation 'com.android.support.test:runner:1.0.2' | ||||
|     androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' | ||||
| } | ||||
|  | ||||
| apply plugin: 'com.google.gms.google-services' | ||||
|   | ||||
							
								
								
									
										42
									
								
								android/app/google-services.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| { | ||||
|   "project_info": { | ||||
|     "project_number": "441874387819", | ||||
|     "firebase_url": "https://ha-client-c73c4.firebaseio.com", | ||||
|     "project_id": "ha-client-c73c4", | ||||
|     "storage_bucket": "ha-client-c73c4.appspot.com" | ||||
|   }, | ||||
|   "client": [ | ||||
|     { | ||||
|       "client_info": { | ||||
|         "mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45", | ||||
|         "android_client_info": { | ||||
|           "package_name": "com.keyboardcrumbs.haclient" | ||||
|         } | ||||
|       }, | ||||
|       "oauth_client": [ | ||||
|         { | ||||
|           "client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com", | ||||
|           "client_type": 3 | ||||
|         } | ||||
|       ], | ||||
|       "api_key": [ | ||||
|         { | ||||
|           "current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU" | ||||
|         } | ||||
|       ], | ||||
|       "services": { | ||||
|         "analytics_service": { | ||||
|           "status": 1 | ||||
|         }, | ||||
|         "appinvite_service": { | ||||
|           "status": 1, | ||||
|           "other_platform_oauth_client": [] | ||||
|         }, | ||||
|         "ads_service": { | ||||
|           "status": 2 | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "configuration_version": "1" | ||||
| } | ||||
| @@ -15,7 +15,8 @@ | ||||
|     <application | ||||
|         android:name="io.flutter.app.FlutterApplication" | ||||
|         android:label="HA Client" | ||||
|         android:icon="@mipmap/ic_launcher"> | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:usesCleartextTraffic="true"> | ||||
|         <activity | ||||
|             android:name=".MainActivity" | ||||
|             android:launchMode="singleTop" | ||||
| @@ -30,6 +31,10 @@ | ||||
|             <meta-data | ||||
|                 android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" | ||||
|                 android:value="true" /> | ||||
|             <intent-filter> | ||||
|                 <action android:name="FLUTTER_NOTIFICATION_CLICK" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN"/> | ||||
|                 <category android:name="android.intent.category.LAUNCHER"/> | ||||
|   | ||||
| Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 4.0 KiB | 
| Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.6 KiB | 
| Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.4 KiB | 
| Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 8.0 KiB | 
| Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 11 KiB | 
| @@ -5,7 +5,8 @@ buildscript { | ||||
|     } | ||||
|  | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:3.1.2' | ||||
|         classpath 'com.android.tools.build:gradle:3.3.2' | ||||
|         classpath 'com.google.gms:google-services:4.2.0' | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1 +1,5 @@ | ||||
| org.gradle.jvmargs=-Xmx1536M | ||||
| org.gradle.jvmargs=-Xmx2g | ||||
| org.gradle.daemon=true | ||||
| org.gradle.caching=true | ||||
| android.useAndroidX=true | ||||
| android.enableJetifier=true | ||||
| @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip | ||||
|   | ||||
							
								
								
									
										0
									
								
								android/gradlew
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								docs/empty
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/ha_access_tokens.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 23 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/ha_profile-300x247.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/settings-869x1024.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 102 KiB | 
							
								
								
									
										
											BIN
										
									
								
								fonts/materialdesignicons-webfont-3-5-95.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 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; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -19,8 +19,8 @@ class _EntityViewPageState extends State<EntityViewPage> { | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { | ||||
|       TheLogger.debug("State change event handled by entity page: ${event.entityId}"); | ||||
|       if (event.entityId == widget.entityId) { | ||||
|         Logger.d("State change event handled by entity page: ${event.entityId}"); | ||||
|         setState(() {}); | ||||
|       } | ||||
|     }); | ||||
| @@ -46,12 +46,9 @@ class _EntityViewPageState extends State<EntityViewPage> { | ||||
|         // the App.build method, and use it to set our appbar title. | ||||
|         title: new Text(_title), | ||||
|       ), | ||||
|       body: Padding( | ||||
|           padding: EdgeInsets.all(10.0), | ||||
|           child: HomeAssistantModel( | ||||
|       body: HomeAssistantModel( | ||||
|           homeAssistant: widget.homeAssistant, | ||||
|           child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context) | ||||
|           ) | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										13
									
								
								lib/entity_class/alarm_control_panel.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class AlarmControlPanelEntity extends Entity { | ||||
|   AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return AlarmControlPanelControlsWidget( | ||||
|       extended: false, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										27
									
								
								lib/entity_class/automation_entity.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class AutomationEntity extends Entity { | ||||
|   AutomationEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return SwitchStateWidget(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return Row( | ||||
|       mainAxisAlignment: MainAxisAlignment.center, | ||||
|       mainAxisSize: MainAxisSize.max, | ||||
|       children: <Widget>[ | ||||
|         FlatServiceButton( | ||||
|           serviceDomain: domain, | ||||
|           entityId: entityId, | ||||
|           text: "TRIGGER", | ||||
|           serviceName: "trigger", | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,10 +1,16 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class ButtonEntity extends Entity { | ||||
|   ButtonEntity(Map rawData) : super(rawData); | ||||
|   ButtonEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return ButtonStateWidget(); | ||||
|     return FlatServiceButton( | ||||
|       entityId: entityId, | ||||
|       serviceDomain: domain, | ||||
|       serviceName: 'turn_on', | ||||
|       text: domain == "scene" ? "ACTIVATE" : "EXECUTE", | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								lib/entity_class/camera_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class CameraEntity extends Entity { | ||||
|  | ||||
|   static const SUPPORT_ON_OFF = 1; | ||||
|  | ||||
|   CameraEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportOnOff => ((supportedFeatures & | ||||
|   CameraEntity.SUPPORT_ON_OFF) == | ||||
|       CameraEntity.SUPPORT_ON_OFF); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return CameraStreamView(); | ||||
|   } | ||||
| } | ||||
| @@ -23,44 +23,46 @@ class ClimateEntity extends Entity { | ||||
|   static const SUPPORT_AUX_HEAT = 2048; | ||||
|   static const SUPPORT_ON_OFF = 4096; | ||||
|  | ||||
|   bool get supportTargetTemperature => ((attributes["supported_features"] & | ||||
|   ClimateEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportTargetTemperature => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_TEMPERATURE) == | ||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE); | ||||
|   bool get supportTargetTemperatureHigh => ((attributes["supported_features"] & | ||||
|   bool get supportTargetTemperatureHigh => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) == | ||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH); | ||||
|   bool get supportTargetTemperatureLow => ((attributes["supported_features"] & | ||||
|   bool get supportTargetTemperatureLow => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) == | ||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW); | ||||
|   bool get supportTargetHumidity => ((attributes["supported_features"] & | ||||
|   bool get supportTargetHumidity => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_HUMIDITY) == | ||||
|       ClimateEntity.SUPPORT_TARGET_HUMIDITY); | ||||
|   bool get supportTargetHumidityHigh => ((attributes["supported_features"] & | ||||
|   bool get supportTargetHumidityHigh => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) == | ||||
|       ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH); | ||||
|   bool get supportTargetHumidityLow => ((attributes["supported_features"] & | ||||
|   bool get supportTargetHumidityLow => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) == | ||||
|       ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW); | ||||
|   bool get supportFanMode => | ||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) == | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) == | ||||
|           ClimateEntity.SUPPORT_FAN_MODE); | ||||
|   bool get supportOperationMode => ((attributes["supported_features"] & | ||||
|   bool get supportOperationMode => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_OPERATION_MODE) == | ||||
|       ClimateEntity.SUPPORT_OPERATION_MODE); | ||||
|   bool get supportHoldMode => | ||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) == | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) == | ||||
|           ClimateEntity.SUPPORT_HOLD_MODE); | ||||
|   bool get supportSwingMode => | ||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) == | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) == | ||||
|           ClimateEntity.SUPPORT_SWING_MODE); | ||||
|   bool get supportAwayMode => | ||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) == | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) == | ||||
|           ClimateEntity.SUPPORT_AWAY_MODE); | ||||
|   bool get supportAuxHeat => | ||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) == | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) == | ||||
|           ClimateEntity.SUPPORT_AUX_HEAT); | ||||
|   bool get supportOnOff => | ||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) == | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) == | ||||
|           ClimateEntity.SUPPORT_ON_OFF); | ||||
|  | ||||
|   List<String> get operationList => attributes["operation_list"] != null | ||||
| @@ -80,6 +82,7 @@ class ClimateEntity extends Entity { | ||||
|   double get targetHumidity => _getDoubleAttributeValue('humidity'); | ||||
|   double get maxHumidity => _getDoubleAttributeValue('max_humidity'); | ||||
|   double get minHumidity => _getDoubleAttributeValue('min_humidity'); | ||||
|   double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5; | ||||
|   String get operationMode => attributes['operation_mode']; | ||||
|   String get fanMode => attributes['fan_mode']; | ||||
|   String get swingMode => attributes['swing_mode']; | ||||
| @@ -87,11 +90,9 @@ class ClimateEntity extends Entity { | ||||
|   bool get isOff => state == EntityState.off; | ||||
|   bool get auxHeat => attributes['aux_heat'] == "on"; | ||||
|  | ||||
|   ClimateEntity(Map rawData) : super(rawData); | ||||
|  | ||||
|   @override | ||||
|   void update(Map rawData) { | ||||
|     super.update(rawData); | ||||
|   void update(Map rawData, String webHost) { | ||||
|     super.update(rawData, webHost); | ||||
|     if (supportTargetTemperature) { | ||||
|       historyConfig.numericAttributesToShow.add("temperature"); | ||||
|     } | ||||
|   | ||||
| @@ -28,16 +28,57 @@ class EntityState { | ||||
|   static const unavailable = 'unavailable'; | ||||
|   static const ok = 'ok'; | ||||
|   static const problem = 'problem'; | ||||
|   static const active = 'active'; | ||||
| } | ||||
|  | ||||
| class EntityTapAction { | ||||
| class EntityUIAction { | ||||
|   static const moreInfo = 'more-info'; | ||||
|   static const toggle = 'toggle'; | ||||
|   static const callService = 'call-service'; | ||||
|   static const navigate = 'navigate'; | ||||
|   static const none = 'none'; | ||||
|  | ||||
|   String tapAction = EntityUIAction.moreInfo; | ||||
|   String tapNavigationPath; | ||||
|   String tapService; | ||||
|   Map<String, dynamic> tapServiceData; | ||||
|   String holdAction = EntityUIAction.none; | ||||
|   String holdNavigationPath; | ||||
|   String holdService; | ||||
|   Map<String, dynamic> holdServiceData; | ||||
|  | ||||
|   EntityUIAction({rawEntityData}) { | ||||
|     if (rawEntityData != null) { | ||||
|       if (rawEntityData["tap_action"] != null) { | ||||
|         if (rawEntityData["tap_action"] is String) { | ||||
|           tapAction = rawEntityData["tap_action"]; | ||||
|         } else { | ||||
|           tapAction = | ||||
|               rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo; | ||||
|           tapNavigationPath = rawEntityData["tap_action"]["navigation_path"]; | ||||
|           tapService = rawEntityData["tap_action"]["service"]; | ||||
|           tapServiceData = rawEntityData["tap_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|       if (rawEntityData["hold_action"] != null) { | ||||
|         if (rawEntityData["hold_action"] is String) { | ||||
|           holdAction = rawEntityData["hold_action"]; | ||||
|         } else { | ||||
|           holdAction = | ||||
|               rawEntityData["hold_action"]["action"] ?? EntityUIAction.none; | ||||
|           holdNavigationPath = rawEntityData["hold_action"]["navigation_path"]; | ||||
|           holdService = rawEntityData["hold_action"]["service"]; | ||||
|           holdServiceData = rawEntityData["hold_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class CardType { | ||||
|   static const horizontalStack = "horizontal-stack"; | ||||
|   static const verticalStack = "vertical-stack"; | ||||
|   static const entities = "entities"; | ||||
|   static const glance = "glance"; | ||||
|   static const mediaControl = "media-control"; | ||||
| @@ -54,4 +95,5 @@ class CardType { | ||||
|   static const entityButton = "entity-button"; | ||||
|   static const conditional = "conditional"; | ||||
|   static const alarmPanel = "alarm-panel"; | ||||
|   static const markdown = "markdown"; | ||||
| } | ||||
| @@ -11,29 +11,31 @@ class CoverEntity extends Entity { | ||||
|   static const SUPPORT_STOP_TILT = 64; | ||||
|   static const SUPPORT_SET_TILT_POSITION = 128; | ||||
|  | ||||
|   bool get supportOpen => ((attributes["supported_features"] & | ||||
|   CoverEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportOpen => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_OPEN) == | ||||
|       CoverEntity.SUPPORT_OPEN); | ||||
|   bool get supportClose => ((attributes["supported_features"] & | ||||
|   bool get supportClose => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_CLOSE) == | ||||
|       CoverEntity.SUPPORT_CLOSE); | ||||
|   bool get supportSetPosition => ((attributes["supported_features"] & | ||||
|   bool get supportSetPosition => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_SET_POSITION) == | ||||
|       CoverEntity.SUPPORT_SET_POSITION); | ||||
|   bool get supportStop => ((attributes["supported_features"] & | ||||
|   bool get supportStop => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_STOP) == | ||||
|       CoverEntity.SUPPORT_STOP); | ||||
|  | ||||
|   bool get supportOpenTilt => ((attributes["supported_features"] & | ||||
|   bool get supportOpenTilt => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_OPEN_TILT) == | ||||
|       CoverEntity.SUPPORT_OPEN_TILT); | ||||
|   bool get supportCloseTilt => ((attributes["supported_features"] & | ||||
|   bool get supportCloseTilt => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_CLOSE_TILT) == | ||||
|       CoverEntity.SUPPORT_CLOSE_TILT); | ||||
|   bool get supportStopTilt => ((attributes["supported_features"] & | ||||
|   bool get supportStopTilt => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_STOP_TILT) == | ||||
|       CoverEntity.SUPPORT_STOP_TILT); | ||||
|   bool get supportSetTiltPosition => ((attributes["supported_features"] & | ||||
|   bool get supportSetTiltPosition => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_SET_TILT_POSITION) == | ||||
|       CoverEntity.SUPPORT_SET_TILT_POSITION); | ||||
|  | ||||
| @@ -45,8 +47,6 @@ class CoverEntity extends Entity { | ||||
|   bool get canTiltBeOpened => currentTiltPosition < 100; | ||||
|   bool get canTiltBeClosed => currentTiltPosition > 0; | ||||
|  | ||||
|   CoverEntity(Map rawData) : super(rawData); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return CoverStateWidget(); | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class DateTimeEntity extends Entity { | ||||
|   DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get hasDate => attributes["has_date"] ?? false; | ||||
|   bool get hasTime => attributes["has_time"] ?? false; | ||||
|   int get year => attributes["year"] ?? 1970; | ||||
| @@ -12,8 +14,6 @@ class DateTimeEntity extends Entity { | ||||
|   String get formattedState => _getFormattedState(); | ||||
|   DateTime get dateTimeState => _getDateTimeState(); | ||||
|  | ||||
|   DateTimeEntity(Map rawData) : super(rawData); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return DateTimeStateWidget(); | ||||
|   | ||||
| @@ -1,5 +1,14 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class StatelessEntityType { | ||||
|   static const NONE = 0; | ||||
|   static const MISSED = 1; | ||||
|   static const DIVIDER = 2; | ||||
|   static const SECTION = 3; | ||||
|   static const CALL_SERVICE = 4; | ||||
|   static const WEBLINK = 5; | ||||
| } | ||||
|  | ||||
| class Entity { | ||||
|  | ||||
|   static List badgeDomains = [ | ||||
| @@ -12,14 +21,66 @@ class Entity { | ||||
|     "sensor" | ||||
|   ]; | ||||
|  | ||||
|   static Map StateByDeviceClass = { | ||||
|     "battery.on": "Low", | ||||
|     "battery.off": "Normal", | ||||
|     "cold.on": "Cold", | ||||
|     "cold.off": "Normal", | ||||
|     "connectivity.on": "Connected", | ||||
|     "connectivity.off": "Diconnected", | ||||
|     "door.on": "Open", | ||||
|     "door.off": "Closed", | ||||
|     "garage_door.on": "Open", | ||||
|     "garage_door.off": "Closed", | ||||
|     "gas.on": "Detected", | ||||
|     "gas.off": "Clear", | ||||
|     "heat.on": "Hot", | ||||
|     "heat.off": "Normal", | ||||
|     "light.on": "Detected", | ||||
|     "lignt.off": "No light", | ||||
|     "lock.on": "Unlocked", | ||||
|     "lock.off": "Locked", | ||||
|     "moisture.on": "Wet", | ||||
|     "moisture.off": "Dry", | ||||
|     "motion.on": "Detected", | ||||
|     "motion.off": "Clear", | ||||
|     "moving.on": "Moving", | ||||
|     "moving.off": "Stopped", | ||||
|     "occupancy.on": "Occupied", | ||||
|     "occupancy.off": "Clear", | ||||
|     "opening.on": "Open", | ||||
|     "opening.off": "Closed", | ||||
|     "plug.on": "Plugged in", | ||||
|     "plug.off": "Unplugged", | ||||
|     "power.on": "Powered", | ||||
|     "power.off": "No power", | ||||
|     "presence.on": "Home", | ||||
|     "presence.off": "Away", | ||||
|     "problem.on": "Problem", | ||||
|     "problem.off": "OK", | ||||
|     "safety.on": "Unsafe", | ||||
|     "safety.off": "Safe", | ||||
|     "smoke.on": "Detected", | ||||
|     "smoke.off": "Clear", | ||||
|     "sound.on": "Detected", | ||||
|     "sound.off": "Clear", | ||||
|     "vibration.on": "Detected", | ||||
|     "vibration.off": "Clear", | ||||
|     "window.on": "Open", | ||||
|     "window.off": "Closed" | ||||
|   }; | ||||
|  | ||||
|   Map attributes; | ||||
|   String domain; | ||||
|   String entityId; | ||||
|   String entityPicture; | ||||
|   String state; | ||||
|   String displayState; | ||||
|   DateTime _lastUpdated; | ||||
|   int statelessType = 0; | ||||
|  | ||||
|   List<Entity> childEntities = []; | ||||
|   List<String> attributesToShow = ["all"]; | ||||
|   String deviceClass; | ||||
|   EntityHistoryConfig historyConfig = EntityHistoryConfig( | ||||
|     chartType: EntityHistoryWidgetType.simple | ||||
|   ); | ||||
| @@ -27,7 +88,6 @@ class Entity { | ||||
|   String get displayName => | ||||
|       attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " ")); | ||||
|  | ||||
|   String get deviceClass => attributes["device_class"] ?? null; | ||||
|   bool get isView => | ||||
|       (domain == "group") && | ||||
|       (attributes != null ? attributes["view"] ?? false : false); | ||||
| @@ -35,23 +95,68 @@ class Entity { | ||||
|   bool get isBadge => Entity.badgeDomains.contains(domain); | ||||
|   String get icon => attributes["icon"] ?? ""; | ||||
|   bool get isOn => state == EntityState.on; | ||||
|   String get entityPicture => attributes["entity_picture"]; | ||||
|   String get unitOfMeasurement => attributes["unit_of_measurement"] ?? ""; | ||||
|   List get childEntityIds => attributes["entity_id"] ?? []; | ||||
|   String get lastUpdated => _getLastUpdatedFormatted(); | ||||
|   bool get isHidden => attributes["hidden"] ?? false; | ||||
|   double get doubleState => double.tryParse(state) ?? 0.0; | ||||
|   int get supportedFeatures => attributes["supported_features"] ?? 0; | ||||
|  | ||||
|   Entity(Map rawData) { | ||||
|     update(rawData); | ||||
|   String _getEntityPictureUrl(String webHost) { | ||||
|     String result = attributes["entity_picture"]; | ||||
|     if (result == null) return result; | ||||
|     if (!result.startsWith("http")) { | ||||
|       if (result.startsWith("/")) { | ||||
|         result = "$webHost$result"; | ||||
|       } else { | ||||
|         result = "$webHost/$result"; | ||||
|       } | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   void update(Map rawData) { | ||||
|   Entity(Map rawData, String webHost) { | ||||
|     update(rawData, webHost); | ||||
|   } | ||||
|  | ||||
|   Entity.missed(String entityId) { | ||||
|     statelessType = StatelessEntityType.MISSED; | ||||
|     attributes = {"hidden": false}; | ||||
|     this.entityId = entityId; | ||||
|   } | ||||
|  | ||||
|   Entity.divider() { | ||||
|     statelessType = StatelessEntityType.DIVIDER; | ||||
|     attributes = {"hidden": false}; | ||||
|   } | ||||
|  | ||||
|   Entity.section(String label) { | ||||
|     statelessType = StatelessEntityType.SECTION; | ||||
|     attributes = {"hidden": false, "friendly_name": "$label"}; | ||||
|   } | ||||
|  | ||||
|   Entity.callService({String icon, String name, String service, String actionName}) { | ||||
|     statelessType = StatelessEntityType.CALL_SERVICE; | ||||
|     entityId = service; | ||||
|     displayState = actionName?.toUpperCase() ?? "RUN"; | ||||
|     attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"}; | ||||
|   } | ||||
|  | ||||
|   Entity.weblink({String url, String name, String icon}) { | ||||
|     statelessType = StatelessEntityType.WEBLINK; | ||||
|     entityId = "custom.custom"; //TODO wtf?? | ||||
|     attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; | ||||
|   } | ||||
|  | ||||
|   void update(Map rawData, String webHost) { | ||||
|     attributes = rawData["attributes"] ?? {}; | ||||
|     domain = rawData["entity_id"].split(".")[0]; | ||||
|     entityId = rawData["entity_id"]; | ||||
|     deviceClass = attributes["device_class"]; | ||||
|     state = rawData["state"]; | ||||
|     displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state; | ||||
|     _lastUpdated = DateTime.tryParse(rawData["last_updated"]); | ||||
|     entityPicture = _getEntityPictureUrl(webHost); | ||||
|   } | ||||
|  | ||||
|   double _getDoubleAttributeValue(String attributeName) { | ||||
| @@ -91,13 +196,6 @@ class Entity { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget buildGlanceWidget(BuildContext context, bool showName, bool showState) { | ||||
|     return GlanceEntityContainer( | ||||
|       showName: showName, | ||||
|       showState: showState, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return SimpleEntityState(); | ||||
|   } | ||||
| @@ -117,7 +215,10 @@ class Entity { | ||||
|     return EntityModel( | ||||
|       entityWrapper: EntityWrapper(entity: this), | ||||
|       child: EntityPageContainer(children: <Widget>[ | ||||
|         DefaultEntityContainer(state: _buildStatePartForPage(context)), | ||||
|         Padding( | ||||
|           padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||
|           child: DefaultEntityContainer(state: _buildStatePartForPage(context)), | ||||
|         ), | ||||
|         LastUpdatedWidget(), | ||||
|         Divider(), | ||||
|         _buildAdditionalControlsForPage(context), | ||||
|   | ||||
| @@ -4,12 +4,8 @@ class EntityWrapper { | ||||
|  | ||||
|   String displayName; | ||||
|   String icon; | ||||
|   String tapAction; | ||||
|   String holdAction; | ||||
|   String tapActionService; | ||||
|   Map<String, dynamic> tapActionServiceData; | ||||
|   String holdActionService; | ||||
|   Map<String, dynamic> holdActionServiceData; | ||||
|   String entityPicture; | ||||
|   EntityUIAction uiAction; | ||||
|   Entity entity; | ||||
|  | ||||
|  | ||||
| @@ -17,63 +13,98 @@ class EntityWrapper { | ||||
|     this.entity, | ||||
|     String icon, | ||||
|     String displayName, | ||||
|     this.tapAction: EntityTapAction.moreInfo, | ||||
|     this.holdAction: EntityTapAction.none, | ||||
|     this.tapActionService, | ||||
|     this.tapActionServiceData, | ||||
|     this.holdActionService, | ||||
|     this.holdActionServiceData | ||||
|     this.uiAction | ||||
|   }) { | ||||
|     if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) { | ||||
|       this.icon = icon ?? entity.icon; | ||||
|       if (icon == null) { | ||||
|         entityPicture = entity.entityPicture; | ||||
|       } | ||||
|       this.displayName = displayName ?? entity.displayName; | ||||
|       if (uiAction == null) { | ||||
|         uiAction = EntityUIAction(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void handleTap() { | ||||
|     switch (tapAction) { | ||||
|       case EntityTapAction.toggle: { | ||||
|     switch (uiAction.tapAction) { | ||||
|       case EntityUIAction.toggle: { | ||||
|         eventBus.fire( | ||||
|             ServiceCallEvent("homeassistant", "toggle", entity.entityId, null)); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case EntityTapAction.callService: { | ||||
|       case EntityUIAction.callService: { | ||||
|         if (uiAction.tapService != null) { | ||||
|           eventBus.fire( | ||||
|             ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData)); | ||||
|               ServiceCallEvent(uiAction.tapService.split(".")[0], | ||||
|                   uiAction.tapService.split(".")[1], null, | ||||
|                   uiAction.tapServiceData)); | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case EntityTapAction.none: { | ||||
|       case EntityUIAction.none: { | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case EntityUIAction.moreInfo: { | ||||
|         eventBus.fire( | ||||
|             new ShowEntityPageEvent(entity)); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case EntityUIAction.navigate: { | ||||
|         if (uiAction.tapService.startsWith("/")) { | ||||
|           //TODO handle local urls | ||||
|           Logger.w("Local urls is not supported yet"); | ||||
|         } else { | ||||
|           HAUtils.launchURL(uiAction.tapService); | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       default: { | ||||
|         eventBus.fire( | ||||
|             new ShowEntityPageEvent(entity)); | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void handleHold() { | ||||
|       switch (holdAction) { | ||||
|         case EntityTapAction.toggle: { | ||||
|       switch (uiAction.holdAction) { | ||||
|         case EntityUIAction.toggle: { | ||||
|           eventBus.fire( | ||||
|               ServiceCallEvent("homeassistant", "toggle", entity.entityId, null)); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case EntityTapAction.callService: { | ||||
|         case EntityUIAction.callService: { | ||||
|           if (uiAction.holdService != null) { | ||||
|             eventBus.fire( | ||||
|               ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData)); | ||||
|                 ServiceCallEvent(uiAction.holdService.split(".")[0], | ||||
|                     uiAction.holdService.split(".")[1], null, | ||||
|                     uiAction.holdServiceData)); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case EntityTapAction.moreInfo: { | ||||
|         case EntityUIAction.moreInfo: { | ||||
|           eventBus.fire( | ||||
|               new ShowEntityPageEvent(entity)); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case EntityUIAction.navigate: { | ||||
|           if (uiAction.holdService.startsWith("/")) { | ||||
|             //TODO handle local urls | ||||
|             Logger.w("Local urls is not supported yet"); | ||||
|           } else { | ||||
|             HAUtils.launchURL(uiAction.holdService); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         default: { | ||||
|           break; | ||||
|         } | ||||
|   | ||||
| @@ -6,15 +6,15 @@ class FanEntity extends Entity { | ||||
|   static const SUPPORT_OSCILLATE = 2; | ||||
|   static const SUPPORT_DIRECTION = 4; | ||||
|  | ||||
|   FanEntity(Map rawData) : super(rawData); | ||||
|   FanEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportSetSpeed => ((attributes["supported_features"] & | ||||
|   bool get supportSetSpeed => ((supportedFeatures & | ||||
|   FanEntity.SUPPORT_SET_SPEED) == | ||||
|       FanEntity.SUPPORT_SET_SPEED); | ||||
|   bool get supportOscillate => ((attributes["supported_features"] & | ||||
|   bool get supportOscillate => ((supportedFeatures & | ||||
|   FanEntity.SUPPORT_OSCILLATE) == | ||||
|       FanEntity.SUPPORT_OSCILLATE); | ||||
|   bool get supportDirection => ((attributes["supported_features"] & | ||||
|   bool get supportDirection => ((supportedFeatures & | ||||
|   FanEntity.SUPPORT_DIRECTION) == | ||||
|       FanEntity.SUPPORT_DIRECTION); | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class GroupEntity extends Entity { | ||||
|   GroupEntity(Map rawData) : super(rawData); | ||||
|  | ||||
|   final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"]; | ||||
|   String mutualDomain; | ||||
|   bool switchable = false; | ||||
|  | ||||
|   GroupEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     if (switchable) { | ||||
| @@ -19,8 +20,8 @@ class GroupEntity extends Entity { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void update(Map rawData) { | ||||
|     super.update(rawData); | ||||
|   void update(Map rawData, String webHost) { | ||||
|     super.update(rawData, webHost); | ||||
|     if (_isOneDomain()) { | ||||
|       mutualDomain = attributes['entity_id'][0].split(".")[0]; | ||||
|       switchable = _domainsForSwitchableGroup.contains(mutualDomain); | ||||
|   | ||||
| @@ -10,43 +10,50 @@ class LightEntity extends Entity { | ||||
|   static const SUPPORT_TRANSITION = 32; | ||||
|   static const SUPPORT_WHITE_VALUE = 128; | ||||
|  | ||||
|   bool get supportBrightness => ((attributes["supported_features"] & | ||||
|   bool get supportBrightness => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_BRIGHTNESS) == | ||||
|       LightEntity.SUPPORT_BRIGHTNESS); | ||||
|   bool get supportColorTemp => ((attributes["supported_features"] & | ||||
|   bool get supportColorTemp => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_COLOR_TEMP) == | ||||
|       LightEntity.SUPPORT_COLOR_TEMP); | ||||
|   bool get supportEffect => ((attributes["supported_features"] & | ||||
|   bool get supportEffect => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_EFFECT) == | ||||
|       LightEntity.SUPPORT_EFFECT); | ||||
|   bool get supportFlash => ((attributes["supported_features"] & | ||||
|   bool get supportFlash => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_FLASH) == | ||||
|       LightEntity.SUPPORT_FLASH); | ||||
|   bool get supportColor => ((attributes["supported_features"] & | ||||
|   bool get supportColor => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_COLOR) == | ||||
|       LightEntity.SUPPORT_COLOR); | ||||
|   bool get supportTransition => ((attributes["supported_features"] & | ||||
|   bool get supportTransition => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_TRANSITION) == | ||||
|       LightEntity.SUPPORT_TRANSITION); | ||||
|   bool get supportWhiteValue => ((attributes["supported_features"] & | ||||
|   bool get supportWhiteValue => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_WHITE_VALUE) == | ||||
|       LightEntity.SUPPORT_WHITE_VALUE); | ||||
|  | ||||
|   int get brightness => _getIntAttributeValue("brightness"); | ||||
|   int get whiteValue => _getIntAttributeValue("white_value"); | ||||
|   String get effect => attributes["effect"]; | ||||
|   int get colorTemp => _getIntAttributeValue("color_temp"); | ||||
|   double get maxMireds => _getDoubleAttributeValue("max_mireds"); | ||||
|   double get minMireds => _getDoubleAttributeValue("min_mireds"); | ||||
|   Color get color => _getColor(); | ||||
|   bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0)); | ||||
|   HSVColor get color => _getColor(); | ||||
|   bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0)); | ||||
|   List<String> get effectList => getStringListAttributeValue("effect_list"); | ||||
|  | ||||
|   LightEntity(Map rawData) : super(rawData); | ||||
|   LightEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   Color _getColor() { | ||||
|   HSVColor _getColor() { | ||||
|     List hs = attributes["hs_color"]; | ||||
|     List rgb = attributes["rgb_color"]; | ||||
|     try { | ||||
|       if ((rgb != null) && (rgb.length > 0)) { | ||||
|         return Color.fromARGB(255, rgb[0], rgb[1], rgb[2]); | ||||
|       if (hs != null && hs.isNotEmpty) { | ||||
|         double sat = hs[1]/100; | ||||
|         String ssat = sat.toStringAsFixed(2); | ||||
|         return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0); | ||||
|       } else if (rgb != null && rgb.isNotEmpty) { | ||||
|         return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2])); | ||||
|       } else { | ||||
|         return null; | ||||
|       } | ||||
| @@ -62,7 +69,7 @@ class LightEntity extends Entity { | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     if (!isAdditionalControls) { | ||||
|     if (!isAdditionalControls || state == EntityState.unavailable) { | ||||
|       return Container(height: 0.0, width: 0.0); | ||||
|     } else { | ||||
|       return LightControlsWidget(); | ||||
|   | ||||
| @@ -1,12 +1,21 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class LockEntity extends Entity { | ||||
|   LockEntity(Map rawData) : super(rawData); | ||||
|   LockEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get isLocked => state == "locked"; | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return LockStateWidget(); | ||||
|     return LockStateWidget( | ||||
|       assumedState: false, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePartForPage(BuildContext context) { | ||||
|     return LockStateWidget( | ||||
|       assumedState: true, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -20,55 +20,55 @@ class MediaPlayerEntity extends Entity { | ||||
|   static const SUPPORT_SHUFFLE_SET = 32768; | ||||
|   static const SUPPORT_SELECT_SOUND_MODE = 65536; | ||||
|  | ||||
|   MediaPlayerEntity(Map rawData) : super(rawData); | ||||
|   MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportPause => ((attributes["supported_features"] & | ||||
|   bool get supportPause => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_PAUSE) == | ||||
|       MediaPlayerEntity.SUPPORT_PAUSE); | ||||
|   bool get supportSeek => ((attributes["supported_features"] & | ||||
|   bool get supportSeek => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_SEEK) == | ||||
|       MediaPlayerEntity.SUPPORT_SEEK); | ||||
|   bool get supportVolumeSet => ((attributes["supported_features"] & | ||||
|   bool get supportVolumeSet => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_VOLUME_SET) == | ||||
|       MediaPlayerEntity.SUPPORT_VOLUME_SET); | ||||
|   bool get supportVolumeMute => ((attributes["supported_features"] & | ||||
|   bool get supportVolumeMute => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_VOLUME_MUTE) == | ||||
|       MediaPlayerEntity.SUPPORT_VOLUME_MUTE); | ||||
|   bool get supportPreviousTrack => ((attributes["supported_features"] & | ||||
|   bool get supportPreviousTrack => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) == | ||||
|       MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK); | ||||
|   bool get supportNextTrack => ((attributes["supported_features"] & | ||||
|   bool get supportNextTrack => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_NEXT_TRACK) == | ||||
|       MediaPlayerEntity.SUPPORT_NEXT_TRACK); | ||||
|  | ||||
|   bool get supportTurnOn => ((attributes["supported_features"] & | ||||
|   bool get supportTurnOn => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_TURN_ON) == | ||||
|       MediaPlayerEntity.SUPPORT_TURN_ON); | ||||
|   bool get supportTurnOff => ((attributes["supported_features"] & | ||||
|   bool get supportTurnOff => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_TURN_OFF) == | ||||
|       MediaPlayerEntity.SUPPORT_TURN_OFF); | ||||
|   bool get supportPlayMedia => ((attributes["supported_features"] & | ||||
|   bool get supportPlayMedia => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_PLAY_MEDIA) == | ||||
|       MediaPlayerEntity.SUPPORT_PLAY_MEDIA); | ||||
|   bool get supportVolumeStep => ((attributes["supported_features"] & | ||||
|   bool get supportVolumeStep => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_VOLUME_STEP) == | ||||
|       MediaPlayerEntity.SUPPORT_VOLUME_STEP); | ||||
|   bool get supportSelectSource => ((attributes["supported_features"] & | ||||
|   bool get supportSelectSource => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_SELECT_SOURCE) == | ||||
|       MediaPlayerEntity.SUPPORT_SELECT_SOURCE); | ||||
|   bool get supportStop => ((attributes["supported_features"] & | ||||
|   bool get supportStop => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_STOP) == | ||||
|       MediaPlayerEntity.SUPPORT_STOP); | ||||
|   bool get supportClearPlaylist => ((attributes["supported_features"] & | ||||
|   bool get supportClearPlaylist => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) == | ||||
|       MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST); | ||||
|   bool get supportPlay => ((attributes["supported_features"] & | ||||
|   bool get supportPlay => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_PLAY) == | ||||
|       MediaPlayerEntity.SUPPORT_PLAY); | ||||
|   bool get supportShuffleSet => ((attributes["supported_features"] & | ||||
|   bool get supportShuffleSet => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_SHUFFLE_SET) == | ||||
|       MediaPlayerEntity.SUPPORT_SHUFFLE_SET); | ||||
|   bool get supportSelectSoundMode => ((attributes["supported_features"] & | ||||
|   bool get supportSelectSoundMode => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) == | ||||
|       MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE); | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class SunEntity extends Entity { | ||||
|   SunEntity(Map rawData) : super(rawData); | ||||
|   SunEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
| } | ||||
|  | ||||
| class SensorEntity extends Entity { | ||||
| @@ -12,6 +12,6 @@ class SensorEntity extends Entity { | ||||
|       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>() | ||||
|       : []; | ||||
|  | ||||
|   SelectEntity(Map rawData) : super(rawData); | ||||
|   SelectEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class SliderEntity extends Entity { | ||||
|   SliderEntity(Map rawData) : super(rawData); | ||||
|   SliderEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   double get minValue => _getDoubleAttributeValue("min") ?? 0.0; | ||||
|   double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class SwitchEntity extends Entity { | ||||
|   SwitchEntity(Map rawData) : super(rawData); | ||||
|   SwitchEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class TextEntity extends Entity { | ||||
|   TextEntity(Map rawData) : super(rawData); | ||||
|   TextEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   int get valueMinLength => attributes["min"] ?? -1; | ||||
|   int get valueMaxLength => attributes["max"] ?? -1; | ||||
|   | ||||
							
								
								
									
										45
									
								
								lib/entity_class/timer_entity.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,45 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class TimerEntity extends Entity { | ||||
|   TimerEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   Duration duration; | ||||
|  | ||||
|   @override | ||||
|   void update(Map rawData, String webHost) { | ||||
|     super.update(rawData, webHost); | ||||
|     String durationSource = "${attributes["duration"]}"; | ||||
|     if (durationSource != null && durationSource.isNotEmpty) { | ||||
|       try { | ||||
|         List<String> durationList = durationSource.split(":"); | ||||
|         if (durationList.length == 1) { | ||||
|           duration = Duration(seconds: int.tryParse(durationList[0] ?? 0)); | ||||
|         } else if (durationList.length == 2) { | ||||
|           duration = Duration( | ||||
|               hours: int.tryParse(durationList[0]) ?? 0, | ||||
|               minutes: int.tryParse(durationList[1]) ?? 0 | ||||
|           ); | ||||
|         } else if (durationList.length == 3) { | ||||
|           duration = Duration( | ||||
|               hours: int.tryParse(durationList[0]) ?? 0, | ||||
|               minutes: int.tryParse(durationList[1]) ?? 0, | ||||
|               seconds: int.tryParse(durationList[2]) ?? 0 | ||||
|           ); | ||||
|         } else { | ||||
|           Logger.e("Strange $entityId duration format: $durationSource"); | ||||
|           duration = Duration(seconds: 0); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         Logger.e("Error parsing duration for $entityId: ${e.toString()}"); | ||||
|         duration = Duration(seconds: 0); | ||||
|       } | ||||
|     } else { | ||||
|       duration = Duration(seconds: 0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return TimerState(); | ||||
|   } | ||||
| } | ||||
| @@ -2,13 +2,15 @@ part of 'main.dart'; | ||||
|  | ||||
| class EntityCollection { | ||||
|  | ||||
|   final homeAssistantWebHost; | ||||
|  | ||||
|   Map<String, Entity> _allEntities; | ||||
|   //Map<String, Entity> views; | ||||
|  | ||||
|   bool get isEmpty => _allEntities.isEmpty; | ||||
|   List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList(); | ||||
|  | ||||
|   EntityCollection() { | ||||
|   EntityCollection(this.homeAssistantWebHost) { | ||||
|     _allEntities = {}; | ||||
|     //views = {}; | ||||
|   } | ||||
| @@ -19,7 +21,7 @@ class EntityCollection { | ||||
|     _allEntities.clear(); | ||||
|     //views.clear(); | ||||
|  | ||||
|     TheLogger.debug("Parsing ${rawData.length} Home Assistant entities"); | ||||
|     Logger.d("Parsing ${rawData.length} Home Assistant entities"); | ||||
|     rawData.forEach((rawEntityData) { | ||||
|       addFromRaw(rawEntityData); | ||||
|     }); | ||||
| @@ -33,67 +35,85 @@ class EntityCollection { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void clear() { | ||||
|     _allEntities.clear(); | ||||
|   } | ||||
|  | ||||
|   Entity _createEntityInstance(rawEntityData) { | ||||
|     switch (rawEntityData["entity_id"].split(".")[0]) { | ||||
|       case 'sun': { | ||||
|         return SunEntity(rawEntityData); | ||||
|         return SunEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "media_player": { | ||||
|         return MediaPlayerEntity(rawEntityData); | ||||
|         return MediaPlayerEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case 'sensor': { | ||||
|         return SensorEntity(rawEntityData); | ||||
|         return SensorEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case 'lock': { | ||||
|         return LockEntity(rawEntityData); | ||||
|         return LockEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "automation": | ||||
|       case "automation": { | ||||
|         return AutomationEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|  | ||||
|       case "input_boolean": | ||||
|       case "switch": { | ||||
|         return SwitchEntity(rawEntityData); | ||||
|         return SwitchEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "light": { | ||||
|         return LightEntity(rawEntityData); | ||||
|         return LightEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "group": { | ||||
|         return GroupEntity(rawEntityData); | ||||
|         return GroupEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "script": | ||||
|       case "scene": { | ||||
|         return ButtonEntity(rawEntityData); | ||||
|         return ButtonEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "input_datetime": { | ||||
|         return DateTimeEntity(rawEntityData); | ||||
|         return DateTimeEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "input_select": { | ||||
|         return SelectEntity(rawEntityData); | ||||
|         return SelectEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "input_number": { | ||||
|         return SliderEntity(rawEntityData); | ||||
|         return SliderEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "input_text": { | ||||
|         return TextEntity(rawEntityData); | ||||
|         return TextEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "climate": { | ||||
|         return ClimateEntity(rawEntityData); | ||||
|         return ClimateEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "cover": { | ||||
|         return CoverEntity(rawEntityData); | ||||
|         return CoverEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "fan": { | ||||
|         return FanEntity(rawEntityData); | ||||
|         return FanEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "camera": { | ||||
|         return CameraEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "alarm_control_panel": { | ||||
|         return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "timer": { | ||||
|         return TimerEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       default: { | ||||
|         return Entity(rawEntityData); | ||||
|         return Entity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void updateState(Map rawStateData) { | ||||
|   bool updateState(Map rawStateData) { | ||||
|     if (isExist(rawStateData["entity_id"])) { | ||||
|       updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); | ||||
|       return false; | ||||
|     } else { | ||||
|       addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -101,14 +121,13 @@ class EntityCollection { | ||||
|     _allEntities[entity.entityId] = entity; | ||||
|   } | ||||
|  | ||||
|   Entity addFromRaw(Map rawEntityData) { | ||||
|   void addFromRaw(Map rawEntityData) { | ||||
|     Entity entity = _createEntityInstance(rawEntityData); | ||||
|     _allEntities[entity.entityId] = entity; | ||||
|     return entity; | ||||
|   } | ||||
|  | ||||
|   void updateFromRaw(Map rawEntityData) { | ||||
|     get("${rawEntityData["entity_id"]}")?.update(rawEntityData); | ||||
|     get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost); | ||||
|   } | ||||
|  | ||||
|   Entity get(String entityId) { | ||||
|   | ||||
							
								
								
									
										50
									
								
								lib/entity_widgets/button_entity_container.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class ButtonEntityContainer extends StatelessWidget { | ||||
|  | ||||
|   ButtonEntityContainer({ | ||||
|     Key key, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { | ||||
|       return MissedEntityWidget(); | ||||
|     } | ||||
|     if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|     return InkWell( | ||||
|       onTap: () => entityWrapper.handleTap(), | ||||
|       onLongPress: () => entityWrapper.handleHold(), | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|           FractionallySizedBox( | ||||
|             widthFactor: 0.4, | ||||
|             child: FittedBox( | ||||
|                 fit: BoxFit.fitHeight, | ||||
|                 child: EntityIcon( | ||||
|                   padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0), | ||||
|                   size: Sizes.iconSize, | ||||
|                 ) | ||||
|             ), | ||||
|           ), | ||||
|           _buildName() | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildName() { | ||||
|     return EntityName( | ||||
|       padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding), | ||||
|       textOverflow: TextOverflow.ellipsis, | ||||
|       maxLines: 3, | ||||
|       wordsWrap: true, | ||||
|       textAlign: TextAlign.center, | ||||
|       fontSize: Sizes.nameFontSize, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -14,16 +14,37 @@ class BadgeWidget extends StatelessWidget { | ||||
|         { | ||||
|           badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon" | ||||
|               ? Icon( | ||||
|             MaterialDesignIcons.createIconDataFromIconCode(0xf0dc), | ||||
|             MaterialDesignIcons.getIconDataFromIconCode(0xf0dc), | ||||
|             size: iconSize, | ||||
|           ) | ||||
|               : Icon( | ||||
|             MaterialDesignIcons.createIconDataFromIconCode(0xf5a8), | ||||
|             MaterialDesignIcons.getIconDataFromIconCode(0xf5a8), | ||||
|             size: iconSize, | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "sensor": | ||||
|       case "camera": | ||||
|       case "media_player": | ||||
|       case "binary_sensor": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|             padding: EdgeInsets.all(0.0), | ||||
|             size: iconSize, | ||||
|             color: Colors.black | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "device_tracker": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|               padding: EdgeInsets.all(0.0), | ||||
|               size: iconSize, | ||||
|               color: Colors.black | ||||
|           ); | ||||
|           onBadgeTextValue = entityModel.entityWrapper.entity.state; | ||||
|           break; | ||||
|         } | ||||
|       default: | ||||
|         { | ||||
|           onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement; | ||||
|           badgeIcon = Center( | ||||
| @@ -37,18 +58,6 @@ class BadgeWidget extends StatelessWidget { | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "device_tracker": | ||||
|         { | ||||
|           badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData( | ||||
|               entityModel.entityWrapper, iconSize, Colors.black); | ||||
|           onBadgeTextValue = entityModel.entityWrapper.entity.state; | ||||
|           break; | ||||
|         } | ||||
|       default: | ||||
|         { | ||||
|           badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData( | ||||
|               entityModel.entityWrapper, iconSize, Colors.black); | ||||
|         } | ||||
|     } | ||||
|     Widget onBadgeText; | ||||
|     if (onBadgeTextValue == null || onBadgeTextValue.length == 0) { | ||||
|   | ||||
							
								
								
									
										177
									
								
								lib/entity_widgets/common/camera_stream_view.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,177 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class CameraStreamView extends StatefulWidget { | ||||
|  | ||||
|   CameraStreamView({Key key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _CameraStreamViewState createState() => _CameraStreamViewState(); | ||||
| } | ||||
|  | ||||
| class _CameraStreamViewState extends State<CameraStreamView> { | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   CameraEntity _entity; | ||||
|   String _webHost; | ||||
|  | ||||
|   http.Client client; | ||||
|   http.StreamedResponse response; | ||||
|   List<int> binaryImage = []; | ||||
|   bool timeToStop = false; | ||||
|   Completer streamCompleter; | ||||
|   bool started = false; | ||||
|   bool useSVG = false; | ||||
|  | ||||
|   void _connect() async { | ||||
|     started = true; | ||||
|     timeToStop = false; | ||||
|     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 | ||||
|     http.Request request = new http.Request("GET", Uri.parse(streamUrl));  // create get request | ||||
|     Logger.d("[Sending] ==> $streamUrl"); | ||||
|     response = await client.send(request); | ||||
|     Logger.d("[Received] <== ${response.headers}"); | ||||
|     String frameBoundary = response.headers['content-type'].split('boundary=')[1]; | ||||
|     final int frameBoundarySize = frameBoundary.length; | ||||
|     List<int> primaryBuffer=[]; | ||||
|     int imageSizeStart = 59; | ||||
|     int imageSizeEnd = 0; | ||||
|     int imageStart = 0; | ||||
|     int imageSize = 0; | ||||
|     String strBuffer = ""; | ||||
|     String contentType = ""; | ||||
|     streamCompleter = Completer(); | ||||
|     response.stream.transform( | ||||
|         StreamTransformer.fromHandlers( | ||||
|           handleData: (data, sink) { | ||||
|             primaryBuffer.addAll(data); | ||||
|             imageStart = 0; | ||||
|             imageSizeEnd = 0; | ||||
|             if (primaryBuffer.length >= imageSizeStart + 10) { | ||||
|               contentType = utf8.decode( | ||||
|                   primaryBuffer.sublist(frameBoundarySize+16, imageSizeStart + 10), allowMalformed: true).split("\r\n")[0]; | ||||
|               useSVG = contentType == "image/svg+xml"; | ||||
|               imageSizeStart = frameBoundarySize + 16 + contentType.length + 18; | ||||
|               for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) { | ||||
|                 strBuffer = utf8.decode( | ||||
|                     primaryBuffer.sublist(i, i + 4), allowMalformed: true); | ||||
|                 if (strBuffer == "\r\n\r\n") { | ||||
|                   imageSizeEnd = i; | ||||
|                   imageStart = i + 4; | ||||
|                   break; | ||||
|                 } | ||||
|               } | ||||
|               if (imageSizeEnd > 0) { | ||||
|                 imageSize = int.tryParse(utf8.decode( | ||||
|                     primaryBuffer.sublist(imageSizeStart, imageSizeEnd), | ||||
|                     allowMalformed: true)); | ||||
|                 //Logger.d("content-length: $imageSize"); | ||||
|                 if (imageSize != null && | ||||
|                     primaryBuffer.length >= imageStart + imageSize + 2) { | ||||
|                   sink.add( | ||||
|                       primaryBuffer.sublist( | ||||
|                           imageStart, imageStart + imageSize)); | ||||
|                   primaryBuffer.removeRange(0, imageStart + imageSize + 2); | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|             if (timeToStop) { | ||||
|               sink?.close(); | ||||
|               streamCompleter.complete(); | ||||
|             } | ||||
|           }, | ||||
|           handleError: (error, stack, sink) { | ||||
|             Logger.e("Error parsing MJPEG stream: $error"); | ||||
|           }, | ||||
|           handleDone: (sink) { | ||||
|             Logger.d("Camera stream finished. Reconnecting..."); | ||||
|             sink?.close(); | ||||
|             streamCompleter?.complete(); | ||||
|             _reconnect(); | ||||
|           }, | ||||
|         ) | ||||
|     ).listen((d) { | ||||
|       if (!timeToStop) { | ||||
|         setState(() { | ||||
|           binaryImage = d; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _reconnect() { | ||||
|     disconnect().then((_){ | ||||
|       _connect(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future disconnect() { | ||||
|     Completer disconF = Completer(); | ||||
|     timeToStop = true; | ||||
|     if (streamCompleter != null && !streamCompleter.isCompleted) { | ||||
|       streamCompleter.future.then((_) { | ||||
|         client?.close(); | ||||
|         disconF.complete(); | ||||
|       }); | ||||
|     } else { | ||||
|       client?.close(); | ||||
|       disconF.complete(); | ||||
|     } | ||||
|     return disconF.future; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (!started) { | ||||
|       _entity = EntityModel | ||||
|           .of(context) | ||||
|           .entityWrapper | ||||
|           .entity; | ||||
|       _webHost = HomeAssistantModel.of(context).homeAssistant.connection.httpWebHost; | ||||
|       _connect(); | ||||
|     } | ||||
|  | ||||
|     if (binaryImage.isEmpty) { | ||||
|       return Column( | ||||
|         children: <Widget>[ | ||||
|           Container( | ||||
|               padding: const EdgeInsets.all(20.0), | ||||
|               child: const CircularProgressIndicator() | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       if (useSVG) { | ||||
|         return Column( | ||||
|           children: <Widget>[ | ||||
|             SvgPicture.memory( | ||||
|               Uint8List.fromList(binaryImage), | ||||
|               placeholderBuilder: (BuildContext context) => | ||||
|               new Container( | ||||
|                   padding: const EdgeInsets.all(20.0), | ||||
|                   child: const CircularProgressIndicator() | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ); | ||||
|       } else { | ||||
|         return Column( | ||||
|           children: <Widget>[ | ||||
|             Image.memory( | ||||
|                 Uint8List.fromList(binaryImage), gaplessPlayback: true), | ||||
|           ], | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     disconnect(); | ||||
|     super.dispose(); | ||||
|   } | ||||
| } | ||||
| @@ -7,24 +7,16 @@ class EntityAttributesList extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     List<Widget> attrs = []; | ||||
|     if ((entityModel.entityWrapper.entity.attributesToShow == null) || | ||||
|         (entityModel.entityWrapper.entity.attributesToShow.contains("all"))) { | ||||
|     entityModel.entityWrapper.entity.attributes.forEach((name, value) { | ||||
|         attrs.add(_buildSingleAttribute("$name", "$value")); | ||||
|       attrs.add(_buildSingleAttribute("$name", "${value ?? '-'}")); | ||||
|     }); | ||||
|     } else { | ||||
|       entityModel.entityWrapper.entity.attributesToShow.forEach((String attr) { | ||||
|         String attrValue = entityModel.entityWrapper.entity.getAttribute("$attr"); | ||||
|         if (attrValue != null) { | ||||
|           attrs.add( | ||||
|               _buildSingleAttribute("$attr", "$attrValue")); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|     return Column( | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|       child: Column( | ||||
|         children: attrs, | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -46,7 +38,7 @@ class EntityAttributesList extends StatelessWidget { | ||||
|             padding: EdgeInsets.fromLTRB( | ||||
|                 0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||
|             child: Text( | ||||
|               "$value", | ||||
|               "${value}", | ||||
|               textAlign: TextAlign.right, | ||||
|             ), | ||||
|           ), | ||||
|   | ||||
							
								
								
									
										41
									
								
								lib/entity_widgets/common/flat_service_button.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class FlatServiceButton extends StatelessWidget { | ||||
|  | ||||
|   final String serviceDomain; | ||||
|   final String serviceName; | ||||
|   final String entityId; | ||||
|   final String text; | ||||
|   final double fontSize; | ||||
|  | ||||
|   FlatServiceButton({ | ||||
|     Key key, | ||||
|     @required this.serviceDomain, | ||||
|     @required this.serviceName, | ||||
|     @required this.entityId, | ||||
|     @required this.text, | ||||
|     this.fontSize: Sizes.stateFontSize | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   void _setNewState() { | ||||
|     eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SizedBox( | ||||
|         height: fontSize*2.5, | ||||
|         child: FlatButton( | ||||
|           onPressed: (() { | ||||
|             _setNewState(); | ||||
|           }), | ||||
|           child: Text( | ||||
|             text, | ||||
|             textAlign: TextAlign.right, | ||||
|             style: | ||||
|             new TextStyle(fontSize: fontSize, color: Colors.blue), | ||||
|           ), | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -6,7 +6,7 @@ class LastUpdatedWidget extends StatelessWidget { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.fromLTRB( | ||||
|           Sizes.leftWidgetPadding, 0.0, 0.0, 0.0), | ||||
|           Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, 0.0), | ||||
|       child: Text( | ||||
|         '${entityModel.entityWrapper.entity.lastUpdated}', | ||||
|         textAlign: TextAlign.left, | ||||
|   | ||||
							
								
								
									
										101
									
								
								lib/entity_widgets/common/light_color_picker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,101 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class LightColorPicker extends StatefulWidget { | ||||
|  | ||||
|   final HSVColor color; | ||||
|   final onColorSelected; | ||||
|   final double hueStep; | ||||
|   final double saturationStep; | ||||
|   final EdgeInsets padding; | ||||
|  | ||||
|   LightColorPicker({this.color, this.onColorSelected, this.hueStep: 15.0, this.saturationStep: 0.2, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}); | ||||
|  | ||||
|   @override | ||||
|   LightColorPickerState createState() => new LightColorPickerState(); | ||||
| } | ||||
|  | ||||
| class LightColorPickerState extends State<LightColorPicker> { | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     List<Widget> colorRows = []; | ||||
|     Border border; | ||||
|     bool isSomethingSelected = false; | ||||
|     Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]"); | ||||
|     for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) { | ||||
|       List<Widget> rowChildren = []; | ||||
|       //Logger.d("$saturation"); | ||||
|       double roundedSaturation = double.parse(widget.color.saturation.toStringAsFixed(1)); | ||||
|       //Logger.d("Rounded saturation=$roundedSaturation"); | ||||
|       for (double hue = 0; hue <= (365 - widget.hueStep); | ||||
|       hue += widget.hueStep) { | ||||
|         bool isExactHue = widget.color.hue.round() == hue; | ||||
|         bool isHueInRange = widget.color.hue.round() > hue && widget.color.hue.round() < (hue+widget.hueStep); | ||||
|         bool isExactSaturation = roundedSaturation == saturation; | ||||
|         bool isSaturationInRange = roundedSaturation > saturation && roundedSaturation < double.parse((saturation+widget.saturationStep).toStringAsFixed(1)); | ||||
|         if ((isExactHue || isHueInRange) && (isExactSaturation || isSaturationInRange)) { | ||||
|           //Logger.d("$isExactHue $isHueInRange $isExactSaturation $isSaturationInRange (${saturation+widget.saturationStep})"); | ||||
|           border = Border.all( | ||||
|             width: 2.0, | ||||
|             color: Colors.white, | ||||
|           ); | ||||
|           isSomethingSelected = true; | ||||
|         } else { | ||||
|           border = null; | ||||
|         } | ||||
|         HSVColor currentColor = HSVColor.fromAHSV(1.0, hue, double.parse(saturation.toStringAsFixed(2)), 1.0); | ||||
|         rowChildren.add( | ||||
|             Flexible( | ||||
|                 child: GestureDetector( | ||||
|                   child: Container( | ||||
|                     height: 40.0, | ||||
|                     decoration: BoxDecoration( | ||||
|                         color: currentColor.toColor(), | ||||
|                         border: border, | ||||
|                     ), | ||||
|                   ), | ||||
|                   onTap: () => widget.onColorSelected(currentColor), | ||||
|                 ) | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|       colorRows.add( | ||||
|           Row( | ||||
|             children: rowChildren, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     colorRows.add( | ||||
|         Flexible( | ||||
|             child: GestureDetector( | ||||
|               child: Container( | ||||
|                 height: 40.0, | ||||
|                 decoration: BoxDecoration( | ||||
|                   color: Colors.white, | ||||
|                   border: isSomethingSelected ? null : Border.all( | ||||
|                     width: 2.0, | ||||
|                     color: Colors.amber[200], | ||||
|                   ) | ||||
|                 ), | ||||
|               ), | ||||
|               onTap: () => widget.onColorSelected(HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0)), | ||||
|             ) | ||||
|         ) | ||||
|     ); | ||||
|     return Padding( | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|         children: colorRows, | ||||
|       ), | ||||
|       padding: widget.padding, | ||||
|     ); | ||||
|  | ||||
|   } | ||||
| } | ||||
| @@ -7,8 +7,8 @@ class ModeSelectorWidget extends StatelessWidget { | ||||
|   final String value; | ||||
|   final double captionFontSize; | ||||
|   final double valueFontSize; | ||||
|   final double bottomPadding; | ||||
|   final onChange; | ||||
|   final EdgeInsets padding; | ||||
|  | ||||
|   ModeSelectorWidget({ | ||||
|     Key key, | ||||
| @@ -18,12 +18,14 @@ class ModeSelectorWidget extends StatelessWidget { | ||||
|     @required this.onChange, | ||||
|     this.captionFontSize, | ||||
|     this.valueFontSize, | ||||
|     this.bottomPadding | ||||
|     this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Column( | ||||
|     return Padding( | ||||
|       padding: padding, | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Text("$caption", style: TextStyle( | ||||
| @@ -54,9 +56,9 @@ class ModeSelectorWidget extends StatelessWidget { | ||||
|                 ), | ||||
|               ) | ||||
|             ], | ||||
|         ), | ||||
|         Container(height: bottomPadding ?? Sizes.rowPadding,) | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,6 +7,7 @@ class ModeSwitchWidget extends StatelessWidget { | ||||
|   final double captionFontSize; | ||||
|   final bool value; | ||||
|   final bool expanded; | ||||
|   final EdgeInsets padding; | ||||
|  | ||||
|   ModeSwitchWidget({ | ||||
|     Key key, | ||||
| @@ -14,12 +15,15 @@ class ModeSwitchWidget extends StatelessWidget { | ||||
|     @required this.onChange, | ||||
|     this.captionFontSize, | ||||
|     this.value, | ||||
|     this.expanded: true | ||||
|     this.expanded: true, | ||||
|     this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding) | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Row( | ||||
|     return Padding( | ||||
|       padding: this.padding, | ||||
|       child: Row( | ||||
|         children: <Widget>[ | ||||
|           _buildCaption(), | ||||
|           Switch( | ||||
| @@ -27,6 +31,7 @@ class ModeSwitchWidget extends StatelessWidget { | ||||
|             value: value ?? false, | ||||
|           ) | ||||
|         ], | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -10,8 +10,9 @@ class UniversalSlider extends StatelessWidget { | ||||
|   final double min; | ||||
|   final double max; | ||||
|   final double value; | ||||
|   final EdgeInsets padding; | ||||
|  | ||||
|   const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value}) : super(key: key); | ||||
|   const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -33,7 +34,9 @@ class UniversalSlider extends StatelessWidget { | ||||
|     if (closing != null) { | ||||
|       row.add(closing); | ||||
|     } | ||||
|     return Column( | ||||
|     return Padding( | ||||
|       padding: padding, | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Container(height: Sizes.rowPadding,), | ||||
| @@ -48,6 +51,7 @@ class UniversalSlider extends StatelessWidget { | ||||
|           ), | ||||
|           Container(height: Sizes.rowPadding,) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
							
								
								
									
										262
									
								
								lib/entity_widgets/controls/alarm_control_panel_controls.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,262 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class AlarmControlPanelControlsWidget extends StatefulWidget { | ||||
|  | ||||
|   final bool extended; | ||||
|   final List states; | ||||
|  | ||||
|   const AlarmControlPanelControlsWidget({Key key, @required this.extended, this.states}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _AlarmControlPanelControlsWidgetWidgetState createState() => _AlarmControlPanelControlsWidgetWidgetState(); | ||||
|  | ||||
| } | ||||
|  | ||||
| class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPanelControlsWidget> { | ||||
|  | ||||
|   String code = ""; | ||||
|   List supportedStates; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     supportedStates = widget.states ?? ["arm_home", "arm_away"]; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   void _callService(AlarmControlPanelEntity entity, String service) { | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|           entity.domain, service, entity.entityId, | ||||
|           {"code": "$code"})); | ||||
|     setState(() { | ||||
|       code = ""; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _pinPadHandler(value) { | ||||
|     setState(() { | ||||
|       code += "$value"; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _pinPadClear() { | ||||
|     setState(() { | ||||
|       code = ""; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _askToTrigger(AlarmControlPanelEntity entity) { | ||||
|     // flutter defined function | ||||
|     showDialog( | ||||
|       context: context, | ||||
|       builder: (BuildContext context) { | ||||
|         // return object of type Dialog | ||||
|         return AlertDialog( | ||||
|           title: new Text("Are you sure?"), | ||||
|           content: new Text("Are you sure want to trigger alarm ${entity.displayName}?"), | ||||
|           actions: <Widget>[ | ||||
|             FlatButton( | ||||
|               child: new Text("Yes"), | ||||
|               onPressed: () { | ||||
|                 eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null)); | ||||
|                 Navigator.of(context).pop(); | ||||
|               }, | ||||
|             ), | ||||
|             FlatButton( | ||||
|               child: new Text("No"), | ||||
|               onPressed: () { | ||||
|                 Navigator.of(context).pop(); | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final AlarmControlPanelEntity entity = entityModel.entityWrapper.entity; | ||||
|     List<Widget> buttons = []; | ||||
|     if (entity.state == EntityState.alarm_disarmed) { | ||||
|       if (supportedStates.contains("arm_home")) { | ||||
|         buttons.add( | ||||
|           RaisedButton( | ||||
|             onPressed: () => _callService(entity, "alarm_arm_home"), | ||||
|             child: Text("ARM HOME"), | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|       if (supportedStates.contains("arm_away")) { | ||||
|         buttons.add( | ||||
|             RaisedButton( | ||||
|               onPressed: () => _callService(entity, "alarm_arm_away"), | ||||
|               child: Text("ARM AWAY"), | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|       if (widget.extended) { | ||||
|         if (supportedStates.contains("arm_night")) { | ||||
|           buttons.add( | ||||
|               RaisedButton( | ||||
|                 onPressed: () => _callService(entity, "alarm_arm_night"), | ||||
|                 child: Text("ARM NIGHT"), | ||||
|               ) | ||||
|           ); | ||||
|         } | ||||
|         if (supportedStates.contains("arm_custom_bypass")) { | ||||
|           buttons.add( | ||||
|               RaisedButton( | ||||
|                 onPressed: () => | ||||
|                     _callService(entity, "alarm_arm_custom_bypass"), | ||||
|                 child: Text("ARM CUSTOM BYPASS"), | ||||
|               ) | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       buttons.add( | ||||
|         RaisedButton( | ||||
|           onPressed: () => _callService(entity, "alarm_disarm"), | ||||
|           child: Text("DISARM"), | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|     Widget pinPad; | ||||
|     if (entity.attributes["code_format"] == null) { | ||||
|       pinPad = Container(width: 0.0, height: 0.0,); | ||||
|     } else { | ||||
|       pinPad = Padding( | ||||
|           padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: <Widget>[ | ||||
|               Wrap( | ||||
|                 spacing: 5.0, | ||||
|                 children: <Widget>[ | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("1"), | ||||
|                     child: Text("1"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("2"), | ||||
|                     child: Text("2"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("3"), | ||||
|                     child: Text("3"), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|               Wrap( | ||||
|                 spacing: 5.0, | ||||
|                 children: <Widget>[ | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("4"), | ||||
|                     child: Text("4"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("5"), | ||||
|                     child: Text("5"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("6"), | ||||
|                     child: Text("6"), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|               Wrap( | ||||
|                 spacing: 5.0, | ||||
|                 children: <Widget>[ | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("7"), | ||||
|                     child: Text("7"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("8"), | ||||
|                     child: Text("8"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("9"), | ||||
|                     child: Text("9"), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|               Wrap( | ||||
|                 spacing: 5.0, | ||||
|                 alignment: WrapAlignment.end, | ||||
|                 children: <Widget>[ | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("0"), | ||||
|                     child: Text("0"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadClear(), | ||||
|                     child: Text("CLEAR"), | ||||
|                   ) | ||||
|                 ], | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     Widget inputWrapper; | ||||
|     if (entity.attributes["code_format"] == null) { | ||||
|       inputWrapper = Container(width: 0.0, height: 0.0,); | ||||
|     } else { | ||||
|       inputWrapper = Container( | ||||
|           width: 150.0, | ||||
|           child: TextField( | ||||
|               decoration: InputDecoration( | ||||
|                   labelText: "Alarm Code" | ||||
|               ), | ||||
|               //focusNode: _focusNode, | ||||
|               obscureText: true, | ||||
|               controller: new TextEditingController.fromValue( | ||||
|                   new TextEditingValue( | ||||
|                       text: code, | ||||
|                       selection: | ||||
|                       new TextSelection.collapsed(offset: code.length) | ||||
|                   ) | ||||
|               ), | ||||
|               onChanged: (value) { | ||||
|                 code = value; | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     Widget buttonsWrapper = Padding( | ||||
|         padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding), | ||||
|         child: Wrap( | ||||
|             alignment: WrapAlignment.center, | ||||
|             spacing: 15.0, | ||||
|             runSpacing: Sizes.rowPadding, | ||||
|             children: buttons | ||||
|         ) | ||||
|     ); | ||||
|     Widget triggerButton = Row( | ||||
|       mainAxisAlignment: MainAxisAlignment.end, | ||||
|       children: [ | ||||
|         FlatButton( | ||||
|           child: Text( | ||||
|             "TRIGGER", | ||||
|             style: TextStyle(color: Colors.redAccent) | ||||
|           ), | ||||
|           onPressed: () => _askToTrigger(entity), | ||||
|         ) | ||||
|       ] | ||||
|     ); | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         widget.extended ? buttonsWrapper : inputWrapper, | ||||
|         widget.extended ? inputWrapper : buttonsWrapper, | ||||
|         widget.extended ? pinPad : triggerButton | ||||
|       ] | ||||
|  | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -13,6 +13,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|   bool _showPending = false; | ||||
|   bool _changedHere = false; | ||||
|   Timer _resetTimer; | ||||
|   Timer _tempThrottleTimer; | ||||
|   Timer _targetTempThrottleTimer; | ||||
|   double _tmpTemperature = 0.0; | ||||
|   double _tmpTargetLow = 0.0; | ||||
|   double _tmpTargetHigh = 0.0; | ||||
| @@ -40,53 +42,69 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|     _changedHere = false; | ||||
|   } | ||||
|  | ||||
|   void _temperatureUp(ClimateEntity entity, double step) { | ||||
|     _tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp; | ||||
|   void _temperatureUp(ClimateEntity entity) { | ||||
|     _tmpTemperature = ((_tmpTemperature + entity.temperatureStep) <= entity.maxTemp) ? _tmpTemperature + entity.temperatureStep : entity.maxTemp; | ||||
|     _setTemperature(entity); | ||||
|   } | ||||
|  | ||||
|   void _temperatureDown(ClimateEntity entity, double step) { | ||||
|     _tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp; | ||||
|   void _temperatureDown(ClimateEntity entity) { | ||||
|     _tmpTemperature = ((_tmpTemperature - entity.temperatureStep) >= entity.minTemp) ? _tmpTemperature - entity.temperatureStep : entity.minTemp; | ||||
|     _setTemperature(entity); | ||||
|   } | ||||
|  | ||||
|   void _targetLowUp(ClimateEntity entity, double step) { | ||||
|     _tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp; | ||||
|   void _targetLowUp(ClimateEntity entity) { | ||||
|     _tmpTargetLow = ((_tmpTargetLow + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetLow + entity.temperatureStep : entity.maxTemp; | ||||
|     _setTargetTemp(entity); | ||||
|   } | ||||
|  | ||||
|   void _targetLowDown(ClimateEntity entity, double step) { | ||||
|     _tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp; | ||||
|   void _targetLowDown(ClimateEntity entity) { | ||||
|     _tmpTargetLow = ((_tmpTargetLow - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetLow - entity.temperatureStep : entity.minTemp; | ||||
|     _setTargetTemp(entity); | ||||
|   } | ||||
|  | ||||
|   void _targetHighUp(ClimateEntity entity, double step) { | ||||
|     _tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp; | ||||
|   void _targetHighUp(ClimateEntity entity) { | ||||
|     _tmpTargetHigh = ((_tmpTargetHigh + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetHigh + entity.temperatureStep : entity.maxTemp; | ||||
|     _setTargetTemp(entity); | ||||
|   } | ||||
|  | ||||
|   void _targetHighDown(ClimateEntity entity, double step) { | ||||
|     _tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp; | ||||
|   void _targetHighDown(ClimateEntity entity) { | ||||
|     _tmpTargetHigh = ((_tmpTargetHigh - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetHigh - entity.temperatureStep : entity.minTemp; | ||||
|     _setTargetTemp(entity); | ||||
|   } | ||||
|  | ||||
|   void _setTemperature(ClimateEntity entity) { | ||||
|     if (_tempThrottleTimer!=null) { | ||||
|       _tempThrottleTimer.cancel(); | ||||
|     } | ||||
|     setState(() { | ||||
|       _changedHere = true; | ||||
|       _tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1)); | ||||
|     }); | ||||
|     _tempThrottleTimer = Timer(Duration(seconds: 2), () { | ||||
|       setState(() { | ||||
|         _changedHere = true; | ||||
|         eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"})); | ||||
|         _resetStateTimer(entity); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setTargetTemp(ClimateEntity entity) { | ||||
|     if (_targetTempThrottleTimer!=null) { | ||||
|       _targetTempThrottleTimer.cancel(); | ||||
|     } | ||||
|     setState(() { | ||||
|       _changedHere = true; | ||||
|       _tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1)); | ||||
|       _tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1)); | ||||
|     }); | ||||
|     _targetTempThrottleTimer = Timer(Duration(seconds: 2), () { | ||||
|       setState(() { | ||||
|         _changedHere = true; | ||||
|         eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"})); | ||||
|         _resetStateTimer(entity); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setTargetHumidity(ClimateEntity entity, double value) { | ||||
| @@ -167,7 +185,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final ClimateEntity entity = entityModel.entityWrapper.entity; | ||||
|     if (_changedHere) { | ||||
|       _showPending = (_tmpTemperature != entity.temperature); | ||||
|       _showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow); | ||||
|       _changedHere = false; | ||||
|     } else { | ||||
|       _resetTimer?.cancel(); | ||||
| @@ -278,10 +296,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|           TemperatureControlWidget( | ||||
|             value: _tmpTemperature, | ||||
|             fontColor: _showPending ? Colors.red : Colors.black, | ||||
|             onLargeDec: () => _temperatureDown(entity, 0.5), | ||||
|             onLargeInc: () => _temperatureUp(entity, 0.5), | ||||
|             onSmallDec: () => _temperatureDown(entity, 0.1), | ||||
|             onSmallInc: () => _temperatureUp(entity, 0.1), | ||||
|             onDec: () => _temperatureDown(entity), | ||||
|             onInc: () => _temperatureUp(entity), | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
| @@ -297,10 +313,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|         TemperatureControlWidget( | ||||
|           value: _tmpTargetLow, | ||||
|           fontColor: _showPending ? Colors.red : Colors.black, | ||||
|           onLargeDec: () => _targetLowDown(entity, 0.5), | ||||
|           onLargeInc: () => _targetLowUp(entity, 0.5), | ||||
|           onSmallDec: () => _targetLowDown(entity, 0.1), | ||||
|           onSmallInc: () => _targetLowUp(entity, 0.1), | ||||
|           onDec: () => _targetLowDown(entity), | ||||
|           onInc: () => _targetLowUp(entity), | ||||
|         ), | ||||
|         Expanded( | ||||
|           child: Container(height: 10.0), | ||||
| @@ -312,10 +326,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|           TemperatureControlWidget( | ||||
|             value: _tmpTargetHigh, | ||||
|             fontColor: _showPending ? Colors.red : Colors.black, | ||||
|             onLargeDec: () => _targetHighDown(entity, 0.5), | ||||
|             onLargeInc: () => _targetHighUp(entity, 0.5), | ||||
|             onSmallDec: () => _targetHighDown(entity, 0.1), | ||||
|             onSmallInc: () => _targetHighUp(entity, 0.1), | ||||
|             onDec: () => _targetHighDown(entity), | ||||
|             onInc: () => _targetHighUp(entity), | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
| @@ -401,18 +413,14 @@ class TemperatureControlWidget extends StatelessWidget { | ||||
|   final double value; | ||||
|   final double fontSize; | ||||
|   final Color fontColor; | ||||
|   final onSmallInc; | ||||
|   final onLargeInc; | ||||
|   final onSmallDec; | ||||
|   final onLargeDec; | ||||
|   final onInc; | ||||
|   final onDec; | ||||
|  | ||||
|   TemperatureControlWidget( | ||||
|       {Key key, | ||||
|         @required this.value, | ||||
|         @required this.onSmallInc, | ||||
|         @required this.onSmallDec, | ||||
|         @required this.onLargeInc, | ||||
|         @required this.onLargeDec, | ||||
|         @required this.onInc, | ||||
|         @required this.onDec, | ||||
|         this.fontSize, | ||||
|         this.fontColor}) | ||||
|       : super(key: key); | ||||
| @@ -432,32 +440,16 @@ class TemperatureControlWidget extends StatelessWidget { | ||||
|         Column( | ||||
|           children: <Widget>[ | ||||
|             IconButton( | ||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName( | ||||
|               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||
|                   'mdi:chevron-up')), | ||||
|               iconSize: 30.0, | ||||
|               onPressed: () => onSmallInc(), | ||||
|               onPressed: () => onInc(), | ||||
|             ), | ||||
|             IconButton( | ||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName( | ||||
|               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||
|                   'mdi:chevron-down')), | ||||
|               iconSize: 30.0, | ||||
|               onPressed: () => onSmallDec(), | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|         Column( | ||||
|           children: <Widget>[ | ||||
|             IconButton( | ||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName( | ||||
|                   'mdi:chevron-double-up')), | ||||
|               iconSize: 30.0, | ||||
|               onPressed: () => onLargeInc(), | ||||
|             ), | ||||
|             IconButton( | ||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName( | ||||
|                   'mdi:chevron-double-down')), | ||||
|               iconSize: 30.0, | ||||
|               onPressed: () => onLargeDec(), | ||||
|               onPressed: () => onDec(), | ||||
|             ) | ||||
|           ], | ||||
|         ) | ||||
|   | ||||
| @@ -157,7 +157,7 @@ class CoverTiltControlsWidget extends StatelessWidget { | ||||
|     if (entity.supportOpenTilt) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.createIconDataFromIconName( | ||||
|             MaterialDesignIcons.getIconDataFromIconName( | ||||
|                 "mdi:arrow-top-right"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
| @@ -170,7 +170,7 @@ class CoverTiltControlsWidget extends StatelessWidget { | ||||
|     if (entity.supportStopTilt) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.createIconDataFromIconName("mdi:stop"), | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:stop"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: () => _stop(entity))); | ||||
| @@ -182,7 +182,7 @@ class CoverTiltControlsWidget extends StatelessWidget { | ||||
|     if (entity.supportCloseTilt) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.createIconDataFromIconName( | ||||
|             MaterialDesignIcons.getIconDataFromIconName( | ||||
|                 "mdi:arrow-bottom-left"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|   | ||||
| @@ -10,16 +10,18 @@ class LightControlsWidget extends StatefulWidget { | ||||
| class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|  | ||||
|   int _tmpBrightness; | ||||
|   int _tmpColorTemp; | ||||
|   Color _tmpColor; | ||||
|   int _tmpWhiteValue; | ||||
|   int _tmpColorTemp = 0; | ||||
|   HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0); | ||||
|   bool _changedHere = false; | ||||
|   String _tmpEffect; | ||||
|  | ||||
|   void _resetState(LightEntity entity) { | ||||
|     _tmpBrightness = entity.brightness ?? 0; | ||||
|     _tmpColorTemp = entity.colorTemp; | ||||
|     _tmpColor = entity.color; | ||||
|     _tmpEffect = null; | ||||
|     _tmpWhiteValue = entity.whiteValue ?? 0; | ||||
|     _tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt(); | ||||
|     _tmpColor = entity.color ?? _tmpColor; | ||||
|     _tmpEffect = entity.effect; | ||||
|   } | ||||
|  | ||||
|   void _setBrightness(LightEntity entity, double value) { | ||||
| @@ -38,6 +40,17 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setWhiteValue(LightEntity entity, double value) { | ||||
|     setState(() { | ||||
|       _tmpWhiteValue = value.round(); | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_on", entity.entityId, | ||||
|             {"white_value": _tmpWhiteValue})); | ||||
|  | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setColorTemp(LightEntity entity, double value) { | ||||
|     setState(() { | ||||
|       _tmpColorTemp = value.round(); | ||||
| @@ -48,20 +61,14 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setColor(LightEntity entity, Color color) { | ||||
|   void _setColor(LightEntity entity, HSVColor color) { | ||||
|     setState(() { | ||||
|       _tmpColor = color; | ||||
|       _changedHere = true; | ||||
|       TheLogger.debug( "Color: [${color.red}, ${color.green}, ${color.blue}]"); | ||||
|       if ((color == Colors.black) || ((color.red == color.green) && (color.green == color.blue)))  { | ||||
|         eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_off", entity.entityId, | ||||
|             null)); | ||||
|       } else { | ||||
|       Logger.d( "HS Color: [${color.hue}, ${color.saturation}]"); | ||||
|       eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "turn_on", entity.entityId, | ||||
|             {"rgb_color": [color.red, color.green, color.blue]})); | ||||
|       } | ||||
|           {"hs_color": [color.hue, color.saturation*100]})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -90,6 +97,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         _buildBrightnessControl(entity), | ||||
|         _buildWhiteValueControl(entity), | ||||
|         _buildColorTempControl(entity), | ||||
|         _buildColorControl(entity), | ||||
|         _buildEffectControl(entity) | ||||
| @@ -98,7 +106,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|   } | ||||
|  | ||||
|   Widget _buildBrightnessControl(LightEntity entity) { | ||||
|     if ((entity.supportBrightness) && (_tmpBrightness != null) && (entity.state != EntityState.unavailable)) { | ||||
|     if ((entity.supportBrightness) && (_tmpBrightness != null)) { | ||||
|       return UniversalSlider( | ||||
|         onChanged: (value) { | ||||
|           setState(() { | ||||
| @@ -109,7 +117,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|         min: 0.0, | ||||
|         max: 255.0, | ||||
|         onChangeEnd: (value) => _setBrightness(entity, value), | ||||
|         value: _tmpBrightness.toDouble(), | ||||
|         value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(), | ||||
|         leading: Icon(Icons.brightness_5), | ||||
|         title: "Brightness", | ||||
|       ); | ||||
| @@ -118,12 +126,33 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildWhiteValueControl(LightEntity entity) { | ||||
|     if ((entity.supportWhiteValue) && (_tmpWhiteValue != null)) { | ||||
|       return UniversalSlider( | ||||
|         onChanged: (value) { | ||||
|           setState(() { | ||||
|             _changedHere = true; | ||||
|             _tmpWhiteValue = value.round(); | ||||
|           }); | ||||
|         }, | ||||
|         min: 0.0, | ||||
|         max: 255.0, | ||||
|         onChangeEnd: (value) => _setWhiteValue(entity, value), | ||||
|         value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(), | ||||
|         leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")), | ||||
|         title: "White", | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildColorTempControl(LightEntity entity) { | ||||
|     if ((entity.supportColorTemp) && (_tmpColorTemp != null)) { | ||||
|     if (entity.supportColorTemp) { | ||||
|       return UniversalSlider( | ||||
|         title: "Color temperature", | ||||
|         leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),), | ||||
|         value:  _tmpColorTemp.toDouble(), | ||||
|         value:  _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(), | ||||
|         onChangeEnd: (value) => _setColorTemp(entity, value), | ||||
|         max: entity.maxMireds, | ||||
|         min: entity.minMireds, | ||||
| @@ -141,25 +170,39 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|   } | ||||
|  | ||||
|   Widget _buildColorControl(LightEntity entity) { | ||||
|     if ((entity.supportColor) && (entity.color != null)) { | ||||
|     if (entity.supportColor) { | ||||
|       HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor; | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|           Container(height: Sizes.rowPadding,), | ||||
|           RaisedButton( | ||||
|             onPressed: () => _showColorPicker(entity), | ||||
|             color: _tmpColor ?? Colors.black45, | ||||
|             child: Text( | ||||
|               "COLOR", | ||||
|               textAlign: TextAlign.center, | ||||
|               style: TextStyle( | ||||
|                 fontSize: 50.0, | ||||
|                 fontWeight: FontWeight.bold, | ||||
|                 color: Colors.black12, | ||||
|           LightColorPicker( | ||||
|             color: _tmpColor, | ||||
|             onColorSelected: (color) => _setColor(entity, color), | ||||
|           ), | ||||
|           Row( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: <Widget>[ | ||||
|               FlatButton( | ||||
|                 color: _tmpColor.toColor(), | ||||
|                 child: Text('Copy color'), | ||||
|                 onPressed: _tmpColor == null ? null : () { | ||||
|                   setState(() { | ||||
|                     HomeAssistantModel | ||||
|                         .of(context) | ||||
|                         .homeAssistant | ||||
|                         .savedColor = _tmpColor; | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|           ), | ||||
|           Container(height: 2*Sizes.rowPadding,), | ||||
|               FlatButton( | ||||
|                 color: savedColor?.toColor() ?? Colors.transparent, | ||||
|                 child: Text('Paste color'), | ||||
|                 onPressed: savedColor == null ? null : () { | ||||
|                   _setColor(entity, savedColor); | ||||
|                 }, | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
| @@ -167,28 +210,6 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _showColorPicker(LightEntity entity) { | ||||
|     showDialog( | ||||
|       context: context, | ||||
|       builder: (BuildContext context) { | ||||
|         return AlertDialog( | ||||
|           titlePadding: EdgeInsets.all(0.0), | ||||
|           contentPadding: EdgeInsets.all(0.0), | ||||
|           content: SingleChildScrollView( | ||||
|             child: MaterialPicker( | ||||
|               pickerColor: _tmpColor, | ||||
|               onColorChanged: (color) { | ||||
|                 _setColor(entity, color); | ||||
|                 Navigator.of(context).pop(); | ||||
|               }, | ||||
|               enableLabel: true, | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildEffectControl(LightEntity entity) { | ||||
|     if ((entity.supportEffect) && (entity.effectList != null)) { | ||||
|       return ModeSelectorWidget( | ||||
|   | ||||
| @@ -73,7 +73,7 @@ class MediaPlayerWidget extends StatelessWidget { | ||||
|  | ||||
|   Widget _buildImage(MediaPlayerEntity entity) { | ||||
|     String state = entity.state; | ||||
|     if (homeAssistantWebHost != null && entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) { | ||||
|     if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) { | ||||
|       return Container( | ||||
|         color: Colors.black, | ||||
|         child: Row( | ||||
| @@ -81,7 +81,7 @@ class MediaPlayerWidget extends StatelessWidget { | ||||
|           children: <Widget>[ | ||||
|             Flexible( | ||||
|               child: Image( | ||||
|                 image: CachedNetworkImageProvider("$homeAssistantWebHost${entity.entityPicture}"), | ||||
|                 image: CachedNetworkImageProvider("${entity.entityPicture}"), | ||||
|                 height: 240.0, | ||||
|                 //width: 320.0, | ||||
|                 fit: BoxFit.contain, | ||||
| @@ -95,7 +95,7 @@ class MediaPlayerWidget extends StatelessWidget { | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: <Widget>[ | ||||
|           Icon( | ||||
|             MaterialDesignIcons.createIconDataFromIconName("mdi:movie"), | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:movie"), | ||||
|             size: 150.0, | ||||
|             color: EntityColor.stateColor("$state"), | ||||
|           ) | ||||
| @@ -120,12 +120,12 @@ class MediaPlayerPlaybackControls extends StatelessWidget { | ||||
|   void _setPower(MediaPlayerEntity entity) { | ||||
|     if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) { | ||||
|       if (entity.state == EntityState.off) { | ||||
|         TheLogger.debug("${entity.entityId} turn_on"); | ||||
|         Logger.d("${entity.entityId} turn_on"); | ||||
|         eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_on", entity.entityId, | ||||
|             null)); | ||||
|       } else { | ||||
|         TheLogger.debug("${entity.entityId} turn_off"); | ||||
|         Logger.d("${entity.entityId} turn_off"); | ||||
|         eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_off", entity.entityId, | ||||
|             null)); | ||||
| @@ -134,7 +134,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget { | ||||
|   } | ||||
|  | ||||
|   void _callAction(MediaPlayerEntity entity, String action) { | ||||
|     TheLogger.debug("${entity.entityId} $action"); | ||||
|     Logger.d("${entity.entityId} $action"); | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "$action", entity.entityId, | ||||
|         null)); | ||||
| @@ -227,7 +227,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget { | ||||
|     if (showMenu) { | ||||
|       result.add( | ||||
|           IconButton( | ||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName( | ||||
|               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||
|                   "mdi:dots-vertical")), | ||||
|               onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity)) | ||||
|           ) | ||||
| @@ -307,11 +307,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> { | ||||
|     if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) { | ||||
|       Widget muteWidget; | ||||
|       Widget volumeStepWidget; | ||||
|       if (entity.supportVolumeMute) { | ||||
|       if (entity.supportVolumeMute  || entity.attributes["is_volume_muted"] != null) { | ||||
|         bool isMuted = entity.attributes["is_volume_muted"] ?? false; | ||||
|         muteWidget = | ||||
|             IconButton( | ||||
|                 icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up), | ||||
|                 icon: Icon(isMuted ? Icons.volume_up : Icons.volume_off), | ||||
|                 onPressed: () => _setVolumeMute(!isMuted, entity.entityId) | ||||
|             ); | ||||
|       } else { | ||||
| @@ -322,11 +322,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> { | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: <Widget>[ | ||||
|             IconButton( | ||||
|                 icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:plus")), | ||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")), | ||||
|                 onPressed: () => _setVolumeUp(entity.entityId) | ||||
|             ), | ||||
|             IconButton( | ||||
|                 icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:minus")), | ||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")), | ||||
|                 onPressed: () => _setVolumeDown(entity.entityId) | ||||
|             ) | ||||
|           ], | ||||
|   | ||||
| @@ -11,6 +11,25 @@ class DefaultEntityContainer extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityModel entityModel = EntityModel.of(context); | ||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { | ||||
|       return MissedEntityWidget(); | ||||
|     } | ||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) { | ||||
|       return Divider(); | ||||
|     } | ||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|           Divider(), | ||||
|           Text( | ||||
|               "${entityModel.entityWrapper.entity.displayName}", | ||||
|             style: TextStyle(color: Colors.blue), | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|     return InkWell( | ||||
|       onLongPress: () { | ||||
|         if (entityModel.handleTap) { | ||||
| @@ -30,7 +49,9 @@ class DefaultEntityContainer extends StatelessWidget { | ||||
|           Flexible( | ||||
|             fit: FlexFit.tight, | ||||
|             flex: 3, | ||||
|             child: EntityName(), | ||||
|             child: EntityName( | ||||
|               padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0), | ||||
|             ), | ||||
|           ), | ||||
|           state | ||||
|         ], | ||||
|   | ||||
| @@ -2,6 +2,8 @@ part of '../main.dart'; | ||||
|  | ||||
| class EntityColor { | ||||
|  | ||||
|   static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0); | ||||
|  | ||||
|   static const badgeColors = { | ||||
|     "default": Color.fromRGBO(223, 76, 30, 1.0), | ||||
|     "binary_sensor": Color.fromRGBO(3, 155, 229, 1.0) | ||||
| @@ -10,19 +12,29 @@ class EntityColor { | ||||
|   static const _stateColors = { | ||||
|     EntityState.on: Colors.amber, | ||||
|     "auto": Colors.amber, | ||||
|     EntityState.idle: Colors.amber, | ||||
|     EntityState.active: Colors.amber, | ||||
|     EntityState.playing: Colors.amber, | ||||
|     "above_horizon": Colors.amber, | ||||
|     EntityState.home:  Colors.amber, | ||||
|     EntityState.open:  Colors.amber, | ||||
|     EntityState.off: Color.fromRGBO(68, 115, 158, 1.0), | ||||
|     EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0), | ||||
|     "below_horizon": Color.fromRGBO(68, 115, 158, 1.0), | ||||
|     "default": Color.fromRGBO(68, 115, 158, 1.0), | ||||
|     EntityState.off: defaultStateColor, | ||||
|     EntityState.closed: defaultStateColor, | ||||
|     "below_horizon": defaultStateColor, | ||||
|     "default": defaultStateColor, | ||||
|     EntityState.idle: defaultStateColor, | ||||
|     "heat": Colors.redAccent, | ||||
|     "cool": Colors.lightBlue, | ||||
|     EntityState.unavailable: Colors.black26, | ||||
|     EntityState.unknown: Colors.black26, | ||||
|     EntityState.alarm_disarmed: Colors.green, | ||||
|     EntityState.alarm_armed_away: Colors.redAccent, | ||||
|     EntityState.alarm_armed_custom_bypass: Colors.redAccent, | ||||
|     EntityState.alarm_armed_home: Colors.redAccent, | ||||
|     EntityState.alarm_armed_night: Colors.redAccent, | ||||
|     EntityState.alarm_triggered: Colors.redAccent, | ||||
|     EntityState.alarm_arming: Colors.amber, | ||||
|     EntityState.alarm_disarming: Colors.amber, | ||||
|     EntityState.alarm_pending: Colors.amber, | ||||
|   }; | ||||
|  | ||||
|   static Color stateColor(String state) { | ||||
|   | ||||
| @@ -3,20 +3,71 @@ part of '../main.dart'; | ||||
| class EntityIcon extends StatelessWidget { | ||||
|  | ||||
|   final EdgeInsetsGeometry padding; | ||||
|   final double iconSize; | ||||
|   final double size; | ||||
|   final Color color; | ||||
|  | ||||
|   const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB( | ||||
|       Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key); | ||||
|   const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key); | ||||
|  | ||||
|   int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { | ||||
|     String domain = entityId.split(".")[0]; | ||||
|     String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"]; | ||||
|     String iconNameByDeviceClass; | ||||
|     if (deviceClass != null) { | ||||
|       iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"]; | ||||
|     } | ||||
|     String iconName = iconNameByDeviceClass ?? iconNameByDomain; | ||||
|     if (iconName != null) { | ||||
|       return MaterialDesignIcons.iconsDataMap[iconName] ?? 0; | ||||
|     } else { | ||||
|       return 0; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget buildIcon(EntityWrapper data, Color color) { | ||||
|     if (data == null) { | ||||
|       return null; | ||||
|     } | ||||
|     if (data.entityPicture != null) { | ||||
|       return Container( | ||||
|         height: size+12, | ||||
|         width: size+12, | ||||
|         decoration: BoxDecoration( | ||||
|             shape: BoxShape.circle, | ||||
|             image: DecorationImage( | ||||
|               fit:BoxFit.cover, | ||||
|               image: CachedNetworkImageProvider( | ||||
|                 "${data.entityPicture}" | ||||
|               ), | ||||
|             ) | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|     String iconName = data.icon; | ||||
|     int iconCode = 0; | ||||
|     if (iconName.length > 0) { | ||||
|       iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName); | ||||
|     } else { | ||||
|       iconCode = getDefaultIconByEntityId(data.entity.entityId, | ||||
|           data.entity.deviceClass, data.entity.state); // | ||||
|     } | ||||
|     return Padding( | ||||
|         padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0), | ||||
|         child: Icon( | ||||
|           IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||
|           size: size, | ||||
|           color: color, | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     return Padding( | ||||
|       padding: padding, | ||||
|       child: MaterialDesignIcons.createIconWidgetFromEntityData( | ||||
|       child: buildIcon( | ||||
|           entityWrapper, | ||||
|           iconSize, | ||||
|           EntityColor.stateColor(entityWrapper.entity.state) | ||||
|           color ?? EntityColor.stateColor(entityWrapper.entity.state) | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -7,19 +7,25 @@ class EntityName extends StatelessWidget { | ||||
|   final bool wordsWrap; | ||||
|   final double fontSize; | ||||
|   final TextAlign textAlign; | ||||
|   final int maxLines; | ||||
|  | ||||
|   const EntityName({Key key, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key); | ||||
|   const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     TextStyle textStyle = TextStyle(fontSize: fontSize); | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) { | ||||
|       textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline); | ||||
|     } | ||||
|     return Padding( | ||||
|       padding: padding, | ||||
|       child: Text( | ||||
|         "${entityWrapper.displayName}", | ||||
|         overflow: textOverflow, | ||||
|         softWrap: wordsWrap, | ||||
|         style: TextStyle(fontSize: fontSize), | ||||
|         maxLines: maxLines, | ||||
|         style: textStyle, | ||||
|         textAlign: textAlign, | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -4,37 +4,54 @@ class GlanceEntityContainer extends StatelessWidget { | ||||
|  | ||||
|   final bool showName; | ||||
|   final bool showState; | ||||
|   final bool nameInTheBottom; | ||||
|   final double iconSize; | ||||
|   final double nameFontSize; | ||||
|   final bool wordsWrapInName; | ||||
|  | ||||
|   GlanceEntityContainer({ | ||||
|     Key key, @required this.showName, @required this.showState, | ||||
|     Key key, | ||||
|     @required this.showName, | ||||
|     @required this.showState, | ||||
|     this.nameInTheBottom: false, | ||||
|     this.iconSize: Sizes.iconSize, | ||||
|     this.nameFontSize: Sizes.smallFontSize, | ||||
|     this.wordsWrapInName: false | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { | ||||
|       return MissedEntityWidget(); | ||||
|     } | ||||
|     if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|     List<Widget> result = []; | ||||
|     if (!nameInTheBottom) { | ||||
|       if (showName) { | ||||
|       result.add(EntityName( | ||||
|         padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|         textOverflow: TextOverflow.ellipsis, | ||||
|         wordsWrap: false, | ||||
|         textAlign: TextAlign.center, | ||||
|         fontSize: Sizes.smallFontSize, | ||||
|       )); | ||||
|         result.add(_buildName()); | ||||
|       } | ||||
|     } else { | ||||
|       if (showState) { | ||||
|         result.add(_buildState()); | ||||
|       } | ||||
|     } | ||||
|     result.add( | ||||
|       EntityIcon( | ||||
|         padding: EdgeInsets.all(0.0), | ||||
|         iconSize: Sizes.iconSize, | ||||
|         size: iconSize, | ||||
|       ) | ||||
|     ); | ||||
|     if (!nameInTheBottom) { | ||||
|       if (showState) { | ||||
|       result.add(SimpleEntityState( | ||||
|         textAlign: TextAlign.center, | ||||
|         expanded: false, | ||||
|         padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||
|       )); | ||||
|         result.add(_buildState()); | ||||
|       } | ||||
|     } else { | ||||
|       result.add(_buildName()); | ||||
|     } | ||||
|  | ||||
|     return Center( | ||||
|       child: InkResponse( | ||||
|         child: ConstrainedBox( | ||||
| @@ -51,4 +68,23 @@ class GlanceEntityContainer extends StatelessWidget { | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildName() { | ||||
|     return EntityName( | ||||
|       padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|       textOverflow: TextOverflow.ellipsis, | ||||
|       wordsWrap: wordsWrapInName, | ||||
|       textAlign: TextAlign.center, | ||||
|       fontSize: nameFontSize, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildState() { | ||||
|     return SimpleEntityState( | ||||
|       textAlign: TextAlign.center, | ||||
|       expanded: false, | ||||
|       maxLines: 1, | ||||
|       padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -94,11 +94,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget> | ||||
|   } | ||||
|  | ||||
|   List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() { | ||||
|     TheLogger.debug("  parsing history..."); | ||||
|     Logger.d("  parsing history..."); | ||||
|     Map<String, List<EntityHistoryMoment>> numericDataLists = {}; | ||||
|     int colorIdCounter = 0; | ||||
|     widget.config.numericAttributesToShow.forEach((String attrName) { | ||||
|       TheLogger.debug("    parsing attribute $attrName"); | ||||
|       Logger.d("    parsing attribute $attrName"); | ||||
|       List<EntityHistoryMoment> data = []; | ||||
|       DateTime now = DateTime.now(); | ||||
|       for (var i = 0; i < widget.rawHistory.length; i++) { | ||||
| @@ -152,7 +152,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget> | ||||
|     } | ||||
|     List<charts.Series<EntityHistoryMoment, DateTime>> result = []; | ||||
|     numericDataLists.forEach((attrName, dataList) { | ||||
|       TheLogger.debug("  adding ${dataList.length} data values"); | ||||
|       Logger.d("  adding ${dataList.length} data values"); | ||||
|       result.add( | ||||
|         new charts.Series<EntityHistoryMoment, DateTime>( | ||||
|           id: "value", | ||||
|   | ||||
| @@ -32,6 +32,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> { | ||||
|   List _history; | ||||
|   bool _needToUpdateHistory; | ||||
|   DateTime _historyLastUpdated; | ||||
|   bool _disposed = false; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
| @@ -42,21 +43,25 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> { | ||||
|   void _loadHistory(HomeAssistant ha, String entityId) { | ||||
|     DateTime now = DateTime.now(); | ||||
|     if (_historyLastUpdated != null) { | ||||
|       TheLogger.debug("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago"); | ||||
|       Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago"); | ||||
|     } | ||||
|     if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) { | ||||
|       _historyLastUpdated = now; | ||||
|       ha.getHistory(entityId).then((history){ | ||||
|       ha.connection.getHistory(entityId).then((history){ | ||||
|         if (!_disposed) { | ||||
|           setState(() { | ||||
|             _history = history.isNotEmpty ? history[0] : []; | ||||
|             _needToUpdateHistory = false; | ||||
|           }); | ||||
|         } | ||||
|       }).catchError((e) { | ||||
|         TheLogger.error("Error loading $entityId history: $e"); | ||||
|         Logger.e("Error loading $entityId history: $e"); | ||||
|         if (!_disposed) { | ||||
|           setState(() { | ||||
|             _history = []; | ||||
|             _needToUpdateHistory = false; | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| @@ -122,7 +127,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> { | ||||
|       } | ||||
|  | ||||
|       default: { | ||||
|         TheLogger.debug("  Simple selected as default"); | ||||
|         Logger.d("  Simple selected as default"); | ||||
|         return SimpleStateHistoryChartWidget( | ||||
|           rawHistory: _history, | ||||
|         ); | ||||
| @@ -131,4 +136,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> { | ||||
|  | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _disposed = true; | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										19
									
								
								lib/entity_widgets/missed_entity.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class MissedEntityWidget extends StatelessWidget { | ||||
|   MissedEntityWidget({ | ||||
|     Key key | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityModel entityModel = EntityModel.of(context); | ||||
|     return Container( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.all(5.0), | ||||
|           child: Text("Entity not available: ${entityModel.entityWrapper.entity.entityId}"), | ||||
|         ), | ||||
|         color: Colors.amber[100], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class ButtonStateWidget extends StatelessWidget { | ||||
|  | ||||
|   void _setNewState(Entity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent(entity.domain, "turn_on", entity.entityId, null)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     return SizedBox( | ||||
|       height: 34.0, | ||||
|       child: FlatButton( | ||||
|         onPressed: (() { | ||||
|           _setNewState(entityModel.entityWrapper.entity); | ||||
|         }), | ||||
|         child: Text( | ||||
|           "EXECUTE", | ||||
|           textAlign: TextAlign.right, | ||||
|           style: | ||||
|           new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), | ||||
|         ), | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -24,7 +24,7 @@ class CoverStateWidget extends StatelessWidget { | ||||
|     if (entity.supportOpen) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"), | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-up"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: entity.canBeOpened ? () => _open(entity) : null)); | ||||
| @@ -36,7 +36,7 @@ class CoverStateWidget extends StatelessWidget { | ||||
|     if (entity.supportStop) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.createIconDataFromIconName("mdi:stop"), | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:stop"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: () => _stop(entity))); | ||||
| @@ -48,7 +48,7 @@ class CoverStateWidget extends StatelessWidget { | ||||
|     if (entity.supportClose) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"), | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-down"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: entity.canBeClosed ? () => _close(entity) : null)); | ||||
|   | ||||
| @@ -54,7 +54,7 @@ class DateTimeStateWidget extends StatelessWidget { | ||||
|         } | ||||
|       }); | ||||
|     } else { | ||||
|       TheLogger.warning( "${entity.entityId} has no date and no time"); | ||||
|       Logger.w( "${entity.entityId} has no date and no time"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,10 @@ part of '../../main.dart'; | ||||
|  | ||||
| class LockStateWidget extends StatelessWidget { | ||||
|  | ||||
|   final bool assumedState; | ||||
|  | ||||
|   const LockStateWidget({Key key, this.assumedState: false}) : super(key: key); | ||||
|  | ||||
|   void _lock(Entity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null)); | ||||
|   } | ||||
| @@ -14,6 +18,35 @@ class LockStateWidget extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final LockEntity entity = entityModel.entityWrapper.entity; | ||||
|     if (assumedState) { | ||||
|       return Row( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|         SizedBox( | ||||
|         height: 34.0, | ||||
|         child: FlatButton( | ||||
|           onPressed: () => _unlock(entity), | ||||
|           child: Text("UNLOCK", | ||||
|               textAlign: TextAlign.right, | ||||
|               style: | ||||
|               new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), | ||||
|             ), | ||||
|           ) | ||||
|         ), | ||||
|         SizedBox( | ||||
|             height: 34.0, | ||||
|             child: FlatButton( | ||||
|               onPressed: () => _lock(entity), | ||||
|               child: Text("LOCK", | ||||
|                 textAlign: TextAlign.right, | ||||
|                 style: | ||||
|                 new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), | ||||
|               ), | ||||
|             ) | ||||
|         ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       return SizedBox( | ||||
|           height: 34.0, | ||||
|           child: FlatButton( | ||||
| @@ -30,3 +63,4 @@ class LockStateWidget extends StatelessWidget { | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -5,23 +5,39 @@ class SimpleEntityState extends StatelessWidget { | ||||
|   final bool expanded; | ||||
|   final TextAlign textAlign; | ||||
|   final EdgeInsetsGeometry padding; | ||||
|   final int maxLines; | ||||
|   final String customValue; | ||||
|  | ||||
|   const SimpleEntityState({Key key, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0)}) : super(key: key); | ||||
|   const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     String state; | ||||
|     if (customValue == null) { | ||||
|       state = entityModel.entityWrapper.entity.displayState ?? ""; | ||||
|       state = state.replaceAll("\n", "").replaceAll("\t", " ").trim(); | ||||
|     } else { | ||||
|       state = customValue; | ||||
|     } | ||||
|     TextStyle textStyle =  TextStyle( | ||||
|       fontSize: Sizes.stateFontSize, | ||||
|     ); | ||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) { | ||||
|       textStyle = textStyle.apply(color: Colors.blue); | ||||
|     } | ||||
|     while (state.contains("  ")){ | ||||
|       state = state.replaceAll("  ", " "); | ||||
|     } | ||||
|     Widget result = Padding( | ||||
|       padding: padding, | ||||
|       child: Text( | ||||
|         "${entityModel.entityWrapper.entity.state} ${entityModel.entityWrapper.entity.unitOfMeasurement}", | ||||
|         "$state ${entityModel.entityWrapper.entity.unitOfMeasurement}", | ||||
|         textAlign: textAlign, | ||||
|         maxLines: 10, | ||||
|         maxLines: maxLines, | ||||
|         overflow: TextOverflow.ellipsis, | ||||
|         softWrap: true, | ||||
|         style: new TextStyle( | ||||
|           fontSize: Sizes.stateFontSize, | ||||
|         ) | ||||
|         style: textStyle | ||||
|       ) | ||||
|     ); | ||||
|     if (expanded) { | ||||
|   | ||||
| @@ -71,13 +71,13 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> { | ||||
|           children: <Widget>[ | ||||
|             IconButton( | ||||
|               onPressed: () => _setNewState(false, entity), | ||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")), | ||||
|               icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash-off")), | ||||
|               color: newState == EntityState.on ? Colors.black : Colors.blue, | ||||
|               iconSize: Sizes.iconSize, | ||||
|             ), | ||||
|             IconButton( | ||||
|                 onPressed: () => _setNewState(true, entity), | ||||
|                 icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")), | ||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash")), | ||||
|                 color: newState == EntityState.on ? Colors.blue : Colors.black, | ||||
|                 iconSize: Sizes.iconSize | ||||
|             ) | ||||
|   | ||||
| @@ -85,7 +85,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> { | ||||
|             }), | ||||
|       ); | ||||
|     } else { | ||||
|       TheLogger.warning( "Unsupported input mode for ${entity.entityId}"); | ||||
|       Logger.w( "Unsupported input mode for ${entity.entityId}"); | ||||
|       return SimpleEntityState(); | ||||
|     } | ||||
|   } | ||||
|   | ||||
							
								
								
									
										65
									
								
								lib/entity_widgets/state/timer_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,65 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class TimerState extends StatefulWidget  { | ||||
|   //final bool expanded; | ||||
|   //final TextAlign textAlign; | ||||
|   //final EdgeInsetsGeometry padding; | ||||
|   //final int maxLines; | ||||
|  | ||||
|   const TimerState({Key key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _TimerStateState createState() => _TimerStateState(); | ||||
|  | ||||
| } | ||||
|  | ||||
| class _TimerStateState extends State<TimerState> { | ||||
|  | ||||
|   Timer timer; | ||||
|   Duration remaining = Duration(seconds: 0); | ||||
|  | ||||
|   void checkState(TimerEntity entity) { | ||||
|     if (entity.state == EntityState.active) { | ||||
|       //Logger.d("Timer is active"); | ||||
|       if (timer == null || !timer.isActive) { | ||||
|         timer = Timer.periodic(Duration(seconds: 1), (timer) { | ||||
|           setState(() { | ||||
|             try { | ||||
|               int passed = DateTime | ||||
|                   .now() | ||||
|                   .difference(entity._lastUpdated) | ||||
|                   .inSeconds; | ||||
|               remaining = Duration(seconds: entity.duration.inSeconds - passed); | ||||
|             } catch (e) { | ||||
|               Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}"); | ||||
|               remaining = Duration(seconds: 0); | ||||
|             } | ||||
|           }); | ||||
|         }); | ||||
|       } | ||||
|     } else { | ||||
|       timer?.cancel(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     EntityModel model = EntityModel.of(context); | ||||
|     TimerEntity entity = model.entityWrapper.entity; | ||||
|     checkState(entity); | ||||
|     if (entity.state != EntityState.active) { | ||||
|       return SimpleEntityState(); | ||||
|     } else { | ||||
|       return SimpleEntityState( | ||||
|         customValue: "${remaining.toString().split('.')[0]}", | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     timer?.cancel(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,44 +1,27 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class HomeAssistant { | ||||
|   String _webSocketAPIEndpoint; | ||||
|   String _password; | ||||
|   String _authType; | ||||
|  | ||||
|   final Connection connection = Connection(); | ||||
|  | ||||
|   bool _useLovelace = false; | ||||
|   //bool isSettingsLoaded = false; | ||||
|  | ||||
|  | ||||
|  | ||||
|   IOWebSocketChannel _hassioChannel; | ||||
|   SendMessageQueue _messageQueue; | ||||
|  | ||||
|   int _currentMessageId = 0; | ||||
|   int _statesMessageId = 0; | ||||
|   int _servicesMessageId = 0; | ||||
|   int _subscriptionMessageId = 0; | ||||
|   int _configMessageId = 0; | ||||
|   int _userInfoMessageId = 0; | ||||
|   int _lovelaceMessageId = 0; | ||||
|   EntityCollection entities; | ||||
|   HomeAssistantUI ui; | ||||
|   Map _instanceConfig = {}; | ||||
|   String _userName; | ||||
|   String hostname; | ||||
|   HSVColor savedColor; | ||||
|  | ||||
|   Map _rawLovelaceData; | ||||
|  | ||||
|   Completer _fetchCompleter; | ||||
|   Completer _statesCompleter; | ||||
|   Completer _servicesCompleter; | ||||
|   Completer _lovelaceCompleter; | ||||
|   Completer _configCompleter; | ||||
|   Completer _connectionCompleter; | ||||
|   Completer _userInfoCompleter; | ||||
|   Timer _connectionTimer; | ||||
|   Timer _fetchTimer; | ||||
|   bool autoReconnect = false; | ||||
|   List<Panel> panels = []; | ||||
|  | ||||
|   StreamSubscription _socketSubscription; | ||||
|  | ||||
|   int messageExpirationTime = 30; //seconds | ||||
|   Duration fetchTimeout = Duration(seconds: 30); | ||||
|   Duration connectTimeout = Duration(seconds: 15); | ||||
|  | ||||
|   String get locationName { | ||||
|     if (_useLovelace) { | ||||
| @@ -49,118 +32,39 @@ class HomeAssistant { | ||||
|   } | ||||
|   String get userName => _userName ?? locationName; | ||||
|   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; | ||||
|  | ||||
|   HomeAssistant() { | ||||
|     entities = EntityCollection(); | ||||
|     _messageQueue = SendMessageQueue(messageExpirationTime); | ||||
|   HomeAssistant(); | ||||
|  | ||||
|   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, String authType, bool useLovelace) { | ||||
|     _webSocketAPIEndpoint = url; | ||||
|     _password = password; | ||||
|     _authType = authType; | ||||
|     _useLovelace = useLovelace; | ||||
|     TheLogger.debug( "Use lovelace is $_useLovelace"); | ||||
|   } | ||||
|   Completer _fetchCompleter; | ||||
|  | ||||
|   Future fetch() { | ||||
|     if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) { | ||||
|       TheLogger.warning("Previous fetch is not complited"); | ||||
|     } else { | ||||
|       _fetchCompleter = new Completer(); | ||||
|       _fetchTimer = Timer(fetchTimeout, () { | ||||
|         TheLogger.error( "Data fetching timeout"); | ||||
|         disconnect().then((_) { | ||||
|           _completeFetching({ | ||||
|             "errorCode": 9, | ||||
|             "errorMessage": "Couldn't get data from server" | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|       _connection().then((r) { | ||||
|         _getData(); | ||||
|       }).catchError((e) { | ||||
|         _completeFetching(e); | ||||
|       }); | ||||
|     } | ||||
|     if (_fetchCompleter != null && !_fetchCompleter.isCompleted) { | ||||
|       Logger.w("Previous data fetch is not completed yet"); | ||||
|       return _fetchCompleter.future; | ||||
|     } | ||||
|  | ||||
|   disconnect() async { | ||||
|     if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) { | ||||
|       await _hassioChannel.sink.close().timeout(Duration(seconds: 3), | ||||
|         onTimeout: () => TheLogger.debug( "Socket sink closed") | ||||
|       ); | ||||
|       await _socketSubscription.cancel(); | ||||
|       _hassioChannel = null; | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
|   Future _connection() { | ||||
|     if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) { | ||||
|       TheLogger.debug("Previous connection is not complited"); | ||||
|     } else { | ||||
|       if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { | ||||
|         _connectionCompleter = new Completer(); | ||||
|         autoReconnect = false; | ||||
|         disconnect().then((_){ | ||||
|           TheLogger.debug( "Socket connecting..."); | ||||
|           _connectionTimer = Timer(connectTimeout, () { | ||||
|             TheLogger.error( "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() { | ||||
|     TheLogger.debug("Socket disconnected. Automatic reconnect is $autoReconnect"); | ||||
|     if (autoReconnect) { | ||||
|       _reconnect(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _handleSocketError(e) { | ||||
|     TheLogger.error("Socket stream Error: $e"); | ||||
|     TheLogger.debug("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 { | ||||
|     _fetchCompleter = Completer(); | ||||
|     List<Future> futures = []; | ||||
|     futures.add(_getStates()); | ||||
|     if (_useLovelace) { | ||||
| @@ -169,254 +73,111 @@ class HomeAssistant { | ||||
|     futures.add(_getConfig()); | ||||
|     futures.add(_getServices()); | ||||
|     futures.add(_getUserInfo()); | ||||
|     try { | ||||
|       await Future.wait(futures); | ||||
|     futures.add(_getPanels()); | ||||
|     Future.wait(futures).then((_) { | ||||
|       _createUI(); | ||||
|       _completeFetching(null); | ||||
|     } catch (error) { | ||||
|       _completeFetching(error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _completeFetching(error) { | ||||
|     _fetchTimer.cancel(); | ||||
|     _completeConnecting(error); | ||||
|     if (!_fetchCompleter.isCompleted) { | ||||
|       if (error != null) { | ||||
|         _fetchCompleter.completeError(error); | ||||
|       } else { | ||||
|         autoReconnect = true; | ||||
|         TheLogger.debug( "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") { | ||||
|       _sendAuthMessageRaw('{"type": "auth","$_authType": "$_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") { | ||||
|       TheLogger.debug("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}"); | ||||
|       if (data["id"] == _configMessageId) { | ||||
|         _parseConfig(data); | ||||
|       } else if (data["id"] == _statesMessageId) { | ||||
|         _parseEntities(data); | ||||
|       } else if (data["id"] == _lovelaceMessageId) { | ||||
|         _handleLovelace(data); | ||||
|       } else if (data["id"] == _servicesMessageId) { | ||||
|         _parseServices(data); | ||||
|       } else if (data["id"] == _userInfoMessageId) { | ||||
|         _parseUserInfo(data); | ||||
|       } | ||||
|     } else if (data["type"] == "event") { | ||||
|       if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) { | ||||
|         TheLogger.debug("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}"); | ||||
|         _handleEntityStateChange(data["event"]["data"]); | ||||
|       } else if (data["event"] != null) { | ||||
|         TheLogger.warning("Unhandled event type: ${data["event"]["event_type"]}"); | ||||
|       } else { | ||||
|         TheLogger.error("Event is null: $message"); | ||||
|       } | ||||
|     } else { | ||||
|       TheLogger.warning("Unknown message type: $message"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _sendSubscribe() { | ||||
|     _incrementMessageId(); | ||||
|     _subscriptionMessageId = _currentMessageId; | ||||
|     _sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false); | ||||
|   } | ||||
|  | ||||
|   Future _getConfig() { | ||||
|     _configCompleter = new Completer(); | ||||
|     _incrementMessageId(); | ||||
|     _configMessageId = _currentMessageId; | ||||
|     _sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false); | ||||
|  | ||||
|     return _configCompleter.future; | ||||
|   } | ||||
|  | ||||
|   Future _getStates() { | ||||
|     _statesCompleter = new Completer(); | ||||
|     _incrementMessageId(); | ||||
|     _statesMessageId = _currentMessageId; | ||||
|     _sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false); | ||||
|  | ||||
|     return _statesCompleter.future; | ||||
|   } | ||||
|  | ||||
|   Future _getLovelace() { | ||||
|     _lovelaceCompleter = new Completer(); | ||||
|     _incrementMessageId(); | ||||
|     _lovelaceMessageId = _currentMessageId; | ||||
|     _sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false); | ||||
|  | ||||
|     return _lovelaceCompleter.future; | ||||
|   } | ||||
|  | ||||
|   Future _getUserInfo() { | ||||
|     _userInfoCompleter = new Completer(); | ||||
|     _incrementMessageId(); | ||||
|     _userInfoMessageId = _currentMessageId; | ||||
|     _sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false); | ||||
|  | ||||
|     return _userInfoCompleter.future; | ||||
|   } | ||||
|  | ||||
|   Future _getServices() { | ||||
|     _servicesCompleter = new Completer(); | ||||
|     _incrementMessageId(); | ||||
|     _servicesMessageId = _currentMessageId; | ||||
|     _sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false); | ||||
|  | ||||
|     return _servicesCompleter.future; | ||||
|   } | ||||
|  | ||||
|   _incrementMessageId() { | ||||
|     _currentMessageId += 1; | ||||
|   } | ||||
|  | ||||
|   void _sendAuthMessageRaw(String message) { | ||||
|     TheLogger.debug( "[Sending] ==> auth request"); | ||||
|     _hassioChannel.sink.add(message); | ||||
|   } | ||||
|  | ||||
|   _sendMessageRaw(String message, bool queued) { | ||||
|     var sendCompleter = Completer(); | ||||
|     if (queued) _messageQueue.add(message); | ||||
|     _connection().then((r) { | ||||
|       _messageQueue.getActualMessages().forEach((message){ | ||||
|         TheLogger.debug( "[Sending queued] ==> $message"); | ||||
|         _hassioChannel.sink.add(message); | ||||
|       }); | ||||
|       if (!queued) { | ||||
|         TheLogger.debug( "[Sending] ==> $message"); | ||||
|         _hassioChannel.sink.add(message); | ||||
|       } | ||||
|       sendCompleter.complete(); | ||||
|     }).catchError((e) { | ||||
|       sendCompleter.completeError(e); | ||||
|       _fetchCompleter.completeError(e); | ||||
|     }); | ||||
|     return sendCompleter.future; | ||||
|     return _fetchCompleter.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"'; | ||||
|           } | ||||
|   Future logout() async { | ||||
|     Logger.d("Logging out..."); | ||||
|     await connection.logout().then((_) { | ||||
|       ui?.clear(); | ||||
|       entities?.clear(); | ||||
|     }); | ||||
|   } | ||||
|       message += '}}'; | ||||
|     } else { | ||||
|       message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"'; | ||||
|       if (additionalParams != null && additionalParams.isNotEmpty) { | ||||
|         message += ', "service_data": {'; | ||||
|         bool first = true; | ||||
|         additionalParams.forEach((name, value) { | ||||
|           if (!first) { | ||||
|             message += ', '; | ||||
|           } | ||||
|           if ((value is double) || (value is int) || (value is List)) { | ||||
|             message += '"$name" : $value'; | ||||
|           } else { | ||||
|             message += '"$name" : "$value"'; | ||||
|           } | ||||
|           first = false; | ||||
|         }); | ||||
|  | ||||
|         message += '}'; | ||||
|   Future _getConfig() async { | ||||
|     await connection.sendSocketMessage(type: "get_config").then((data) { | ||||
|       _instanceConfig = Map.from(data); | ||||
|     }).catchError((e) { | ||||
|       throw {"errorCode": 1, "errorMessage": "Error getting config: $e"}; | ||||
|     }); | ||||
|   } | ||||
|       message += '}'; | ||||
|  | ||||
|   Future _getStates() async { | ||||
|     await connection.sendSocketMessage(type: "get_states").then( | ||||
|             (data) => entities.parse(data) | ||||
|     ).catchError((e) { | ||||
|       throw {"errorCode": 1, "errorMessage": "Error getting states: $e"}; | ||||
|     }); | ||||
|   } | ||||
|     return _sendMessageRaw(message, true); | ||||
|  | ||||
|   Future _getLovelace() async { | ||||
|     await connection.sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) { | ||||
|       throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"}; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future _getUserInfo() async { | ||||
|     _userName = null; | ||||
|     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 { | ||||
|     await connection.sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) { | ||||
|       Logger.w("Can't get services: ${e}"); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future _getPanels() async { | ||||
|     panels.clear(); | ||||
|     await connection.sendSocketMessage(type: "get_panels").then((data) { | ||||
|       data.forEach((k,v) { | ||||
|         String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}"; | ||||
|         panels.add(Panel( | ||||
|             id: k, | ||||
|             type: v["component_name"], | ||||
|             title: title, | ||||
|             urlPath: v["url_path"], | ||||
|             config: v["config"], | ||||
|             icon: v["icon"] | ||||
|         ) | ||||
|         ); | ||||
|       }); | ||||
|     }).catchError((e) { | ||||
|       throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"}; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _handleEntityStateChange(Map eventData) { | ||||
|     //TheLogger.debug( "New state for ${eventData['entity_id']}"); | ||||
|     Map data = Map.from(eventData); | ||||
|     entities.updateState(data); | ||||
|     eventBus.fire(new StateChangedEvent(data["entity_id"], null)); | ||||
|   } | ||||
|  | ||||
|   void _parseConfig(Map data) { | ||||
|     if (data["success"] == true) { | ||||
|       _instanceConfig = Map.from(data["result"]); | ||||
|       _configCompleter.complete(); | ||||
|     } else { | ||||
|       _configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]}); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _parseUserInfo(Map data) { | ||||
|     if (data["success"] == true) { | ||||
|       _userName = data["result"]["name"]; | ||||
|     } else { | ||||
|       _userName = null; | ||||
|       TheLogger.warning("There was an error getting current user: $data"); | ||||
|     } | ||||
|     _userInfoCompleter.complete(); | ||||
|   } | ||||
|  | ||||
|   void _parseServices(response) { | ||||
|     _servicesCompleter.complete(); | ||||
|   } | ||||
|  | ||||
|   void _handleLovelace(response) { | ||||
|     if (response["success"] == true) { | ||||
|       _rawLovelaceData = response["result"]; | ||||
|     } else { | ||||
|       TheLogger.error("There was an error getting Lovelace config: $response"); | ||||
|       _rawLovelaceData = null; | ||||
|     } | ||||
|     _lovelaceCompleter.complete(); | ||||
|     eventBus.fire(new StateChangedEvent( | ||||
|       entityId: data["entity_id"], | ||||
|       needToRebuildUI: entities.updateState(data) | ||||
|     )); | ||||
|   } | ||||
|  | ||||
|   void _parseLovelace() { | ||||
|       TheLogger.debug("--Title: ${_rawLovelaceData["title"]}"); | ||||
|       Logger.d("--Title: ${_rawLovelaceData["title"]}"); | ||||
|       ui.title = _rawLovelaceData["title"]; | ||||
|       int viewCounter = 0; | ||||
|       TheLogger.debug("--Views count: ${_rawLovelaceData['views'].length}"); | ||||
|       Logger.d("--Views count: ${_rawLovelaceData['views'].length}"); | ||||
|       _rawLovelaceData["views"].forEach((rawView){ | ||||
|         TheLogger.debug("----view id: ${rawView['id']}"); | ||||
|         Logger.d("----view id: ${rawView['id']}"); | ||||
|         HAView view = HAView( | ||||
|             count: viewCounter, | ||||
|             id: "${rawView['id']}", | ||||
|             name: rawView['title'], | ||||
|             iconName: rawView['icon'] | ||||
|         ); | ||||
|  | ||||
|         if (rawView['badges'] != null && rawView['badges'] is List) { | ||||
|           rawView['badges'].forEach((entity) { | ||||
|             if (entities.isExist(entity)) { | ||||
|               Entity e = entities.get(entity); | ||||
|               view.badges.add(e); | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|  | ||||
|         view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? [])); | ||||
|         ui.views.add( | ||||
|             view | ||||
| @@ -428,42 +189,91 @@ class HomeAssistant { | ||||
|   List<HACard> _createLovelaceCards(List rawCards) { | ||||
|     List<HACard> result = []; | ||||
|     rawCards.forEach((rawCard){ | ||||
|       if (rawCard["cards"] != null) { | ||||
|         result.addAll(_createLovelaceCards(rawCard["cards"])); | ||||
|       } else { | ||||
|       try { | ||||
|         bool isThereCardOptionsInside = rawCard["card"] != null; | ||||
|         HACard card = HACard( | ||||
|             id: "card", | ||||
|             name: rawCard["title"], | ||||
|             type: rawCard['type'], | ||||
|             columnsCount: rawCard['columns'] ?? 4, | ||||
|             showName: rawCard['show_name'] ?? true, | ||||
|             showState: rawCard['show_state'] ?? true, | ||||
|             name: isThereCardOptionsInside ? rawCard["card"]["title"] ?? | ||||
|                 rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"], | ||||
|             type: isThereCardOptionsInside | ||||
|                 ? rawCard["card"]['type'] | ||||
|                 : rawCard['type'], | ||||
|             columnsCount: isThereCardOptionsInside | ||||
|                 ? rawCard["card"]['columns'] ?? 4 | ||||
|                 : rawCard['columns'] ?? 4, | ||||
|             showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ?? | ||||
|                 true : rawCard['show_name'] ?? true, | ||||
|             showState: isThereCardOptionsInside | ||||
|                 ? rawCard["card"]['show_state'] ?? true | ||||
|                 : rawCard['show_state'] ?? true, | ||||
|             showEmpty: rawCard['show_empty'] ?? true, | ||||
|             stateFilter: rawCard['state_filter'] ?? [], | ||||
|             states: rawCard['states'], | ||||
|             content: rawCard['content'] | ||||
|         ); | ||||
|         if (rawCard["cards"] != null) { | ||||
|           card.childCards = _createLovelaceCards(rawCard["cards"]); | ||||
|         } | ||||
|         rawCard["entities"]?.forEach((rawEntity) { | ||||
|           if (rawEntity is String) { | ||||
|             if (entities.isExist(rawEntity)) { | ||||
|               card.entities.add(EntityWrapper(entity: entities.get(rawEntity))); | ||||
|             } else { | ||||
|               card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity))); | ||||
|             } | ||||
|           } else { | ||||
|             if (entities.isExist(rawEntity["entity"])) { | ||||
|             if (rawEntity["type"] == "divider") { | ||||
|               card.entities.add(EntityWrapper(entity: Entity.divider())); | ||||
|             } else if (rawEntity["type"] == "section") { | ||||
|               card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? ""))); | ||||
|             } else if (rawEntity["type"] == "call-service") { | ||||
|               Map uiActionData = { | ||||
|                 "tap_action": { | ||||
|                   "action": EntityUIAction.callService, | ||||
|                   "service": rawEntity["service"], | ||||
|                   "service_data": rawEntity["service_data"] | ||||
|                 }, | ||||
|                 "hold_action": EntityUIAction.none | ||||
|               }; | ||||
|               card.entities.add(EntityWrapper( | ||||
|                   entity: Entity.callService( | ||||
|                     icon: rawEntity["icon"], | ||||
|                     name: rawEntity["name"], | ||||
|                     service: rawEntity["service"], | ||||
|                     actionName: rawEntity["action_name"] | ||||
|                   ), | ||||
|                 uiAction: EntityUIAction(rawEntityData: uiActionData) | ||||
|               ) | ||||
|               ); | ||||
|             } else if (rawEntity["type"] == "weblink") { | ||||
|               Map uiActionData = { | ||||
|                 "tap_action": { | ||||
|                   "action": EntityUIAction.navigate, | ||||
|                   "service": rawEntity["url"] | ||||
|                 }, | ||||
|                 "hold_action": EntityUIAction.none | ||||
|               }; | ||||
|               card.entities.add(EntityWrapper( | ||||
|                   entity: Entity.weblink( | ||||
|                       icon: rawEntity["icon"], | ||||
|                       name: rawEntity["name"], | ||||
|                       url: rawEntity["url"] | ||||
|                   ), | ||||
|                   uiAction: EntityUIAction(rawEntityData: uiActionData) | ||||
|               ) | ||||
|               ); | ||||
|             } else if (entities.isExist(rawEntity["entity"])) { | ||||
|               Entity e = entities.get(rawEntity["entity"]); | ||||
|               String tapAction = EntityTapAction.moreInfo; | ||||
|               String holdAction = EntityTapAction.none; | ||||
|               if (card.type == CardType.glance) { | ||||
|                 tapAction = rawEntity["tap_action"] ?? EntityTapAction.moreInfo; | ||||
|                 holdAction = rawEntity["hold_action"] ?? EntityTapAction.none; | ||||
|               } | ||||
|               card.entities.add( | ||||
|                   EntityWrapper( | ||||
|                       entity: e, | ||||
|                       displayName: rawEntity["name"], | ||||
|                       icon: rawEntity["icon"], | ||||
|                     tapAction: tapAction, | ||||
|                     holdAction: holdAction, | ||||
|                     tapActionService: rawEntity["service"], | ||||
|                     tapActionServiceData: rawEntity["service_data"] ?? {"entity_id": e.entityId} | ||||
|                       uiAction: EntityUIAction(rawEntityData: rawEntity) | ||||
|                   ) | ||||
|               ); | ||||
|             } else { | ||||
|               card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"]))); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
| @@ -471,41 +281,45 @@ class HomeAssistant { | ||||
|           var en = rawCard["entity"]; | ||||
|           if (en is String) { | ||||
|             if (entities.isExist(en)) { | ||||
|               card.linkedEntity = EntityWrapper(entity: entities.get(en)); | ||||
|               Entity e = entities.get(en); | ||||
|               card.linkedEntityWrapper = EntityWrapper( | ||||
|                   entity: e, | ||||
|                   icon: rawCard["icon"], | ||||
|                   displayName: rawCard["name"], | ||||
|                   uiAction: EntityUIAction(rawEntityData: rawCard) | ||||
|               ); | ||||
|             } else { | ||||
|               card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en)); | ||||
|             } | ||||
|           } else { | ||||
|             if (entities.isExist(en["entity"])) { | ||||
|               card.linkedEntity = EntityWrapper( | ||||
|                 entity: entities.get(en["entity"]), | ||||
|               Entity e = entities.get(en["entity"]); | ||||
|               card.linkedEntityWrapper = EntityWrapper( | ||||
|                   entity: e, | ||||
|                   icon: en["icon"], | ||||
|                 displayName: en["name"] | ||||
|                   displayName: en["name"], | ||||
|                   uiAction: EntityUIAction(rawEntityData: rawCard) | ||||
|               ); | ||||
|             } else { | ||||
|               card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"])); | ||||
|             } | ||||
|           } | ||||
|  | ||||
|         } | ||||
|         result.add(card); | ||||
|       } catch (e) { | ||||
|           Logger.e("There was an error parsing card: ${e.toString()}"); | ||||
|       } | ||||
|     }); | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   void _parseEntities(response) async { | ||||
|     if (response["success"] == false) { | ||||
|       _statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]}); | ||||
|       return; | ||||
|     } | ||||
|     entities.parse(response["result"]); | ||||
|     _statesCompleter.complete(); | ||||
|   } | ||||
|  | ||||
|   void _createUI() { | ||||
|     ui = HomeAssistantUI(); | ||||
|     if ((_useLovelace) && (_rawLovelaceData != null)) { | ||||
|       TheLogger.debug("Creating Lovelace UI"); | ||||
|       Logger.d("Creating Lovelace UI"); | ||||
|       _parseLovelace(); | ||||
|     } else { | ||||
|       TheLogger.debug("Creating group-based UI"); | ||||
|       Logger.d("Creating group-based UI"); | ||||
|       int viewCounter = 0; | ||||
|       if (!entities.hasDefaultView) { | ||||
|         HAView view = HAView( | ||||
| @@ -535,38 +349,12 @@ class HomeAssistant { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget buildViews(BuildContext context, bool lovelace) { | ||||
|     return ui.build(context); | ||||
|   } | ||||
|  | ||||
|   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"; | ||||
|     TheLogger.debug("[Sending] ==> $url"); | ||||
|     http.Response historyResponse; | ||||
|     if (_authType == "access_token") { | ||||
|       historyResponse = await http.get(url, headers: { | ||||
|         "authorization": "Bearer $_password", | ||||
|         "Content-Type": "application/json" | ||||
|       }); | ||||
|     } else { | ||||
|       historyResponse = await http.get(url, headers: { | ||||
|         "X-HA-Access": "$_password", | ||||
|         "Content-Type": "application/json" | ||||
|       }); | ||||
|     } | ||||
|     var history = json.decode(historyResponse.body); | ||||
|     if (history is List) { | ||||
|       TheLogger.debug( "[Received] <== ${history.first.length} history recors"); | ||||
|       return history; | ||||
|     } else { | ||||
|       return []; | ||||
|     } | ||||
|   Widget buildViews(BuildContext context, TabController tabController) { | ||||
|     return ui.build(context, tabController); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /* | ||||
| class SendMessageQueue { | ||||
|   int _messageTimeout; | ||||
|   List<HAMessage> _queue = []; | ||||
| @@ -605,4 +393,4 @@ class HAMessage { | ||||
|   bool isExpired() { | ||||
|     return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout; | ||||
|   } | ||||
| } | ||||
| }*/ | ||||
|   | ||||
| @@ -19,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> { | ||||
|   } | ||||
|  | ||||
|   _loadLog() async { | ||||
|     _logData = TheLogger.getLog(); | ||||
|     _logData = Logger.getLog(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   | ||||
							
								
								
									
										415
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						| @@ -1,5 +1,6 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:async'; | ||||
| import 'dart:typed_data'; | ||||
| import 'package:flutter/rendering.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| @@ -7,17 +8,23 @@ import 'package:web_socket_channel/io.dart'; | ||||
| import 'package:event_bus/event_bus.dart'; | ||||
| import 'package:flutter/widgets.dart'; | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart' as urlLauncher; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:date_format/date_format.dart'; | ||||
| import 'package:http/http.dart' as http; | ||||
| import 'package:flutter_colorpicker/material_picker.dart'; | ||||
| import 'package:charts_flutter/flutter.dart' as charts; | ||||
| import 'package:progress_indicators/progress_indicators.dart'; | ||||
| import 'package:flutter_markdown/flutter_markdown.dart'; | ||||
| import 'package:flutter_svg/flutter_svg.dart'; | ||||
| import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; | ||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; | ||||
| import 'package:flutter_secure_storage/flutter_secure_storage.dart'; | ||||
|  | ||||
| part 'entity_class/const.dart'; | ||||
| part 'entity_class/entity.class.dart'; | ||||
| part 'entity_class/entity_wrapper.class.dart'; | ||||
| part 'entity_class/timer_entity.dart'; | ||||
| part 'entity_class/switch_entity.class.dart'; | ||||
| part 'entity_class/button_entity.class.dart'; | ||||
| part 'entity_class/text_entity.class.dart'; | ||||
| @@ -32,10 +39,15 @@ part 'entity_class/media_player_entity.class.dart'; | ||||
| part 'entity_class/lock_entity.class.dart'; | ||||
| part 'entity_class/group_entity.class.dart'; | ||||
| part 'entity_class/fan_entity.class.dart'; | ||||
| part 'entity_class/automation_entity.dart'; | ||||
| part 'entity_class/camera_entity.class.dart'; | ||||
| part 'entity_class/alarm_control_panel.class.dart'; | ||||
| part 'entity_widgets/common/badge.dart'; | ||||
| part 'entity_widgets/model_widgets.dart'; | ||||
| part 'entity_widgets/default_entity_container.dart'; | ||||
| part 'entity_widgets/missed_entity.dart'; | ||||
| part 'entity_widgets/glance_entity_container.dart'; | ||||
| part 'entity_widgets/button_entity_container.dart'; | ||||
| part 'entity_widgets/common/entity_attributes_list.dart'; | ||||
| part 'entity_widgets/entity_icon.dart'; | ||||
| part 'entity_widgets/entity_name.dart'; | ||||
| @@ -43,6 +55,9 @@ part 'entity_widgets/common/last_updated.dart'; | ||||
| part 'entity_widgets/common/mode_swicth.dart'; | ||||
| part 'entity_widgets/common/mode_selector.dart'; | ||||
| part 'entity_widgets/common/universal_slider.dart'; | ||||
| part 'entity_widgets/common/flat_service_button.dart'; | ||||
| part 'entity_widgets/common/light_color_picker.dart'; | ||||
| part 'entity_widgets/common/camera_stream_view.dart'; | ||||
| part 'entity_widgets/entity_colors.class.dart'; | ||||
| part 'entity_widgets/entity_page_container.dart'; | ||||
| part 'entity_widgets/history_chart/entity_history.dart'; | ||||
| @@ -56,45 +71,46 @@ part 'entity_widgets/controls/slider_controls.dart'; | ||||
| part 'entity_widgets/state/text_input_state.dart'; | ||||
| part 'entity_widgets/state/select_state.dart'; | ||||
| part 'entity_widgets/state/simple_state.dart'; | ||||
| part 'entity_widgets/state/timer_state.dart'; | ||||
| part 'entity_widgets/state/climate_state.dart'; | ||||
| part 'entity_widgets/state/cover_state.dart'; | ||||
| part 'entity_widgets/state/date_time_state.dart'; | ||||
| part 'entity_widgets/state/button_state.dart'; | ||||
| part 'entity_widgets/state/lock_state.dart'; | ||||
| part 'entity_widgets/controls/climate_controls.dart'; | ||||
| part 'entity_widgets/controls/cover_controls.dart'; | ||||
| part 'entity_widgets/controls/light_controls.dart'; | ||||
| part 'entity_widgets/controls/media_player_widgets.dart'; | ||||
| part 'entity_widgets/controls/fan_controls.dart'; | ||||
| part 'entity_widgets/controls/alarm_control_panel_controls.dart'; | ||||
| part 'settings.page.dart'; | ||||
| part 'panel.page.dart'; | ||||
| part 'home_assistant.class.dart'; | ||||
| part 'log.page.dart'; | ||||
| part 'entity.page.dart'; | ||||
| part 'utils.class.dart'; | ||||
| part 'mdi.class.dart'; | ||||
| part 'entity_collection.class.dart'; | ||||
| part 'auth_manager.class.dart'; | ||||
| part 'connection.class.dart'; | ||||
| part 'ui_class/ui.dart'; | ||||
| part 'ui_class/view.class.dart'; | ||||
| part 'ui_class/card.class.dart'; | ||||
| part 'ui_class/sizes_class.dart'; | ||||
| part 'ui_class/panel_class.dart'; | ||||
| part 'ui_widgets/view.dart'; | ||||
| part 'ui_widgets/entities_card.dart'; | ||||
| part 'ui_widgets/glance_card.dart'; | ||||
| part 'ui_widgets/unsupported_card.dart'; | ||||
| part 'ui_widgets/media_control_card.dart'; | ||||
| part 'ui_widgets/card_widget.dart'; | ||||
| part 'ui_widgets/card_header_widget.dart'; | ||||
| part 'ui_widgets/config_panel_widget.dart'; | ||||
|  | ||||
|  | ||||
| EventBus eventBus = new EventBus(); | ||||
| const String appName = "HA Client"; | ||||
| const appVersion = "0.3.10"; | ||||
|  | ||||
| String homeAssistantWebHost; | ||||
| const appVersion = "0.6.0-alpha1"; | ||||
|  | ||||
| void main() { | ||||
|   FlutterError.onError = (errorDetails) { | ||||
|     TheLogger.error( "${errorDetails.exception}"); | ||||
|     if (TheLogger.isInDebugMode) { | ||||
|     Logger.e( "${errorDetails.exception}"); | ||||
|     if (Logger.isInDebugMode) { | ||||
|       FlutterError.dumpErrorToConsole(errorDetails); | ||||
|     } | ||||
|   }; | ||||
| @@ -102,15 +118,17 @@ void main() { | ||||
|   runZoned(() { | ||||
|     runApp(new HAClientApp()); | ||||
|   }, onError: (error, stack) { | ||||
|     TheLogger.error("$error"); | ||||
|     TheLogger.error("$stack"); | ||||
|     if (TheLogger.isInDebugMode) { | ||||
|     Logger.e("$error"); | ||||
|     Logger.e("$stack"); | ||||
|     if (Logger.isInDebugMode) { | ||||
|       debugPrint("$stack"); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| class HAClientApp extends StatelessWidget { | ||||
|  | ||||
|   final HomeAssistant homeAssistant = HomeAssistant(); | ||||
|   // This widget is the root of your application. | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -121,8 +139,9 @@ class HAClientApp extends StatelessWidget { | ||||
|       ), | ||||
|       initialRoute: "/", | ||||
|       routes: { | ||||
|         "/": (context) => MainPage(title: 'HA Client'), | ||||
|         "/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,), | ||||
|         "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), | ||||
|         "/configuration": (context) => PanelPage(title: "Configuration"), | ||||
|         "/log-view": (context) => LogViewPage(title: "Log") | ||||
|       }, | ||||
|     ); | ||||
| @@ -130,91 +149,101 @@ class HAClientApp extends StatelessWidget { | ||||
| } | ||||
|  | ||||
| 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 HomeAssistant homeAssistant; | ||||
|  | ||||
|   @override | ||||
|   _MainPageState createState() => new _MainPageState(); | ||||
| } | ||||
|  | ||||
| class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|   HomeAssistant _homeAssistant; | ||||
|   //Map _instanceConfig; | ||||
|   String _webSocketApiEndpoint; | ||||
|   String _password; | ||||
|   String _authType; | ||||
|   //int _uiViewsCount = 0; | ||||
|   String _instanceHost; | ||||
| class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin { | ||||
|  | ||||
|   StreamSubscription _stateSubscription; | ||||
|   StreamSubscription _settingsSubscription; | ||||
|   StreamSubscription _serviceCallSubscription; | ||||
|   StreamSubscription _showEntityPageSubscription; | ||||
|   StreamSubscription _refreshDataSubscription; | ||||
|   StreamSubscription _showErrorSubscription; | ||||
|   bool _settingsLoaded = false; | ||||
|   bool _accountMenuExpanded = false; | ||||
|   bool _useLovelaceUI; | ||||
|   StreamSubscription _startAuthSubscription; | ||||
|   StreamSubscription _reloadUISubscription; | ||||
|   int _previousViewCount; | ||||
|   //final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _settingsLoaded = false; | ||||
|     //widget.homeAssistant = HomeAssistant(); | ||||
|     //_settingsLoaded = false; | ||||
|     WidgetsBinding.instance.addObserver(this); | ||||
|  | ||||
|     TheLogger.debug("<!!!> Creating new HomeAssistant instance"); | ||||
|     _homeAssistant = HomeAssistant(); | ||||
|  | ||||
|     _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { | ||||
|       TheLogger.debug("Settings change event: reconnect=${event.reconnect}"); | ||||
|       Logger.d("Settings change event: reconnect=${event.reconnect}"); | ||||
|       if (event.reconnect) { | ||||
|         _homeAssistant.disconnect().then((_){ | ||||
|           _initialLoad(); | ||||
|         }); | ||||
|         _reLoad(); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     _initialLoad(); | ||||
|   } | ||||
|  | ||||
|   void _initialLoad() { | ||||
|     _loadConnectionSettings().then((_){ | ||||
|     _showInfoBottomBar(progress: true,); | ||||
|     _subscribe(); | ||||
|       _refreshData(); | ||||
|     }, onError: (_) { | ||||
|       _showErrorBottomBar(message: _, errorCode: 5); | ||||
|     widget.homeAssistant.init().then((_){ | ||||
|       _fetchData(); | ||||
|     }, 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 | ||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||
|     TheLogger.debug("$state"); | ||||
|     if (state == AppLifecycleState.resumed && _settingsLoaded) { | ||||
|       _refreshData(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   _loadConnectionSettings() async { | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     String domain = prefs.getString('hassio-domain'); | ||||
|     String port = prefs.getString('hassio-port'); | ||||
|     _instanceHost = "$domain:$port"; | ||||
|     _webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; | ||||
|     homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; | ||||
|     _password = prefs.getString('hassio-password'); | ||||
|     _authType = prefs.getString('hassio-auth-type'); | ||||
|     _useLovelaceUI = prefs.getBool('use-lovelace') ?? false; | ||||
|     if ((domain == null) || (port == null) || (_password == null) || | ||||
|         (domain.length == 0) || (port.length == 0) || (_password.length == 0)) { | ||||
|       throw("Check connection settings"); | ||||
|     } else { | ||||
|       _settingsLoaded = true; | ||||
|     Logger.d("$state"); | ||||
|     if (state == AppLifecycleState.resumed) { | ||||
|       _reLoad(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   _subscribe() { | ||||
|     if (_stateSubscription == null) { | ||||
|       _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { | ||||
|         if (event.needToRebuildUI) { | ||||
|           Logger.d("New entity. Need to rebuild UI"); | ||||
|           _reLoad(); | ||||
|         } else { | ||||
|           setState(() {}); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|     if (_reloadUISubscription == null) { | ||||
|       _reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){ | ||||
|         _reLoad(); | ||||
|       }); | ||||
|     } | ||||
|     if (_serviceCallSubscription == null) { | ||||
| @@ -232,37 +261,64 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|           }); | ||||
|     } | ||||
|  | ||||
|     if (_refreshDataSubscription == null) { | ||||
|       _refreshDataSubscription = eventBus.on<RefreshDataEvent>().listen((event){ | ||||
|         _refreshData(); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (_showErrorSubscription == null) { | ||||
|       _showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){ | ||||
|         _showErrorBottomBar(message: event.text, errorCode: event.errorCode); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (_startAuthSubscription == null) { | ||||
|       _startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){ | ||||
|         _showOAuth(); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|   _refreshData() async { | ||||
|     _homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _authType, _useLovelaceUI); | ||||
|     _hideBottomBar(); | ||||
|     _showInfoBottomBar(progress: true,); | ||||
|     await _homeAssistant.fetch().then((result) { | ||||
|       _hideBottomBar(); | ||||
|     }).catchError((e) { | ||||
|       _setErrorState(e); | ||||
|  | ||||
|  | ||||
|     /*_firebaseMessaging.getToken().then((String token) { | ||||
|       //Logger.d("FCM token: $token"); | ||||
|       widget.homeAssistant.sendHTTPPost( | ||||
|           endPoint: '/api/notify.fcm-android', | ||||
|           jsonData:  '{"token": "$token"}' | ||||
|       ); | ||||
|     }); | ||||
|     eventBus.fire(RefreshDataFinishedEvent()); | ||||
|     _firebaseMessaging.configure( | ||||
|         onLaunch: (data) { | ||||
|           Logger.d("Notification [onLaunch]: $data"); | ||||
|         }, | ||||
|         onMessage: (data) { | ||||
|           Logger.d("Notification [onMessage]: $data"); | ||||
|         }, | ||||
|         onResume: (data) { | ||||
|           Logger.d("Notification [onResume]: $data"); | ||||
|         } | ||||
|     );*/ | ||||
|   } | ||||
|  | ||||
|   void _showOAuth() { | ||||
|     Navigator.push( | ||||
|         context, | ||||
|         MaterialPageRoute( | ||||
|           builder: (context) => WebviewScaffold( | ||||
|             url: "${widget.homeAssistant.connection.oauthUrl}", | ||||
|             appBar: new AppBar( | ||||
|               leading: IconButton( | ||||
|                   icon: Icon(Icons.help), | ||||
|                   onPressed: () => HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/docs#authentication") | ||||
|               ), | ||||
|               title: new Text("Login to your Home Assistant"), | ||||
|             ), | ||||
|           ), | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   _setErrorState(e) { | ||||
|     if (e is Error) { | ||||
|       TheLogger.error(e.toString()); | ||||
|       TheLogger.error("${e.stackTrace}"); | ||||
|       Logger.e(e.toString()); | ||||
|       Logger.e("${e.stackTrace}"); | ||||
|       _showErrorBottomBar( | ||||
|           message: "There was some error", | ||||
|           message: "Unknown error", | ||||
|           errorCode: 13 | ||||
|       ); | ||||
|     } else { | ||||
| @@ -273,19 +329,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) { | ||||
|   void _callService(String domain, String service, String entityId, Map additionalParams) { | ||||
|     _showInfoBottomBar( | ||||
|       message: "Calling $domain.$service", | ||||
|       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) { | ||||
|     Navigator.push( | ||||
|         context, | ||||
|         MaterialPageRoute( | ||||
|           builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant), | ||||
|           builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant), | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
| @@ -293,8 +349,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|   List<Tab> buildUIViewTabs() { | ||||
|     List<Tab> result = []; | ||||
|  | ||||
|       if (_homeAssistant.ui.views.isNotEmpty) { | ||||
|         _homeAssistant.ui.views.forEach((HAView view) { | ||||
|       if (widget.homeAssistant.ui.views.isNotEmpty) { | ||||
|         widget.homeAssistant.ui.views.forEach((HAView view) { | ||||
|           result.add(view.buildTab()); | ||||
|         }); | ||||
|       } | ||||
| @@ -306,16 +362,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|     List<Widget> menuItems = []; | ||||
|     menuItems.add( | ||||
|         UserAccountsDrawerHeader( | ||||
|           accountName: Text(_homeAssistant.userName), | ||||
|           accountEmail: Text(_instanceHost ?? "Not configured"), | ||||
|           onDetailsPressed: () { | ||||
|           accountName: Text(widget.homeAssistant.userName), | ||||
|           accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"), | ||||
|           /*onDetailsPressed: () { | ||||
|             setState(() { | ||||
|               _accountMenuExpanded = !_accountMenuExpanded; | ||||
|             }); | ||||
|           }, | ||||
|           },*/ | ||||
|           currentAccountPicture: CircleAvatar( | ||||
|             child: Text( | ||||
|               _homeAssistant.userAvatarText, | ||||
|               widget.homeAssistant.userAvatarText, | ||||
|               style: TextStyle( | ||||
|                   fontSize: 32.0 | ||||
|               ), | ||||
| @@ -323,20 +379,40 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|           ), | ||||
|         ) | ||||
|     ); | ||||
|     if (_accountMenuExpanded) { | ||||
|       if (widget.homeAssistant.panels.isNotEmpty) { | ||||
|         widget.homeAssistant.panels.forEach((Panel panel) { | ||||
|           if (!panel.isHidden) { | ||||
|             menuItems.add( | ||||
|                 new ListTile( | ||||
|                     leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)), | ||||
|                     title: Text("${panel.title}"), | ||||
|                     onTap: () => panel.handleOpen(context) | ||||
|                 ) | ||||
|             ); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|       //TODO check for loaded | ||||
|       menuItems.add( | ||||
|           new ListTile( | ||||
|             leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")), | ||||
|             title: Text("Open Web UI"), | ||||
|             onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost), | ||||
|           ) | ||||
|       ); | ||||
|       menuItems.addAll([ | ||||
|         Divider(), | ||||
|         ListTile( | ||||
|           leading: Icon(Icons.settings), | ||||
|           title: Text("Settings"), | ||||
|           leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")), | ||||
|           title: Text("Connection settings"), | ||||
|           onTap: () { | ||||
|             Navigator.of(context).pop(); | ||||
|             Navigator.of(context).pushNamed('/connection-settings'); | ||||
|             Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant}); | ||||
|           }, | ||||
|         ), | ||||
|         Divider(), | ||||
|         ) | ||||
|       ]); | ||||
|     } else { | ||||
|       menuItems.addAll([ | ||||
|         Divider(), | ||||
|         new ListTile( | ||||
|           leading: Icon(Icons.insert_drive_file), | ||||
|           title: Text("Log"), | ||||
| @@ -346,7 +422,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|           }, | ||||
|         ), | ||||
|         new ListTile( | ||||
|           leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")), | ||||
|           leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")), | ||||
|           title: Text("Report an issue"), | ||||
|           onTap: () { | ||||
|             Navigator.of(context).pop(); | ||||
| @@ -354,15 +430,23 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|           }, | ||||
|         ), | ||||
|         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( | ||||
|           aboutBoxChildren: <Widget>[ | ||||
|             GestureDetector( | ||||
|               onTap: () { | ||||
|                 Navigator.of(context).pop(); | ||||
|                 HAUtils.launchURL("http://www.keyboardcrumbs.io/"); | ||||
|                 HAUtils.launchURL("http://ha-client.homemade.systems/"); | ||||
|               }, | ||||
|               child: Text( | ||||
|                 "www.keyboardcrumbs.io", | ||||
|                 "ha-client.homemade.systems", | ||||
|                 style: TextStyle( | ||||
|                   color: Colors.blue, | ||||
|                   decoration: TextDecoration.underline | ||||
| @@ -371,11 +455,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|             ) | ||||
|           ], | ||||
|           applicationName: appName, | ||||
|           applicationVersion: appVersion, | ||||
|           applicationLegalese: "Keyboard Crumbs", | ||||
|           applicationVersion: appVersion | ||||
|         ) | ||||
|       ]); | ||||
|     } | ||||
|     return new Drawer( | ||||
|       child: ListView( | ||||
|         children: menuItems, | ||||
| @@ -428,7 +510,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|                 child: Text("Retry", style: textStyle), | ||||
|                 onPressed: () { | ||||
|                   //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||
|                   _refreshData(); | ||||
|                   _reLoad(); | ||||
|                 }, | ||||
|             ); | ||||
|             break; | ||||
| @@ -446,12 +528,32 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case 6: { | ||||
|         case 60: { | ||||
|           _bottomBarAction = FlatButton( | ||||
|               child: Text("Settings", style: textStyle), | ||||
|               child: Text("Login", style: textStyle), | ||||
|             onPressed: () { | ||||
|               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||
|               Navigator.pushNamed(context, '/connection-settings'); | ||||
|               _reLoad(); | ||||
|             }, | ||||
|           ); | ||||
|           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; | ||||
| @@ -462,52 +564,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|               child: Text("Refresh", style: textStyle), | ||||
|             onPressed: () { | ||||
|               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||
|               _refreshData(); | ||||
|               _reLoad(); | ||||
|             }, | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case 82: | ||||
|         case 81: | ||||
|         case 8: { | ||||
|           _bottomBarAction = FlatButton( | ||||
|               child: Text("Reconnect", style: textStyle), | ||||
|             onPressed: () { | ||||
|               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||
|               _refreshData(); | ||||
|               _reLoad(); | ||||
|             }, | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         default: { | ||||
|           _bottomBarAction = FlatButton( | ||||
|               child: Text("Reload", style: textStyle), | ||||
|             onPressed: () { | ||||
|               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||
|               _refreshData(); | ||||
|             }, | ||||
|           ); | ||||
|           _bottomBarAction = Container(width: 0.0, height: 0.0,); | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|       setState(() { | ||||
|         _bottomBarProgress = false; | ||||
|         _bottomBarText = "$message (code: $errorCode)"; | ||||
|         _bottomBarText = "$message"; | ||||
|         _showBottomBar = true; | ||||
|       }); | ||||
|       /*_scaffoldKey.currentState.hideCurrentSnackBar(); | ||||
|       _scaffoldKey.currentState.showSnackBar( | ||||
|         SnackBar( | ||||
|           content: Text("$message (code: $errorCode)"), | ||||
|           action: action, | ||||
|           duration: Duration(hours: 1), | ||||
|         ) | ||||
|       );*/ | ||||
|   } | ||||
|  | ||||
|   final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); | ||||
|  | ||||
|   Widget _buildScaffoldBody(bool empty) { | ||||
|     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( | ||||
|       headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||
|         return <Widget>[ | ||||
| @@ -515,17 +616,36 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|             floating: true, | ||||
|             pinned: true, | ||||
|             primary: true, | ||||
|             title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""), | ||||
|             title: Text(widget.homeAssistant.locationName ?? ""), | ||||
|             actions: <Widget>[ | ||||
|               IconButton( | ||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||
|                     "mdi:dots-vertical"), color: Colors.white,), | ||||
|                 onPressed: () { | ||||
|                   showMenu( | ||||
|                     position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0), | ||||
|                     context: context, | ||||
|                     items: popupMenuItems | ||||
|                   ).then((String val) { | ||||
|                     if (val == "reload") { | ||||
|                       _reLoad(); | ||||
|                     } else if (val == "logout") { | ||||
|                       widget.homeAssistant.logout().then((_) { | ||||
|                         _reLoad(); | ||||
|                       }); | ||||
|                     } | ||||
|                   }); | ||||
|                 } | ||||
|               ) | ||||
|             ], | ||||
|             leading: IconButton( | ||||
|               icon: Icon(Icons.menu), | ||||
|               onPressed: () { | ||||
|                 _scaffoldKey.currentState.openDrawer(); | ||||
|                 setState(() { | ||||
|                   _accountMenuExpanded = false; | ||||
|                 }); | ||||
|               }, | ||||
|             ), | ||||
|             bottom: empty ? null : TabBar( | ||||
|               controller: _viewsTabController, | ||||
|               tabs: buildUIViewTabs(), | ||||
|               isScrollable: true, | ||||
|             ), | ||||
| @@ -539,18 +659,20 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|             mainAxisAlignment: MainAxisAlignment.center, | ||||
|             children: [ | ||||
|               Icon( | ||||
|                 MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), | ||||
|                 MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"), | ||||
|                 size: 100.0, | ||||
|                 color: Colors.blue, | ||||
|                 color: Colors.black26, | ||||
|               ), | ||||
|             ] | ||||
|         ), | ||||
|       ) | ||||
|           : | ||||
|       _homeAssistant.buildViews(context, _useLovelaceUI), | ||||
|       widget.homeAssistant.buildViews(context, _viewsTabController), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   TabController _viewsTabController; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     Widget bottomBar; | ||||
| @@ -602,7 +724,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|       } | ||||
|     } | ||||
|     // This method is rerun every time setState is called. | ||||
|     if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) { | ||||
|     if (widget.homeAssistant.isNoViews) { | ||||
|       return Scaffold( | ||||
|         key: _scaffoldKey, | ||||
|         primary: false, | ||||
| @@ -616,9 +738,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|         drawer: _buildAppDrawer(), | ||||
|         primary: false, | ||||
|         bottomNavigationBar: bottomBar, | ||||
|         body: DefaultTabController( | ||||
|           length: _homeAssistant.ui?.views?.length ?? 0, | ||||
|         body: HomeAssistantModel( | ||||
|           child: _buildScaffoldBody(false), | ||||
|           homeAssistant: widget.homeAssistant | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
| @@ -626,14 +748,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     final flutterWebviewPlugin = new FlutterWebviewPlugin(); | ||||
|     flutterWebviewPlugin.dispose(); | ||||
|     WidgetsBinding.instance.removeObserver(this); | ||||
|     if (_stateSubscription != null) _stateSubscription.cancel(); | ||||
|     if (_settingsSubscription != null) _settingsSubscription.cancel(); | ||||
|     if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); | ||||
|     if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); | ||||
|     if (_refreshDataSubscription != null) _refreshDataSubscription.cancel(); | ||||
|     if (_showErrorSubscription != null) _showErrorSubscription.cancel(); | ||||
|     _homeAssistant.disconnect(); | ||||
|     _viewsTabController?.dispose(); | ||||
|     _stateSubscription?.cancel(); | ||||
|     _settingsSubscription?.cancel(); | ||||
|     _serviceCallSubscription?.cancel(); | ||||
|     _showEntityPageSubscription?.cancel(); | ||||
|     _showErrorSubscription?.cancel(); | ||||
|     _startAuthSubscription?.cancel(); | ||||
|     _reloadUISubscription?.cancel(); | ||||
|     //TODO disconnect | ||||
|     //widget.homeAssistant?.disconnect(); | ||||
|     super.dispose(); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										40
									
								
								lib/panel.page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,40 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class PanelPage extends StatefulWidget { | ||||
|   PanelPage({Key key, this.title, this.panel}) : super(key: key); | ||||
|  | ||||
|   final String title; | ||||
|   final Panel panel; | ||||
|  | ||||
|   @override | ||||
|   _PanelPageState createState() => new _PanelPageState(); | ||||
| } | ||||
|  | ||||
| class _PanelPageState extends State<PanelPage> { | ||||
|  | ||||
|   List<ConfigurationItem> _items; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|  | ||||
|     return new Scaffold( | ||||
|       appBar: new AppBar( | ||||
|         leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ | ||||
|           Navigator.pop(context); | ||||
|         }), | ||||
|         title: new Text(widget.title), | ||||
|       ), | ||||
|       body: widget.panel.getWidget(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     super.dispose(); | ||||
|   } | ||||
| } | ||||
| @@ -14,19 +14,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | ||||
|   String _newHassioDomain = ""; | ||||
|   String _hassioPort = ""; | ||||
|   String _newHassioPort = ""; | ||||
|   String _hassioPassword = ""; | ||||
|   String _newHassioPassword = ""; | ||||
|   String _socketProtocol = "wss"; | ||||
|   String _newSocketProtocol = "wss"; | ||||
|   String _authType = "access_token"; | ||||
|   String _newAuthType = "access_token"; | ||||
|   bool _useLovelace = false; | ||||
|   bool _newUseLovelace = false; | ||||
|   bool _useLovelace = true; | ||||
|   bool _newUseLovelace = true; | ||||
|  | ||||
|   String oauthUrl; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _loadSettings(); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   _loadSettings() async { | ||||
| @@ -35,23 +34,20 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | ||||
|     setState(() { | ||||
|       _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; | ||||
|       _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; | ||||
|       _hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? ""; | ||||
|       _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; | ||||
|       _authType = _newAuthType = prefs.getString("hassio-auth-type") ?? 'access_token'; | ||||
|       try { | ||||
|         _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? false; | ||||
|         _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true; | ||||
|       } catch (e) { | ||||
|         _useLovelace = _newUseLovelace = false; | ||||
|         _useLovelace = _newUseLovelace = true; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   bool _checkConfigChanged() { | ||||
|     return ((_newHassioPassword != _hassioPassword) || | ||||
|     return ( | ||||
|       (_newHassioPort != _hassioPort) || | ||||
|       (_newHassioDomain != _hassioDomain) || | ||||
|       (_newSocketProtocol != _socketProtocol) || | ||||
|       (_newAuthType != _authType) || | ||||
|       (_newUseLovelace != _useLovelace)); | ||||
|  | ||||
|   } | ||||
| @@ -63,10 +59,8 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     prefs.setString("hassio-domain", _newHassioDomain); | ||||
|     prefs.setString("hassio-port", _newHassioPort); | ||||
|     prefs.setString("hassio-password", _newHassioPassword); | ||||
|     prefs.setString("hassio-protocol", _newSocketProtocol); | ||||
|     prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); | ||||
|     prefs.setString("hassio-auth-type", _newAuthType); | ||||
|     prefs.setBool("use-lovelace", _newUseLovelace); | ||||
|   } | ||||
|  | ||||
| @@ -83,13 +77,13 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | ||||
|             icon: Icon(Icons.check), | ||||
|             onPressed: (){ | ||||
|               if (_checkConfigChanged()) { | ||||
|                 TheLogger.debug("Settings changed. Saving..."); | ||||
|                 Logger.d("Settings changed. Saving..."); | ||||
|                 _saveSettings().then((r) { | ||||
|                   Navigator.pop(context); | ||||
|                   eventBus.fire(SettingsChangedEvent(true)); | ||||
|                 }); | ||||
|               } else { | ||||
|                 TheLogger.debug("Settings was not changed"); | ||||
|                 Logger.d("Settings was not changed"); | ||||
|                 Navigator.pop(context); | ||||
|               } | ||||
|             } | ||||
| @@ -150,40 +144,9 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | ||||
|               _newHassioPort = value; | ||||
|             } | ||||
|           ), | ||||
|           new Row( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: [ | ||||
|               Flexible( | ||||
|                 child: Text( | ||||
|                   "Login with access token (HA >= 0.78.0)", | ||||
|                   softWrap: true, | ||||
|                   maxLines: 2, | ||||
|                 ), | ||||
|               ), | ||||
|               Switch( | ||||
|                 value: (_newAuthType == "access_token"), | ||||
|                 onChanged: (value) { | ||||
|                   setState(() { | ||||
|                     _newAuthType = value ? "access_token" : "api_password"; | ||||
|                   }); | ||||
|                 }, | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
|           new TextField( | ||||
|             decoration: InputDecoration( | ||||
|                 labelText: _newAuthType == "access_token" ? "Access token" : "API password" | ||||
|             ), | ||||
|             controller: new TextEditingController.fromValue( | ||||
|                 new TextEditingValue( | ||||
|                     text: _newHassioPassword, | ||||
|                     selection: | ||||
|                     new TextSelection.collapsed(offset: _newHassioPassword.length) | ||||
|                 ) | ||||
|             ), | ||||
|             onChanged: (value) { | ||||
|               _newHassioPassword = value; | ||||
|             } | ||||
|           new Text( | ||||
|             "Try ports 80 and 443 if default is not working and you don't know why.", | ||||
|             style: TextStyle(color: Colors.grey), | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.only(top: 20.0), | ||||
|   | ||||
| @@ -2,76 +2,49 @@ part of '../main.dart'; | ||||
|  | ||||
| class HACard { | ||||
|   List<EntityWrapper> entities = []; | ||||
|   EntityWrapper linkedEntity; | ||||
|   List<HACard> childCards = []; | ||||
|   EntityWrapper linkedEntityWrapper; | ||||
|   String name; | ||||
|   String id; | ||||
|   String type; | ||||
|   bool showName; | ||||
|   bool showState; | ||||
|   bool showEmpty; | ||||
|   int columnsCount; | ||||
|   List stateFilter; | ||||
|   List states; | ||||
|   String content; | ||||
|  | ||||
|   HACard({ | ||||
|     this.name, | ||||
|     this.id, | ||||
|     this.linkedEntity, | ||||
|     this.linkedEntityWrapper, | ||||
|     this.columnsCount: 4, | ||||
|     this.showName: true, | ||||
|     this.showState: true, | ||||
|     this.stateFilter: const [], | ||||
|     this.showEmpty: true, | ||||
|     this.content, | ||||
|     this.states, | ||||
|     @required this.type | ||||
|   }); | ||||
|  | ||||
|   List<EntityWrapper> getEntitiesToShow() { | ||||
|     return entities.where((entityWrapper) { | ||||
|       if (entityWrapper.entity.isHidden) { | ||||
|         return false; | ||||
|       } | ||||
|       if (stateFilter.isNotEmpty) { | ||||
|         return stateFilter.contains(entityWrapper.entity.state); | ||||
|       } | ||||
|       return true; | ||||
|     }).toList(); | ||||
|   } | ||||
|  | ||||
|   Widget build(BuildContext context) { | ||||
|       switch (type) { | ||||
|  | ||||
|         case CardType.entities: { | ||||
|           return EntitiesCardWidget( | ||||
|     return CardWidget( | ||||
|       card: this, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|         case CardType.glance: { | ||||
|           return GlanceCardWidget( | ||||
|             card: this, | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         case CardType.mediaControl: { | ||||
|           return MediaControlCardWidget( | ||||
|             card: this, | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         case CardType.weatherForecast: | ||||
|         case CardType.thermostat: | ||||
|         case CardType.sensor: | ||||
|         case CardType.plantStatus: | ||||
|         case CardType.pictureEntity: | ||||
|         case CardType.pictureElements: | ||||
|         case CardType.picture: | ||||
|         case CardType.map: | ||||
|         case CardType.iframe: | ||||
|         case CardType.gauge: | ||||
|         case CardType.entityButton: | ||||
|         case CardType.conditional: | ||||
|         case CardType.alarmPanel: { | ||||
|           return UnsupportedCardWidget( | ||||
|             card: this, | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         default: { | ||||
|           if ((linkedEntity == null) && (entities.isNotEmpty)) { | ||||
|             return EntitiesCardWidget( | ||||
|               card: this, | ||||
|             ); | ||||
|           } else { | ||||
|             return UnsupportedCardWidget( | ||||
|               card: this, | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|       } | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										58
									
								
								lib/ui_class/panel_class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class Panel { | ||||
|  | ||||
|   static const iconsByComponent = { | ||||
|     "config": "mdi:settings", | ||||
|     "history": "mdi:poll-box", | ||||
|     "map": "mdi:tooltip-account", | ||||
|     "logbook": "mdi:format-list-bulleted-type", | ||||
|     "custom": "mdi:home-assistant" | ||||
|   }; | ||||
|  | ||||
|   final String id; | ||||
|   final String type; | ||||
|   final String title; | ||||
|   final String urlPath; | ||||
|   final Map config; | ||||
|   String icon; | ||||
|   bool isHidden = true; | ||||
|  | ||||
|   Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) { | ||||
|     if (icon == null || !icon.startsWith("mdi:")) { | ||||
|       icon = Panel.iconsByComponent[type]; | ||||
|     } | ||||
|     isHidden = (type != "iframe" && type != "config"); | ||||
|   } | ||||
|  | ||||
|   void handleOpen(BuildContext context) { | ||||
|     if (type == "iframe") { | ||||
|       Logger.d("Launching custom tab with ${config["url"]}"); | ||||
|       HAUtils.launchURLInCustomTab(context, config["url"]); | ||||
|     } else if (type == "config") { | ||||
|       Navigator.of(context).push( | ||||
|           MaterialPageRoute( | ||||
|             builder: (context) => PanelPage(title: "$title", panel: this), | ||||
|           ) | ||||
|       ); | ||||
|     } else { | ||||
|       HomeAssistantModel haModel = HomeAssistantModel.of(context); | ||||
|       String url = "${haModel.homeAssistant.connection.httpWebHost}/$urlPath"; | ||||
|       Logger.d("Launching custom tab with $url"); | ||||
|       HAUtils.launchURLInCustomTab(context, url); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget getWidget() { | ||||
|     switch (type) { | ||||
|       case "config": { | ||||
|         return ConfigPanelWidget(); | ||||
|       } | ||||
|  | ||||
|       default: { | ||||
|         return Text("Unsupported panel component: $type"); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,11 +1,12 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class Sizes { | ||||
|   static const rightWidgetPadding = 14.0; | ||||
|   static const leftWidgetPadding = 8.0; | ||||
|   static const rightWidgetPadding = 16.0; | ||||
|   static const leftWidgetPadding = 16.0; | ||||
|   static const buttonPadding = 4.0; | ||||
|   static const extendedWidgetHeight = 50.0; | ||||
|   static const iconSize = 28.0; | ||||
|   static const largeIconSize = 34.0; | ||||
|   static const largeIconSize = 46.0; | ||||
|   static const stateFontSize = 15.0; | ||||
|   static const nameFontSize = 15.0; | ||||
|   static const smallFontSize = 14.0; | ||||
|   | ||||
| @@ -4,12 +4,15 @@ class HomeAssistantUI { | ||||
|   List<HAView> views; | ||||
|   String title; | ||||
|  | ||||
|   bool get isEmpty => views == null || views.isEmpty; | ||||
|  | ||||
|   HomeAssistantUI() { | ||||
|     views = []; | ||||
|   } | ||||
|  | ||||
|   Widget build(BuildContext context) { | ||||
|   Widget build(BuildContext context, TabController tabController) { | ||||
|     return TabBarView( | ||||
|       controller: tabController, | ||||
|       children: _buildViews(context) | ||||
|     ); | ||||
|   } | ||||
| @@ -24,4 +27,8 @@ class HomeAssistantUI { | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   void clear() { | ||||
|     views.clear(); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -28,8 +28,8 @@ class HAView { | ||||
|       HACard card = HACard( | ||||
|           name: e.displayName, | ||||
|           id: e.entityId, | ||||
|           linkedEntity: EntityWrapper(entity: e), | ||||
|           type: "media-control" | ||||
|           linkedEntityWrapper: EntityWrapper(entity: e), | ||||
|           type: CardType.mediaControl | ||||
|       ); | ||||
|       cards.add(card); | ||||
|     }); | ||||
| @@ -40,7 +40,7 @@ class HAView { | ||||
|           HACard card = HACard( | ||||
|               id: groupIdToAdd, | ||||
|               name: entity.domain, | ||||
|               type: "entities" | ||||
|               type: CardType.entities | ||||
|           ); | ||||
|           card.entities.add(EntityWrapper(entity: entity)); | ||||
|           autoGeneratedCards.add(card); | ||||
| @@ -51,16 +51,16 @@ class HAView { | ||||
|         HACard card = HACard( | ||||
|             name: entity.displayName, | ||||
|             id: entity.entityId, | ||||
|             linkedEntity: EntityWrapper(entity: entity), | ||||
|             type: "entities" | ||||
|             linkedEntityWrapper: EntityWrapper(entity: entity), | ||||
|             type: CardType.entities | ||||
|         ); | ||||
|         card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);})); | ||||
|         entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){ | ||||
|           HACard mediaCard = HACard( | ||||
|               name: entity.displayName, | ||||
|               id: entity.entityId, | ||||
|               linkedEntity: EntityWrapper(entity: entity), | ||||
|               type: "media-control" | ||||
|               linkedEntityWrapper: EntityWrapper(entity: entity), | ||||
|               type: CardType.mediaControl | ||||
|           ); | ||||
|           cards.add(mediaCard); | ||||
|         }); | ||||
| @@ -77,7 +77,7 @@ class HAView { | ||||
|           Tab( | ||||
|               icon: | ||||
|               Icon( | ||||
|                 MaterialDesignIcons.createIconDataFromIconName( | ||||
|                 MaterialDesignIcons.getIconDataFromIconName( | ||||
|                     iconName ?? "mdi:home-assistant"), | ||||
|                 size: 24.0, | ||||
|               ) | ||||
| @@ -92,7 +92,7 @@ class HAView { | ||||
|       if (linkedEntity.icon != null && linkedEntity.icon.length > 0) { | ||||
|         return Tab( | ||||
|           icon: Icon( | ||||
|               MaterialDesignIcons.createIconDataFromIconName( | ||||
|               MaterialDesignIcons.getIconDataFromIconName( | ||||
|                   linkedEntity.icon), | ||||
|               size: 24.0, | ||||
|             ) | ||||
|   | ||||
| @@ -3,14 +3,18 @@ part of '../main.dart'; | ||||
| class CardHeaderWidget extends StatelessWidget { | ||||
|  | ||||
|   final String name; | ||||
|   final Widget trailing; | ||||
|   final Widget subtitle; | ||||
|  | ||||
|   const CardHeaderWidget({Key key, this.name}) : super(key: key); | ||||
|   const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     var result; | ||||
|     if ((name != null) && (name.trim().length > 0)) { | ||||
|       result = new ListTile( | ||||
|         trailing: trailing, | ||||
|         subtitle: subtitle, | ||||
|         title: Text("$name", | ||||
|             textAlign: TextAlign.left, | ||||
|             overflow: TextOverflow.ellipsis, | ||||
|   | ||||
							
								
								
									
										289
									
								
								lib/ui_widgets/card_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,289 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class CardWidget extends StatelessWidget { | ||||
|  | ||||
|   final HACard card; | ||||
|  | ||||
|   const CardWidget({ | ||||
|     Key key, | ||||
|     this.card | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (card.linkedEntityWrapper!= null) { | ||||
|       if (card.linkedEntityWrapper.entity.isHidden) { | ||||
|         return Container(width: 0.0, height: 0.0,); | ||||
|       } | ||||
|       if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) { | ||||
|         return EntityModel( | ||||
|           entityWrapper: card.linkedEntityWrapper, | ||||
|           child: MissedEntityWidget(), | ||||
|           handleTap: false, | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     switch (card.type) { | ||||
|  | ||||
|       case CardType.entities: { | ||||
|         return _buildEntitiesCard(context); | ||||
|       } | ||||
|  | ||||
|       case CardType.glance: { | ||||
|         return _buildGlanceCard(context); | ||||
|       } | ||||
|  | ||||
|       case CardType.mediaControl: { | ||||
|         return _buildMediaControlsCard(context); | ||||
|       } | ||||
|  | ||||
|       case CardType.entityButton: { | ||||
|         return _buildEntityButtonCard(context); | ||||
|       } | ||||
|  | ||||
|       case CardType.markdown: { | ||||
|         return _buildMarkdownCard(context); | ||||
|       } | ||||
|  | ||||
|       case CardType.alarmPanel: { | ||||
|         return _buildAlarmPanelCard(context); | ||||
|       } | ||||
|  | ||||
|       case CardType.horizontalStack: { | ||||
|         if (card.childCards.isNotEmpty) { | ||||
|           List<Widget> children = []; | ||||
|           card.childCards.forEach((card) { | ||||
|             if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) { | ||||
|               children.add( | ||||
|                   Flexible( | ||||
|                     fit: FlexFit.tight, | ||||
|                     child: card.build(context), | ||||
|                   ) | ||||
|               ); | ||||
|             } | ||||
|           }); | ||||
|           return Row( | ||||
|             mainAxisSize: MainAxisSize.max, | ||||
|             mainAxisAlignment: MainAxisAlignment.center, | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: children, | ||||
|           ); | ||||
|         } | ||||
|         return Container(height: 0.0, width: 0.0,); | ||||
|       } | ||||
|  | ||||
|       case CardType.verticalStack: { | ||||
|         if (card.childCards.isNotEmpty) { | ||||
|           List<Widget> children = []; | ||||
|           card.childCards.forEach((card) { | ||||
|             children.add( | ||||
|                 card.build(context) | ||||
|             ); | ||||
|           }); | ||||
|           return Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             mainAxisAlignment: MainAxisAlignment.start, | ||||
|             children: children, | ||||
|           ); | ||||
|         } | ||||
|         return Container(height: 0.0, width: 0.0,); | ||||
|       } | ||||
|  | ||||
|       default: { | ||||
|         if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) { | ||||
|           return _buildEntitiesCard(context); | ||||
|         } else { | ||||
|           return _buildUnsupportedCard(context); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildEntitiesCard(BuildContext context) { | ||||
|     List<EntityWrapper> entitiesToShow = card.getEntitiesToShow(); | ||||
|     if (entitiesToShow.isEmpty && !card.showEmpty) { | ||||
|       return Container(height: 0.0, width: 0.0,); | ||||
|     } | ||||
|     List<Widget> body = []; | ||||
|     body.add(CardHeaderWidget(name: card.name)); | ||||
|     entitiesToShow.forEach((EntityWrapper entity) { | ||||
|       if (!entity.entity.isHidden) { | ||||
|         body.add( | ||||
|             Padding( | ||||
|               padding: EdgeInsets.fromLTRB(10.0, 4.0, 0.0, 4.0), | ||||
|               child: EntityModel( | ||||
|                   entityWrapper: entity, | ||||
|                   handleTap: true, | ||||
|                   child: entity.entity.buildDefaultWidget(context) | ||||
|               ), | ||||
|             )); | ||||
|       } | ||||
|     }); | ||||
|     return Card( | ||||
|         child: new Column(mainAxisSize: MainAxisSize.min, children: body) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildMarkdownCard(BuildContext context) { | ||||
|     if (card.content == null) { | ||||
|       return Container(height: 0.0, width: 0.0,); | ||||
|     } | ||||
|     List<Widget> body = []; | ||||
|     body.add(CardHeaderWidget(name: card.name)); | ||||
|     body.add(MarkdownBody(data: card.content)); | ||||
|     return Card( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||
|           child: new Column(mainAxisSize: MainAxisSize.min, children: body), | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildAlarmPanelCard(BuildContext context) { | ||||
|     List<Widget> body = []; | ||||
|     body.add(CardHeaderWidget( | ||||
|       name: card.name ?? "", | ||||
|       subtitle: Text("${card.linkedEntityWrapper.entity.displayState}", | ||||
|         style: TextStyle( | ||||
|             color: Colors.grey | ||||
|         ), | ||||
|       ), | ||||
|       trailing: Row( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           mainAxisAlignment: MainAxisAlignment.end, | ||||
|           children: [ | ||||
|             EntityIcon( | ||||
|               size: 50.0, | ||||
|             ), | ||||
|             Container( | ||||
|                 width: 26.0, | ||||
|                 child: IconButton( | ||||
|                     padding: EdgeInsets.all(0.0), | ||||
|                     alignment: Alignment.centerRight, | ||||
|                     icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||
|                         "mdi:dots-vertical")), | ||||
|                     onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity)) | ||||
|                 ) | ||||
|             ) | ||||
|           ] | ||||
|       ), | ||||
|     )); | ||||
|     body.add( | ||||
|         AlarmControlPanelControlsWidget( | ||||
|           extended: true, | ||||
|           states: card.states, | ||||
|         ) | ||||
|     ); | ||||
|     return Card( | ||||
|         child: EntityModel( | ||||
|             entityWrapper: card.linkedEntityWrapper, | ||||
|             handleTap: null, | ||||
|             child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 children: body | ||||
|             ) | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildGlanceCard(BuildContext context) { | ||||
|     List<EntityWrapper> entitiesToShow = card.getEntitiesToShow(); | ||||
|     if (entitiesToShow.isEmpty && !card.showEmpty) { | ||||
|       return Container(height: 0.0, width: 0.0,); | ||||
|     } | ||||
|     List<Widget> rows = []; | ||||
|     rows.add(CardHeaderWidget(name: card.name)); | ||||
|  | ||||
|     List<Widget> result = []; | ||||
|     int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length; | ||||
|  | ||||
|     entitiesToShow.forEach((EntityWrapper entity) { | ||||
|       result.add( | ||||
|           FractionallySizedBox( | ||||
|             widthFactor: 1/columnsCount, | ||||
|             child: EntityModel( | ||||
|                 entityWrapper: entity, | ||||
|                 child: GlanceEntityContainer( | ||||
|                   showName: card.showName, | ||||
|                   showState: card.showState, | ||||
|                 ), | ||||
|                 handleTap: true | ||||
|             ), | ||||
|           ) | ||||
|       ); | ||||
|     }); | ||||
|     rows.add( | ||||
|         Padding( | ||||
|           padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding), | ||||
|           child: Wrap( | ||||
|             //alignment: WrapAlignment.spaceAround, | ||||
|             runSpacing: Sizes.rowPadding*2, | ||||
|             children: result, | ||||
|           ), | ||||
|         ) | ||||
|     ); | ||||
|  | ||||
|     return Card( | ||||
|         child: new Column(mainAxisSize: MainAxisSize.min, children: rows) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildMediaControlsCard(BuildContext context) { | ||||
|     return Card( | ||||
|         child: EntityModel( | ||||
|             entityWrapper: card.linkedEntityWrapper, | ||||
|             handleTap: null, | ||||
|             child: MediaPlayerWidget() | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildEntityButtonCard(BuildContext context) { | ||||
|     card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ?? | ||||
|         card.linkedEntityWrapper.displayName.toUpperCase(); | ||||
|     return Card( | ||||
|         child: EntityModel( | ||||
|             entityWrapper: card.linkedEntityWrapper, | ||||
|             child: ButtonEntityContainer(), | ||||
|             handleTap: true | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildUnsupportedCard(BuildContext context) { | ||||
|     List<Widget> body = []; | ||||
|     body.add(CardHeaderWidget(name: card.name ?? "")); | ||||
|     List<Widget> result = []; | ||||
|     if (card.linkedEntityWrapper != null) { | ||||
|       result.addAll(<Widget>[ | ||||
|         Padding( | ||||
|           padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), | ||||
|           child: EntityModel( | ||||
|               entityWrapper: card.linkedEntityWrapper, | ||||
|               handleTap: true, | ||||
|               child: card.linkedEntityWrapper.entity.buildDefaultWidget(context) | ||||
|           ), | ||||
|         ) | ||||
|       ]); | ||||
|     } else { | ||||
|       result.addAll(<Widget>[ | ||||
|         Padding( | ||||
|           padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||
|           child: Text("'${card.type}' card is not supported yet"), | ||||
|         ), | ||||
|       ]); | ||||
|     } | ||||
|     body.addAll(result); | ||||
|     return Card( | ||||
|         child: new Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: body | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										95
									
								
								lib/ui_widgets/config_panel_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,95 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class ConfigPanelWidget extends StatefulWidget { | ||||
|   ConfigPanelWidget({Key key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState(); | ||||
| } | ||||
|  | ||||
| class ConfigurationItem { | ||||
|   ConfigurationItem({ this.isExpanded: false, this.header, this.body }); | ||||
|  | ||||
|   bool isExpanded; | ||||
|   final String header; | ||||
|   final Widget body; | ||||
| } | ||||
|  | ||||
| class _ConfigPanelWidgetState extends State<ConfigPanelWidget> { | ||||
|  | ||||
|   List<ConfigurationItem> _items; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _items = <ConfigurationItem>[ | ||||
|       ConfigurationItem( | ||||
|           header: 'General', | ||||
|           body: Padding( | ||||
|             padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               children: <Widget>[ | ||||
|                 Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)), | ||||
|                 Container(height: Sizes.rowPadding,), | ||||
|                 Text("Control your Home Assistant server from HA Client."), | ||||
|                 Divider(), | ||||
|                 Row( | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   children: <Widget>[ | ||||
|                     FlatServiceButton( | ||||
|                       text: "Restart", | ||||
|                       serviceName: "restart", | ||||
|                       serviceDomain: "homeassistant", | ||||
|                       entityId: null, | ||||
|                     ), | ||||
|                     FlatServiceButton( | ||||
|                       text: "Stop", | ||||
|                       serviceName: "stop", | ||||
|                       serviceDomain: "homeassistant", | ||||
|                       entityId: null, | ||||
|                     ), | ||||
|                   ], | ||||
|                 ) | ||||
|               ], | ||||
|             ), | ||||
|           ) | ||||
|       ) | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|  | ||||
|     return ListView( | ||||
|       children: [ | ||||
|         new ExpansionPanelList( | ||||
|           expansionCallback: (int index, bool isExpanded) { | ||||
|             setState(() { | ||||
|               _items[index].isExpanded = !_items[index].isExpanded; | ||||
|             }); | ||||
|           }, | ||||
|           children: _items.map((ConfigurationItem item) { | ||||
|             return new ExpansionPanel( | ||||
|               headerBuilder: (BuildContext context, bool isExpanded) { | ||||
|                 return CardHeaderWidget( | ||||
|                   name: item.header, | ||||
|                 ); | ||||
|               }, | ||||
|               isExpanded: item.isExpanded, | ||||
|               body: new Container( | ||||
|                 child: item.body, | ||||
|               ), | ||||
|             ); | ||||
|           }).toList(), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     super.dispose(); | ||||
|   } | ||||
| } | ||||
| @@ -1,43 +0,0 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntitiesCardWidget extends StatelessWidget { | ||||
|  | ||||
|   final HACard card; | ||||
|  | ||||
|   const EntitiesCardWidget({ | ||||
|     Key key, | ||||
|     this.card | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if ((card.linkedEntity!= null) && (card.linkedEntity.entity.isHidden)) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|     List<Widget> body = []; | ||||
|     body.add(CardHeaderWidget(name: card.name)); | ||||
|     body.addAll(_buildCardBody(context)); | ||||
|     return Card( | ||||
|         child: new Column(mainAxisSize: MainAxisSize.min, children: body) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   List<Widget> _buildCardBody(BuildContext context) { | ||||
|     List<Widget> result = []; | ||||
|     card.entities.forEach((EntityWrapper entity) { | ||||
|       if (!entity.entity.isHidden) { | ||||
|         result.add( | ||||
|             Padding( | ||||
|               padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), | ||||
|               child: EntityModel( | ||||
|                   entityWrapper: entity, | ||||
|                   handleTap: true, | ||||
|                   child: entity.entity.buildDefaultWidget(context) | ||||
|               ), | ||||
|             )); | ||||
|       } | ||||
|     }); | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class GlanceCardWidget extends StatelessWidget { | ||||
|  | ||||
|   final HACard card; | ||||
|  | ||||
|   const GlanceCardWidget({ | ||||
|     Key key, | ||||
|     this.card | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if ((card.linkedEntity!= null) && (card.linkedEntity.entity.isHidden)) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|     List<Widget> rows = []; | ||||
|     rows.add(CardHeaderWidget(name: card.name)); | ||||
|     rows.add(_buildRows(context)); | ||||
|     return Card( | ||||
|         child: new Column(mainAxisSize: MainAxisSize.min, children: rows) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildRows(BuildContext context) { | ||||
|     List<Widget> result = []; | ||||
|     List<EntityWrapper> toShow = card.entities.where((entity) {return !entity.entity.isHidden;}).toList(); | ||||
|     int columnsCount = toShow.length >= card.columnsCount ? card.columnsCount : toShow.length; | ||||
|  | ||||
|     toShow.forEach((EntityWrapper entity) { | ||||
|       result.add( | ||||
|         FractionallySizedBox( | ||||
|           widthFactor: 1/columnsCount, | ||||
|           child: EntityModel( | ||||
|             entityWrapper: entity, | ||||
|             child: entity.entity.buildGlanceWidget(context, card.showName, card.showState), | ||||
|             handleTap: true | ||||
|           ), | ||||
|         ) | ||||
|       ); | ||||
|     }); | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding), | ||||
|       child: Wrap( | ||||
|         //alignment: WrapAlignment.spaceAround, | ||||
|         runSpacing: Sizes.rowPadding*2, | ||||
|         children: result, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class MediaControlCardWidget extends StatelessWidget { | ||||
|  | ||||
|   final HACard card; | ||||
|  | ||||
|   const MediaControlCardWidget({ | ||||
|     Key key, | ||||
|     this.card | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if ((card.linkedEntity == null) || (card.linkedEntity.entity.isHidden)) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|  | ||||
|     return Card( | ||||
|         child: EntityModel( | ||||
|             entityWrapper: card.linkedEntity, | ||||
|             handleTap: null, | ||||
|             child: MediaPlayerWidget() | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -1,53 +0,0 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class UnsupportedCardWidget extends StatelessWidget { | ||||
|  | ||||
|   final HACard card; | ||||
|  | ||||
|   const UnsupportedCardWidget({ | ||||
|     Key key, | ||||
|     this.card | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if ((card.linkedEntity!= null) && (card.linkedEntity.entity.isHidden)) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|     List<Widget> body = []; | ||||
|     body.add(CardHeaderWidget(name: card.name ?? "")); | ||||
|     body.addAll(_buildCardBody(context)); | ||||
|     return Card( | ||||
|         child: new Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: body | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   List<Widget> _buildCardBody(BuildContext context) { | ||||
|     List<Widget> result = []; | ||||
|     if (card.linkedEntity != null) { | ||||
|       result.addAll(<Widget>[ | ||||
|           Padding( | ||||
|             padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), | ||||
|             child: EntityModel( | ||||
|                 entityWrapper: card.linkedEntity, | ||||
|                 handleTap: true, | ||||
|                 child: card.linkedEntity.entity.buildDefaultWidget(context) | ||||
|             ), | ||||
|           ) | ||||
|       ]); | ||||
|     } else { | ||||
|       result.addAll(<Widget>[ | ||||
|         Padding( | ||||
|           padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||
|           child: Text("'${card.type}' card is not supported yet"), | ||||
|         ), | ||||
|       ]); | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -17,29 +17,17 @@ class ViewWidget extends StatefulWidget { | ||||
|  | ||||
| class ViewWidgetState extends State<ViewWidget> { | ||||
|  | ||||
|   StreamSubscription _refreshDataSubscription; | ||||
|   Completer _refreshCompleter; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) { | ||||
|       if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) { | ||||
|         _refreshCompleter.complete(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return RefreshIndicator( | ||||
|       color: Colors.amber, | ||||
|       child: ListView( | ||||
|     return ListView( | ||||
|       padding: EdgeInsets.all(0.0), | ||||
|         physics: const AlwaysScrollableScrollPhysics(), | ||||
|       //physics: const AlwaysScrollableScrollPhysics(), | ||||
|       children: _buildChildren(context), | ||||
|       ), | ||||
|       onRefresh: () => _refreshData(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -57,12 +45,22 @@ class ViewWidgetState extends State<ViewWidget> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     List<Widget> cards = []; | ||||
|     widget.view.cards.forEach((HACard card){ | ||||
|       result.add( | ||||
|           card.build(context) | ||||
|       cards.add( | ||||
|           ConstrainedBox( | ||||
|             constraints: BoxConstraints(maxWidth: 500), | ||||
|             child: card.build(context), | ||||
|           ) | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     result.add( | ||||
|       Column ( | ||||
|         children: cards, | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
| @@ -76,19 +74,8 @@ class ViewWidgetState extends State<ViewWidget> { | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   Future _refreshData() { | ||||
|     if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) { | ||||
|       TheLogger.debug("Previous data refresh is still in progress"); | ||||
|     } else { | ||||
|       _refreshCompleter = Completer(); | ||||
|       eventBus.fire(RefreshDataEvent()); | ||||
|     } | ||||
|     return _refreshCompleter.future; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _refreshDataSubscription.cancel(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class TheLogger { | ||||
| class Logger { | ||||
|  | ||||
|   static List<String> _log = []; | ||||
|  | ||||
| @@ -20,15 +20,15 @@ class TheLogger { | ||||
|     return inDebugMode; | ||||
|   } | ||||
|  | ||||
|   static void error(String message) { | ||||
|   static void e(String message) { | ||||
|     _writeToLog("Error", message); | ||||
|   } | ||||
|  | ||||
|   static void warning(String message) { | ||||
|   static void w(String message) { | ||||
|     _writeToLog("Warning", message); | ||||
|   } | ||||
|  | ||||
|   static void debug(String message) { | ||||
|   static void d(String message) { | ||||
|     _writeToLog("Debug", message); | ||||
|   } | ||||
|  | ||||
| @@ -47,10 +47,42 @@ class TheLogger { | ||||
|  | ||||
| class HAUtils { | ||||
|   static void launchURL(String url) async { | ||||
|     if (await canLaunch(url)) { | ||||
|       await launch(url); | ||||
|     if (await urlLauncher.canLaunch(url)) { | ||||
|       await urlLauncher.launch(url); | ||||
|     } else { | ||||
|       TheLogger.error( "Could not launch $url"); | ||||
|       Logger.e( "Could not launch $url"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static void launchURLInCustomTab(BuildContext context, String url) async { | ||||
|     try { | ||||
|       await launch( | ||||
|         "$url", | ||||
|         option: new CustomTabsOption( | ||||
|           toolbarColor: Theme.of(context).primaryColor, | ||||
|           enableDefaultShare: true, | ||||
|           enableUrlBarHiding: true, | ||||
|           showPageTitle: true, | ||||
|           animation: new CustomTabsAnimation.slideIn() | ||||
|           // or user defined animation. | ||||
|           /*animation: new CustomTabsAnimation( | ||||
|           startEnter: 'slide_up', | ||||
|           startExit: 'android:anim/fade_out', | ||||
|           endEnter: 'android:anim/fade_in', | ||||
|           endExit: 'slide_down', | ||||
|         )*/, | ||||
|         extraCustomTabs: <String>[ | ||||
|           // ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox | ||||
|           'org.mozilla.firefox', | ||||
|           // ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx | ||||
|           'com.microsoft.emmx', | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     } catch (e) { | ||||
|       Logger.w("Can't open custom tab: ${e.toString()}"); | ||||
|       Logger.w("Launching in default browser"); | ||||
|       HAUtils.launchURL(url); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -58,8 +90,13 @@ class HAUtils { | ||||
| class StateChangedEvent { | ||||
|   String entityId; | ||||
|   String newState; | ||||
|   bool needToRebuildUI; | ||||
|  | ||||
|   StateChangedEvent(this.entityId, this.newState); | ||||
|   StateChangedEvent({ | ||||
|     this.entityId, | ||||
|     this.newState, | ||||
|     this.needToRebuildUI: false | ||||
|   }); | ||||
| } | ||||
|  | ||||
| class SettingsChangedEvent { | ||||
| @@ -68,14 +105,20 @@ class SettingsChangedEvent { | ||||
|   SettingsChangedEvent(this.reconnect); | ||||
| } | ||||
|  | ||||
| class RefreshDataEvent { | ||||
|   RefreshDataEvent(); | ||||
| } | ||||
|  | ||||
| class RefreshDataFinishedEvent { | ||||
|   RefreshDataFinishedEvent(); | ||||
| } | ||||
|  | ||||
| class ReloadUIEvent { | ||||
|   ReloadUIEvent(); | ||||
| } | ||||
|  | ||||
| class StartAuthEvent { | ||||
|   String oauthUrl; | ||||
|  | ||||
|   StartAuthEvent(this.oauthUrl); | ||||
| } | ||||
|  | ||||
| class ServiceCallEvent { | ||||
|   String domain; | ||||
|   String service; | ||||
|   | ||||
							
								
								
									
										131
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						| @@ -7,7 +7,7 @@ packages: | ||||
|       name: archive | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.4" | ||||
|     version: "2.0.8" | ||||
|   args: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -35,7 +35,7 @@ packages: | ||||
|       name: cached_network_image | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.5.0+1" | ||||
|     version: "0.7.0" | ||||
|   charcode: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -49,14 +49,14 @@ packages: | ||||
|       name: charts_common | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.5.0" | ||||
|     version: "0.6.0" | ||||
|   charts_flutter: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: charts_flutter | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.5.0" | ||||
|     version: "0.6.0" | ||||
|   collection: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -70,7 +70,7 @@ packages: | ||||
|       name: convert | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.2" | ||||
|     version: "2.1.1" | ||||
|   crypto: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -83,7 +83,7 @@ packages: | ||||
|     description: | ||||
|       path: "." | ||||
|       ref: HEAD | ||||
|       resolved-ref: c5727795659e886a7db8b39a14e2c8987280fe1f | ||||
|       resolved-ref: a7ed88a4793e094a4d5d5c2d88a89e55510accde | ||||
|       url: "https://github.com/MarkOSullivan94/dart_config.git" | ||||
|     source: git | ||||
|     version: "0.5.0" | ||||
| @@ -93,14 +93,21 @@ packages: | ||||
|       name: date_format | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.0.5" | ||||
|     version: "1.0.6" | ||||
|   event_bus: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: event_bus | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.0.1" | ||||
|     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: | ||||
|     dependency: "direct main" | ||||
|     description: flutter | ||||
| @@ -112,33 +119,61 @@ packages: | ||||
|       name: flutter_cache_manager | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.2.0+1" | ||||
|   flutter_colorpicker: | ||||
|     version: "0.3.2" | ||||
|   flutter_custom_tabs: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_colorpicker | ||||
|       name: flutter_custom_tabs | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.1.0" | ||||
|     version: "0.6.0" | ||||
|   flutter_launcher_icons: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: flutter_launcher_icons | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.6.1" | ||||
|     version: "0.7.0" | ||||
|   flutter_markdown: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_markdown | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     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: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_svg | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.10.4" | ||||
|   flutter_test: | ||||
|     dependency: "direct dev" | ||||
|     description: flutter | ||||
|     source: sdk | ||||
|     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: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: http | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.12.0" | ||||
|     version: "0.12.0+1" | ||||
|   http_parser: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -152,7 +187,7 @@ packages: | ||||
|       name: image | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.4" | ||||
|     version: "2.0.7" | ||||
|   intl: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -167,6 +202,13 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.11.3+2" | ||||
|   markdown: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: markdown | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.2" | ||||
|   matcher: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -188,20 +230,48 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.6.2" | ||||
|   path_drawing: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_drawing | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.4.0" | ||||
|   path_parsing: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_parsing | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.1.3" | ||||
|   path_provider: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_provider | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.4.1" | ||||
|     version: "0.5.0+1" | ||||
|   pedantic: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pedantic | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.4.0" | ||||
|   petitparser: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: petitparser | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.2" | ||||
|     version: "2.1.1" | ||||
|   platform: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: platform | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.2.0" | ||||
|   progress_indicators: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -222,7 +292,7 @@ packages: | ||||
|       name: shared_preferences | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.4.3" | ||||
|     version: "0.5.1+2" | ||||
|   sky_engine: | ||||
|     dependency: transitive | ||||
|     description: flutter | ||||
| @@ -234,7 +304,14 @@ packages: | ||||
|       name: source_span | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.4.1" | ||||
|     version: "1.5.4" | ||||
|   sqflite: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.1.3" | ||||
|   stack_trace: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -262,21 +339,21 @@ packages: | ||||
|       name: synchronized | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.5.3" | ||||
|     version: "2.1.0" | ||||
|   term_glyph: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: term_glyph | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.0.1" | ||||
|     version: "1.1.0" | ||||
|   test_api: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: test_api | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.2.1" | ||||
|     version: "0.2.2" | ||||
|   typed_data: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -290,14 +367,14 @@ packages: | ||||
|       name: url_launcher | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "4.0.1" | ||||
|     version: "5.0.2" | ||||
|   uuid: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: uuid | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.0.3" | ||||
|     version: "2.0.0" | ||||
|   vector_math: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -318,7 +395,7 @@ packages: | ||||
|       name: xml | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.2.3" | ||||
|     version: "3.2.5" | ||||
|   yaml: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -327,5 +404,5 @@ packages: | ||||
|     source: hosted | ||||
|     version: "2.1.15" | ||||
| sdks: | ||||
|   dart: ">=2.0.0 <=2.1.0-dev.9.3.flutter-9c07fb64c4" | ||||
|   flutter: ">=0.5.6 <2.0.0" | ||||
|   dart: ">=2.1.0 <3.0.0" | ||||
|   flutter: ">=1.2.1 <2.0.0" | ||||
|   | ||||
							
								
								
									
										15
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						| @@ -1,7 +1,7 @@ | ||||
| name: hass_client | ||||
| description: Home Assistant Android Client | ||||
|  | ||||
| version: 0.3.10+74 | ||||
| version: 0.6.0+101 | ||||
|  | ||||
| environment: | ||||
|   sdk: ">=2.0.0-dev.68.0 <3.0.0" | ||||
| @@ -16,12 +16,13 @@ dependencies: | ||||
|   cached_network_image: any | ||||
|   url_launcher: any | ||||
|   date_format: any | ||||
|   flutter_colorpicker: any | ||||
|   charts_flutter: any | ||||
|  | ||||
|   # The following adds the Cupertino Icons font to your application. | ||||
|   # Use with the CupertinoIcons class for iOS style icons. | ||||
|   #cupertino_icons: ^0.1.2 | ||||
|   flutter_markdown: any | ||||
|   flutter_svg: ^0.10.3 | ||||
|   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: | ||||
|   flutter_test: | ||||
| @@ -62,7 +63,7 @@ flutter: | ||||
|   fonts: | ||||
|     - family: "Material Design Icons" | ||||
|       fonts: | ||||
|         - asset: fonts/materialdesignicons-webfont.ttf | ||||
|         - asset: fonts/materialdesignicons-webfont-3-5-95.ttf | ||||
|   # fonts: | ||||
|   #   - family: Schyler | ||||
|   #     fonts: | ||||
|   | ||||