Resolves #338 OAuth with Home Assistant

This commit is contained in:
estevez-dev 2019-03-20 23:05:25 +02:00
parent 6a03105d01
commit 67d7bb45f5
3 changed files with 84 additions and 91 deletions

View File

@ -3,6 +3,7 @@ part of 'main.dart';
class HomeAssistant { class HomeAssistant {
String _webSocketAPIEndpoint; String _webSocketAPIEndpoint;
String httpWebHost; String httpWebHost;
String oauthUrl;
//String _password; //String _password;
String _token; String _token;
String _tempToken; String _tempToken;
@ -68,6 +69,7 @@ class HomeAssistant {
throw("Check connection settings"); throw("Check connection settings");
} else { } else {
isSettingsLoaded = true; 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); entities = EntityCollection(httpWebHost);
} }
} }
@ -85,6 +87,7 @@ class HomeAssistant {
} else { } else {
Logger.d("Fetching..."); Logger.d("Fetching...");
_fetchCompleter = new Completer(); _fetchCompleter = new Completer();
_fetchTimer?.cancel();
_fetchTimer = Timer(fetchTimeout, () { _fetchTimer = Timer(fetchTimeout, () {
Logger.e( "Data fetching timeout"); Logger.e( "Data fetching timeout");
disconnect().then((_) { disconnect().then((_) {
@ -104,13 +107,12 @@ class HomeAssistant {
} }
disconnect() async { disconnect() async {
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) { Logger.d( "Socket disconnecting...");
await _hassioChannel.sink.close().timeout(Duration(seconds: 3), await _socketSubscription?.cancel();
await _hassioChannel?.sink?.close()?.timeout(Duration(seconds: 3),
onTimeout: () => Logger.d( "Socket sink closed") 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"); Logger.d("Previous connection is not complited");
} else { } else {
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionTimer?.cancel();
_connectionCompleter = new Completer(); _connectionCompleter = new Completer();
autoReconnect = false; autoReconnect = false;
disconnect().then((_){ disconnect().then((_){
@ -127,9 +130,7 @@ class HomeAssistant {
Logger.e( "Socket connection timeout"); Logger.e( "Socket connection timeout");
_handleSocketError(null); _handleSocketError(null);
}); });
if (_socketSubscription != null) { _socketSubscription?.cancel();
_socketSubscription.cancel();
}
_hassioChannel = IOWebSocketChannel.connect( _hassioChannel = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30)); _webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
_socketSubscription = _hassioChannel.stream.listen( _socketSubscription = _hassioChannel.stream.listen(
@ -199,7 +200,7 @@ class HomeAssistant {
} }
void _completeFetching(error) { void _completeFetching(error) {
_fetchTimer.cancel(); _fetchTimer?.cancel();
_completeConnecting(error); _completeConnecting(error);
if (!_fetchCompleter.isCompleted) { if (!_fetchCompleter.isCompleted) {
if (error != null) { if (error != null) {
@ -213,7 +214,7 @@ class HomeAssistant {
} }
void _completeConnecting(error) { void _completeConnecting(error) {
_connectionTimer.cancel(); _connectionTimer?.cancel();
if (!_connectionCompleter.isCompleted) { if (!_connectionCompleter.isCompleted) {
if (error != null) { if (error != null) {
_connectionCompleter.completeError(error); _connectionCompleter.completeError(error);
@ -241,10 +242,10 @@ class HomeAssistant {
_sendSubscribe(); _sendSubscribe();
} else if (data["type"] == "auth_invalid") { } else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}"); Logger.d("[Received] <== ${data.toString()}");
//TODO remove token and login again _logout();
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"}); _completeConnecting({"errorCode": 62, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") { } 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[data["id"]]?.complete(data);
_messageResolver.remove(data["id"]); _messageResolver.remove(data["id"]);
} else if (data["type"] == "event") { } 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() { void _sendSubscribe() {
_incrementMessageId(); _incrementMessageId();
_subscriptionMessageId = _currentMessageId; _subscriptionMessageId = _currentMessageId;
@ -276,18 +283,19 @@ class HomeAssistant {
} }
Future _getLongLivedToken() async { 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']) { if (data['success']) {
Logger.d("Got long-lived token: ${data['result']}"); Logger.d("Got long-lived token: ${data['result']}");
_token = data['result']; _token = data['result'];
//TODO save token _tempToken = null;
SharedPreferences.getInstance().then((prefs) => prefs.setString("hassio-token", _token));
} else { } else {
_logout();
Logger.e("Error getting long-lived token: ${data['error'].toString()}"); Logger.e("Error getting long-lived token: ${data['error'].toString()}");
//TODO DO DO something here
} }
}).catchError((e) { }).catchError((e) {
Logger.e("Error getting long-lived token: ${e.toString()}"); 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."); Logger.d( "No long leaved token. Need to authenticate.");
final flutterWebviewPlugin = new FlutterWebviewPlugin(); final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.onUrlChanged.listen((String url) { flutterWebviewPlugin.onUrlChanged.listen((String url) {
Logger.d("Launched url: $url");
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) { if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
String authCode = url.split("=")[1]; String authCode = url.split("=")[1];
Logger.d("We have auth code. Getting temporary access token..."); Logger.d("We have auth code. Getting temporary access token...");
@ -354,27 +361,33 @@ class HomeAssistant {
Logger.d("Firing event to reload UI"); Logger.d("Firing event to reload UI");
eventBus.fire(ReloadUIEvent()); eventBus.fire(ReloadUIEvent());
}).catchError((e) { }).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()}"); Logger.e("Error getting temp token: ${e.toString()}");
}); });
} }
}); });
disconnect().then((_){ disconnect();
//TODO create special error code to show "Login" in message _completeFetching({"errorCode": 60, "errorMessage": "Not authenticated"});
_completeConnecting({"errorCode": 6, "errorMessage": "Not authenticated"}); _requestOAuth();
});
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));
} else if (_tempToken != null) { } else if (_tempToken != null) {
Logger.d("We have temp token. Login..."); Logger.d("We have temp token. Login...");
_hassioChannel.sink.add('{"type": "auth","access_token": "$_tempToken"}'); _hassioChannel.sink.add('{"type": "auth","access_token": "$_tempToken"}');
} else { } else {
Logger.e("General login error"); 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}) { Future _sendSocketMessage({String type, Map additionalData, bool noId: false}) {
Completer _completer = Completer(); Completer _completer = Completer();
Map dataObject = {"type": "$type"}; Map dataObject = {"type": "$type"};

View File

@ -104,8 +104,6 @@ EventBus eventBus = new EventBus();
const String appName = "HA Client"; const String appName = "HA Client";
const appVersion = "0.5.2"; const appVersion = "0.5.2";
//String homeAssistantWebHost;
void main() { void main() {
FlutterError.onError = (errorDetails) { FlutterError.onError = (errorDetails) {
Logger.e( "${errorDetails.exception}"); Logger.e( "${errorDetails.exception}");
@ -158,12 +156,7 @@ class MainPage extends StatefulWidget {
} }
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin { class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
//HomeAssistant _homeAssistant;
//Map _instanceConfig;
//String _webSocketApiEndpoint;
//String _password;
//int _uiViewsCount = 0;
//String _instanceHost;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription; StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription; StreamSubscription _serviceCallSubscription;
@ -171,11 +164,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
StreamSubscription _showErrorSubscription; StreamSubscription _showErrorSubscription;
StreamSubscription _startAuthSubscription; StreamSubscription _startAuthSubscription;
StreamSubscription _reloadUISubscription; StreamSubscription _reloadUISubscription;
//bool _settingsLoaded = false;
bool _accountMenuExpanded = false; bool _accountMenuExpanded = false;
//bool _useLovelaceUI;
int _previousViewCount; int _previousViewCount;
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); //final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
@override @override
void initState() { 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() { _subscribe() {
if (_stateSubscription == null) { if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
@ -269,20 +243,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
if (_startAuthSubscription == null) { if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){ _startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
Navigator.push( _showOAuth();
context,
MaterialPageRoute(
builder: (context) => WebviewScaffold(
url: "${event.oauthUrl}",
appBar: new AppBar(
title: new Text("Login"),
),
),
)
);
}); });
} }
/*_firebaseMessaging.getToken().then((String token) { /*_firebaseMessaging.getToken().then((String token) {
//Logger.d("FCM token: $token"); //Logger.d("FCM token: $token");
widget.homeAssistant.sendHTTPPost( 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 { _refreshData() async {
//widget.homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI); //widget.homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
_hideBottomBar(); _hideBottomBar();
@ -539,12 +519,31 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
break; break;
} }
case 6: { case 60: {
_bottomBarAction = FlatButton( _bottomBarAction = FlatButton(
child: Text("Settings", style: textStyle), child: Text("Login", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); _refreshData();
Navigator.pushNamed(context, '/connection-settings'); },
);
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; break;

View File

@ -14,8 +14,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _newHassioDomain = ""; String _newHassioDomain = "";
String _hassioPort = ""; String _hassioPort = "";
String _newHassioPort = ""; String _newHassioPort = "";
String _hassioPassword = "";
String _newHassioPassword = "";
String _socketProtocol = "wss"; String _socketProtocol = "wss";
String _newSocketProtocol = "wss"; String _newSocketProtocol = "wss";
bool _useLovelace = true; bool _useLovelace = true;
@ -36,7 +34,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
setState(() { setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
try { try {
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true; _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
@ -47,7 +44,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
} }
bool _checkConfigChanged() { bool _checkConfigChanged() {
return ((_newHassioPassword != _hassioPassword) || return (
(_newHassioPort != _hassioPort) || (_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) || (_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) || (_newSocketProtocol != _socketProtocol) ||
@ -62,7 +59,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("hassio-domain", _newHassioDomain); prefs.setString("hassio-domain", _newHassioDomain);
prefs.setString("hassio-port", _newHassioPort); prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-password", _newHassioPassword);
prefs.setString("hassio-protocol", _newSocketProtocol); prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setBool("use-lovelace", _newUseLovelace); prefs.setBool("use-lovelace", _newUseLovelace);
@ -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.", "Try ports 80 and 443 if default is not working and you don't know why.",
style: TextStyle(color: Colors.grey), style: TextStyle(color: Colors.grey),
), ),
new TextField(
decoration: InputDecoration(
labelText: "Access token"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioPassword,
selection:
new TextSelection.collapsed(offset: _newHassioPassword.length)
)
),
onChanged: (value) {
_newHassioPassword = value;
}
),
Padding( Padding(
padding: EdgeInsets.only(top: 20.0), padding: EdgeInsets.only(top: 20.0),
child: Text( child: Text(