Resolves #338 OAuth with Home Assistant
This commit is contained in:
		| @@ -3,6 +3,7 @@ part of 'main.dart'; | ||||
| class HomeAssistant { | ||||
|   String _webSocketAPIEndpoint; | ||||
|   String httpWebHost; | ||||
|   String oauthUrl; | ||||
|   //String _password; | ||||
|   String _token; | ||||
|   String _tempToken; | ||||
| @@ -68,6 +69,7 @@ class HomeAssistant { | ||||
|       throw("Check connection settings"); | ||||
|     } else { | ||||
|       isSettingsLoaded = true; | ||||
|       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')}"; | ||||
|       entities = EntityCollection(httpWebHost); | ||||
|     } | ||||
|   } | ||||
| @@ -85,6 +87,7 @@ class HomeAssistant { | ||||
|     } else { | ||||
|       Logger.d("Fetching..."); | ||||
|       _fetchCompleter = new Completer(); | ||||
|       _fetchTimer?.cancel(); | ||||
|       _fetchTimer = Timer(fetchTimeout, () { | ||||
|         Logger.e( "Data fetching timeout"); | ||||
|         disconnect().then((_) { | ||||
| @@ -104,13 +107,12 @@ class HomeAssistant { | ||||
|   } | ||||
|  | ||||
|   disconnect() async { | ||||
|     if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) { | ||||
|       await _hassioChannel.sink.close().timeout(Duration(seconds: 3), | ||||
|     Logger.d( "Socket disconnecting..."); | ||||
|     await _socketSubscription?.cancel(); | ||||
|     await _hassioChannel?.sink?.close()?.timeout(Duration(seconds: 3), | ||||
|         onTimeout: () => Logger.d( "Socket sink closed") | ||||
|       ); | ||||
|       await _socketSubscription.cancel(); | ||||
|       _hassioChannel = null; | ||||
|     } | ||||
|     ); | ||||
|     _hassioChannel = null; | ||||
|  | ||||
|   } | ||||
|  | ||||
| @@ -119,6 +121,7 @@ class HomeAssistant { | ||||
|       Logger.d("Previous connection is not complited"); | ||||
|     } else { | ||||
|       if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { | ||||
|         _connectionTimer?.cancel(); | ||||
|         _connectionCompleter = new Completer(); | ||||
|         autoReconnect = false; | ||||
|         disconnect().then((_){ | ||||
| @@ -127,9 +130,7 @@ class HomeAssistant { | ||||
|             Logger.e( "Socket connection timeout"); | ||||
|             _handleSocketError(null); | ||||
|           }); | ||||
|           if (_socketSubscription != null) { | ||||
|             _socketSubscription.cancel(); | ||||
|           } | ||||
|           _socketSubscription?.cancel(); | ||||
|           _hassioChannel = IOWebSocketChannel.connect( | ||||
|               _webSocketAPIEndpoint, pingInterval: Duration(seconds: 30)); | ||||
|           _socketSubscription = _hassioChannel.stream.listen( | ||||
| @@ -199,7 +200,7 @@ class HomeAssistant { | ||||
|   } | ||||
|  | ||||
|   void _completeFetching(error) { | ||||
|     _fetchTimer.cancel(); | ||||
|     _fetchTimer?.cancel(); | ||||
|     _completeConnecting(error); | ||||
|     if (!_fetchCompleter.isCompleted) { | ||||
|       if (error != null) { | ||||
| @@ -213,7 +214,7 @@ class HomeAssistant { | ||||
|   } | ||||
|  | ||||
|   void _completeConnecting(error) { | ||||
|     _connectionTimer.cancel(); | ||||
|     _connectionTimer?.cancel(); | ||||
|     if (!_connectionCompleter.isCompleted) { | ||||
|       if (error != null) { | ||||
|         _connectionCompleter.completeError(error); | ||||
| @@ -241,10 +242,10 @@ class HomeAssistant { | ||||
|       _sendSubscribe(); | ||||
|     } else if (data["type"] == "auth_invalid") { | ||||
|       Logger.d("[Received] <== ${data.toString()}"); | ||||
|       //TODO remove token and login again | ||||
|       _completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"}); | ||||
|       _logout(); | ||||
|       _completeConnecting({"errorCode": 62, "errorMessage": "${data["message"]}"}); | ||||
|     } else if (data["type"] == "result") { | ||||
|       Logger.d("[Received] <== ${data.toString()}"); | ||||
|       Logger.d("[Received] <== id: ${data['id']}, success: ${data['success']}"); | ||||
|       _messageResolver[data["id"]]?.complete(data); | ||||
|       _messageResolver.remove(data["id"]); | ||||
|     } else if (data["type"] == "event") { | ||||
| @@ -261,6 +262,12 @@ class HomeAssistant { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _logout() { | ||||
|     _token = null; | ||||
|     _tempToken = null; | ||||
|     SharedPreferences.getInstance().then((prefs) => prefs.remove("hassio-token")); | ||||
|   } | ||||
|  | ||||
|   void _sendSubscribe() { | ||||
|     _incrementMessageId(); | ||||
|     _subscriptionMessageId = _currentMessageId; | ||||
| @@ -276,18 +283,19 @@ class HomeAssistant { | ||||
|   } | ||||
|  | ||||
|   Future _getLongLivedToken() async { | ||||
|     await _sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client 3", "client_icon": null, "lifespan": 365}).then((data) { | ||||
|     await _sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app", "lifespan": 365}).then((data) { | ||||
|       if (data['success']) { | ||||
|         Logger.d("Got long-lived token: ${data['result']}"); | ||||
|         _token = data['result']; | ||||
|         //TODO save token | ||||
|         _tempToken = null; | ||||
|         SharedPreferences.getInstance().then((prefs) => prefs.setString("hassio-token", _token)); | ||||
|       } else { | ||||
|         _logout(); | ||||
|         Logger.e("Error getting long-lived token: ${data['error'].toString()}"); | ||||
|         //TODO DO DO something here | ||||
|       } | ||||
|     }).catchError((e) { | ||||
|       Logger.e("Error getting long-lived token: ${e.toString()}"); | ||||
|       //TODO DO DO something here | ||||
|       _logout(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -337,7 +345,6 @@ class HomeAssistant { | ||||
|       Logger.d( "No long leaved token. Need to authenticate."); | ||||
|       final flutterWebviewPlugin = new FlutterWebviewPlugin(); | ||||
|       flutterWebviewPlugin.onUrlChanged.listen((String url) { | ||||
|         Logger.d("Launched url: $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..."); | ||||
| @@ -354,27 +361,33 @@ class HomeAssistant { | ||||
|             Logger.d("Firing event to reload UI"); | ||||
|             eventBus.fire(ReloadUIEvent()); | ||||
|           }).catchError((e) { | ||||
|             //TODO DO DO something here | ||||
|             _logout(); | ||||
|             disconnect(); | ||||
|             flutterWebviewPlugin.close(); | ||||
|             _completeFetching({"errorCode": 61, "errorMessage": "Error getting temp token"}); | ||||
|             Logger.e("Error getting temp token: ${e.toString()}"); | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|       disconnect().then((_){ | ||||
|         //TODO create special error code to show "Login" in message | ||||
|         _completeConnecting({"errorCode": 6, "errorMessage": "Not authenticated"}); | ||||
|       }); | ||||
|       String 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')}"; | ||||
|       Logger.d("OAuth url: $oauthUrl"); | ||||
|       eventBus.fire(StartAuthEvent(oauthUrl)); | ||||
|       disconnect(); | ||||
|       _completeFetching({"errorCode": 60, "errorMessage": "Not authenticated"}); | ||||
|       _requestOAuth(); | ||||
|     } else if (_tempToken != null) { | ||||
|       Logger.d("We have temp token. Login..."); | ||||
|       _hassioChannel.sink.add('{"type": "auth","access_token": "$_tempToken"}'); | ||||
|     } else { | ||||
|       Logger.e("General login error"); | ||||
|       //TODO DO DO something here | ||||
|       _logout(); | ||||
|       disconnect(); | ||||
|       _completeFetching({"errorCode": 61, "errorMessage": "General login error"}); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _requestOAuth() { | ||||
|     Logger.d("OAuth url: $oauthUrl"); | ||||
|     eventBus.fire(StartAuthEvent(oauthUrl)); | ||||
|   } | ||||
|  | ||||
|   Future _sendSocketMessage({String type, Map additionalData, bool noId: false}) { | ||||
|     Completer _completer = Completer(); | ||||
|     Map dataObject = {"type": "$type"}; | ||||
|   | ||||
| @@ -104,8 +104,6 @@ EventBus eventBus = new EventBus(); | ||||
| const String appName = "HA Client"; | ||||
| const appVersion = "0.5.2"; | ||||
|  | ||||
| //String homeAssistantWebHost; | ||||
|  | ||||
| void main() { | ||||
|   FlutterError.onError = (errorDetails) { | ||||
|     Logger.e( "${errorDetails.exception}"); | ||||
| @@ -158,12 +156,7 @@ class MainPage extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin { | ||||
|   //HomeAssistant _homeAssistant; | ||||
|   //Map _instanceConfig; | ||||
|   //String _webSocketApiEndpoint; | ||||
|   //String _password; | ||||
|   //int _uiViewsCount = 0; | ||||
|   //String _instanceHost; | ||||
|  | ||||
|   StreamSubscription _stateSubscription; | ||||
|   StreamSubscription _settingsSubscription; | ||||
|   StreamSubscription _serviceCallSubscription; | ||||
| @@ -171,11 +164,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | ||||
|   StreamSubscription _showErrorSubscription; | ||||
|   StreamSubscription _startAuthSubscription; | ||||
|   StreamSubscription _reloadUISubscription; | ||||
|   //bool _settingsLoaded = false; | ||||
|   bool _accountMenuExpanded = false; | ||||
|   //bool _useLovelaceUI; | ||||
|   int _previousViewCount; | ||||
|   final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); | ||||
|   //final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
| @@ -213,23 +204,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /*_loadConnectionSettings() async { | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     String domain = prefs.getString('hassio-domain'); | ||||
|     String port = prefs.getString('hassio-port'); | ||||
|     _instanceHost = "$domain:$port"; | ||||
|     _webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; | ||||
|     homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; | ||||
|     _password = prefs.getString('hassio-password'); | ||||
|     _useLovelaceUI = prefs.getBool('use-lovelace') ?? true; | ||||
|     if ((domain == null) || (port == null) || (_password == null) || | ||||
|         (domain.length == 0) || (port.length == 0) || (_password.length == 0)) { | ||||
|       throw("Check connection settings"); | ||||
|     } else { | ||||
|       _settingsLoaded = true; | ||||
|     } | ||||
|   }*/ | ||||
|  | ||||
|   _subscribe() { | ||||
|     if (_stateSubscription == null) { | ||||
|       _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { | ||||
| @@ -269,20 +243,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | ||||
|  | ||||
|     if (_startAuthSubscription == null) { | ||||
|       _startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){ | ||||
|         Navigator.push( | ||||
|             context, | ||||
|             MaterialPageRoute( | ||||
|               builder: (context) => WebviewScaffold( | ||||
|                 url: "${event.oauthUrl}", | ||||
|                 appBar: new AppBar( | ||||
|                   title: new Text("Login"), | ||||
|                 ), | ||||
|               ), | ||||
|             ) | ||||
|         ); | ||||
|         _showOAuth(); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     /*_firebaseMessaging.getToken().then((String token) { | ||||
|       //Logger.d("FCM token: $token"); | ||||
|       widget.homeAssistant.sendHTTPPost( | ||||
| @@ -303,6 +269,20 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | ||||
|     );*/ | ||||
|   } | ||||
|  | ||||
|   void _showOAuth() { | ||||
|     Navigator.push( | ||||
|         context, | ||||
|         MaterialPageRoute( | ||||
|           builder: (context) => WebviewScaffold( | ||||
|             url: "${widget.homeAssistant.oauthUrl}", | ||||
|             appBar: new AppBar( | ||||
|               title: new Text("Login"), | ||||
|             ), | ||||
|           ), | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   _refreshData() async { | ||||
|     //widget.homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI); | ||||
|     _hideBottomBar(); | ||||
| @@ -539,12 +519,31 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker | ||||
|           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'); | ||||
|               _refreshData(); | ||||
|             }, | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case 61: { | ||||
|           _bottomBarAction = FlatButton( | ||||
|             child: Text("Try again", style: textStyle), | ||||
|             onPressed: () { | ||||
|               _refreshData(); | ||||
|             }, | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case 62: { | ||||
|           _bottomBarAction = FlatButton( | ||||
|             child: Text("Login again", style: textStyle), | ||||
|             onPressed: () { | ||||
|               _refreshData(); | ||||
|             }, | ||||
|           ); | ||||
|           break; | ||||
|   | ||||
| @@ -14,8 +14,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | ||||
|   String _newHassioDomain = ""; | ||||
|   String _hassioPort = ""; | ||||
|   String _newHassioPort = ""; | ||||
|   String _hassioPassword = ""; | ||||
|   String _newHassioPassword = ""; | ||||
|   String _socketProtocol = "wss"; | ||||
|   String _newSocketProtocol = "wss"; | ||||
|   bool _useLovelace = true; | ||||
| @@ -36,7 +34,6 @@ 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'; | ||||
|       try { | ||||
|         _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true; | ||||
| @@ -47,7 +44,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | ||||
|   } | ||||
|  | ||||
|   bool _checkConfigChanged() { | ||||
|     return ((_newHassioPassword != _hassioPassword) || | ||||
|     return ( | ||||
|       (_newHassioPort != _hassioPort) || | ||||
|       (_newHassioDomain != _hassioDomain) || | ||||
|       (_newSocketProtocol != _socketProtocol) || | ||||
| @@ -62,7 +59,6 @@ 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.setBool("use-lovelace", _newUseLovelace); | ||||
| @@ -152,21 +148,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | ||||
|             "Try ports 80 and 443 if default is not working and you don't know why.", | ||||
|             style: TextStyle(color: Colors.grey), | ||||
|           ), | ||||
|           new TextField( | ||||
|             decoration: InputDecoration( | ||||
|                 labelText: "Access token" | ||||
|             ), | ||||
|             controller: new TextEditingController.fromValue( | ||||
|                 new TextEditingValue( | ||||
|                     text: _newHassioPassword, | ||||
|                     selection: | ||||
|                     new TextSelection.collapsed(offset: _newHassioPassword.length) | ||||
|                 ) | ||||
|             ), | ||||
|             onChanged: (value) { | ||||
|               _newHassioPassword = value; | ||||
|             } | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.only(top: 20.0), | ||||
|             child: Text( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user