diff --git a/android/app/src/main/java/com/keyboardcrumbs/hassclient/Application.java b/android/app/src/main/java/com/keyboardcrumbs/hassclient/Application.java index 22358b3..857bda2 100644 --- a/android/app/src/main/java/com/keyboardcrumbs/hassclient/Application.java +++ b/android/app/src/main/java/com/keyboardcrumbs/hassclient/Application.java @@ -4,13 +4,11 @@ import io.flutter.app.FlutterApplication; import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback; import io.flutter.plugins.GeneratedPluginRegistrant; -import io.flutter.plugins.androidalarmmanager.AlarmService; public class Application extends FlutterApplication implements PluginRegistrantCallback { @Override public void onCreate() { super.onCreate(); - AlarmService.setPluginRegistrant(this); } @Override diff --git a/lib/const.dart b/lib/const.dart index ccd8710..1daf8c4 100644 --- a/lib/const.dart +++ b/lib/const.dart @@ -96,4 +96,28 @@ class CardType { static const conditional = "conditional"; static const alarmPanel = "alarm-panel"; static const markdown = "markdown"; +} + +class UserError { + final int code; + final String message; + + UserError({@required this.code, this.message: ""}); +} + +class ErrorCode { + static const UNKNOWN = 0; + static const NOT_CONFIGURED = 1; + static const AUTH_INVALID = 2; + static const NO_MOBILE_APP_COMPONENT = 3; + static const ERROR_GETTING_CONFIG = 4; + static const ERROR_GETTING_STATES = 5; + static const ERROR_GETTING_LOVELACE_CONFIG = 6; + static const ERROR_GETTING_PANELS = 7; + static const CONNECTION_TIMEOUT = 8; + static const DISCONNECTED = 9; + static const UNABLE_TO_CONNECT = 10; + static const GENERAL_AUTH_ERROR = 11; + static const AUTH_ERROR = 12; + static const NOT_LOGGED_IN = 13; } \ No newline at end of file diff --git a/lib/home_assistant.class.dart b/lib/home_assistant.class.dart index 930c4f9..9b0caf0 100644 --- a/lib/home_assistant.class.dart +++ b/lib/home_assistant.class.dart @@ -68,7 +68,7 @@ class HomeAssistant { _fetchCompleter.complete(); MobileAppIntegrationManager.checkAppRegistration(); } else { - _fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app")])); + _fetchCompleter.completeError(UserError(code: ErrorCode.NO_MOBILE_APP_COMPONENT)); } }).catchError((e) { _fetchCompleter.completeError(e); @@ -89,7 +89,7 @@ class HomeAssistant { await ConnectionManager().sendSocketMessage(type: "get_config").then((data) { _instanceConfig = Map.from(data); }).catchError((e) { - throw HAError("Error getting config: ${e}"); + throw UserError(code: ErrorCode.ERROR_GETTING_CONFIG, message: "$e"); }); } @@ -97,26 +97,26 @@ class HomeAssistant { await ConnectionManager().sendSocketMessage(type: "get_states").then( (data) => entities.parse(data) ).catchError((e) { - throw HAError("Error getting states: $e"); + throw UserError(code: ErrorCode.ERROR_GETTING_STATES, message: "$e"); }); } Future _getLovelace() async { await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) { - throw HAError("Error getting lovelace config: $e"); + throw UserError(code: ErrorCode.ERROR_GETTING_LOVELACE_CONFIG, message: "$e"); }); } Future _getUserInfo() async { _userName = null; await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) { - Logger.w("Can't get user info: ${e}"); + Logger.w("Can't get user info: $e"); }); } Future _getServices() async { await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) { - Logger.w("Can't get services: ${e}"); + Logger.w("Can't get services: $e"); }); } @@ -136,7 +136,7 @@ class HomeAssistant { ); }); }).catchError((e) { - throw HAError("Error getting panels list: $e"); + throw UserError(code: ErrorCode.ERROR_GETTING_PANELS, message: "$e"); }); } diff --git a/lib/main.dart b/lib/main.dart index 125d9f8..f185419 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -114,6 +114,7 @@ part 'ui_widgets/card_widget.dart'; part 'ui_widgets/card_header_widget.dart'; part 'panels/config_panel_widget.dart'; part 'panels/widgets/link_to_web_config.dart'; +part 'user_error_screen.widget.dart'; EventBus eventBus = new EventBus(); @@ -217,8 +218,9 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker StreamSubscription _reloadUISubscription; StreamSubscription _showPageSubscription; int _previousViewCount; - bool _showLoginButton = false; + //bool _showLoginButton = false; bool _preventAppRefresh = false; + UserError _userError; @override void initState() { @@ -292,25 +294,29 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker } void _fullLoad() async { + setState(() { + _userError = null; + }); _showInfoBottomBar(progress: true,); _subscribe().then((_) { ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){ _fetchData(); - StartupUserMessagesManager().checkMessagesToShow(); - }, onError: (e) { - _setErrorState(e); + }, onError: (code) { + _setErrorState(code); }); }); } void _quickLoad() { + setState(() { + _userError = null; + }); _hideBottomBar(); _showInfoBottomBar(progress: true,); ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){ _fetchData(); - StartupUserMessagesManager().checkMessagesToShow(); - }, onError: (e) { - _setErrorState(e); + }, onError: (code) { + _setErrorState(code); }); } @@ -323,12 +329,8 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker _viewsTabController = TabController(vsync: this, length: currentViewCount); _previousViewCount = currentViewCount; } - }).catchError((e) { - if (e is HAError) { - _setErrorState(e); - } else { - _setErrorState(HAError(e.toString())); - } + }).catchError((code) { + _setErrorState(code); }); eventBus.fire(RefreshDataFinishedEvent()); } @@ -371,7 +373,10 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker } if (_reloadUISubscription == null) { _reloadUISubscription = eventBus.on().listen((event){ - _quickLoad(); + if (event.full) + _fullLoad(); + else + _quickLoad(); }); } if (_showPopupDialogSubscription == null) { @@ -421,20 +426,20 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker if (_showErrorSubscription == null) { _showErrorSubscription = eventBus.on().listen((event){ - _showErrorBottomBar(event.error); + _setErrorState(event.error); }); } if (_startAuthSubscription == null) { _startAuthSubscription = eventBus.on().listen((event){ - setState(() { - _showLoginButton = event.showButton; - }); - if (event.showButton) { + if (event.starting) { _showOAuth(); } else { _preventAppRefresh = false; Navigator.of(context).pop(); + setState(() { + _userError = null; + }); } }); } @@ -448,17 +453,27 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker void _showOAuth() { _preventAppRefresh = true; + _setErrorState(UserError(code: ErrorCode.NOT_LOGGED_IN)); Navigator.of(context).pushNamed('/login'); } - _setErrorState(HAError e) { - if (e == null) { + _setErrorState(error) { + if (error is UserError) { + setState(() { + _userError = error; + }); + } else { + setState(() { + _userError = UserError(code: ErrorCode.UNKNOWN); + }); + } + /*if (e == null) { _showErrorBottomBar( HAError("Unknown error") ); } else { _showErrorBottomBar(e); - } + }*/ } void _showPopupDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) { @@ -739,6 +754,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker } } + /* void _showErrorBottomBar(HAError error) { TextStyle textStyle = TextStyle( color: Colors.blue, @@ -803,7 +819,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker _bottomBarText = "${error.message}"; _showBottomBar = true; }); - } + }*/ final GlobalKey _scaffoldKey = new GlobalKey(); @@ -814,18 +830,33 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker child: new Text("Reload"), value: "reload", )); - List emptyBody = [ + /*List emptyBody = [ Text("."), - ]; + ];*/ if (ConnectionManager().isAuthenticated) { - _showLoginButton = false; + //_showLoginButton = false; popupMenuItems.add( PopupMenuItem( child: new Text("Logout"), value: "logout", )); } - if (_showLoginButton) { + Widget bodyWidget; + if (_userError != null) { + bodyWidget = UserErrorScreen(error: _userError); + } else if (empty) { + bodyWidget = Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator() + ], + ); + } else { + bodyWidget = HomeAssistant().buildViews(context, _viewsTabController); + } + /*if (_showLoginButton) { emptyBody = [ FlatButton( child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)), @@ -833,7 +864,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker onPressed: () => _fullLoad(), ) ]; - } + }*/ return NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ @@ -869,7 +900,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker _scaffoldKey.currentState.openDrawer(); }, ), - bottom: empty ? null : TabBar( + bottom: (empty || _userError != null) ? null : TabBar( controller: _viewsTabController, tabs: buildUIViewTabs(), isScrollable: true, @@ -878,15 +909,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker ]; }, - body: empty ? - Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: emptyBody - ), - ) - : - HomeAssistant().buildViews(context, _viewsTabController), + body: bodyWidget, ); } diff --git a/lib/managers/auth_manager.class.dart b/lib/managers/auth_manager.class.dart index bb361fb..bd3172c 100644 --- a/lib/managers/auth_manager.class.dart +++ b/lib/managers/auth_manager.class.dart @@ -33,7 +33,7 @@ class AuthManager { //flutterWebviewPlugin.close(); Logger.e("Error getting temp token: ${e.toString()}"); eventBus.fire(StartAuthEvent(oauthUrl, false)); - completer.completeError(HAError("Error getting temp token")); + completer.completeError(UserError(code: ErrorCode.AUTH_ERROR)); }); } }); diff --git a/lib/managers/connection_manager.class.dart b/lib/managers/connection_manager.class.dart index 3e2698a..0b12ae8 100644 --- a/lib/managers/connection_manager.class.dart +++ b/lib/managers/connection_manager.class.dart @@ -40,6 +40,7 @@ class ConnectionManager { if (loadSettings) { Logger.e("Loading settings..."); SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.reload(); useLovelace = prefs.getBool('use-lovelace') ?? true; _domain = prefs.getString('hassio-domain'); _port = prefs.getString('hassio-port'); @@ -51,14 +52,12 @@ class ConnectionManager { "${prefs.getString('hassio-res-protocol')}://$_domain:$_port"; if ((_domain == null) || (_port == null) || (_domain.isEmpty) || (_port.isEmpty)) { - completer.completeError(HAError.checkConnectionSettings()); + completer.completeError(UserError(code: ErrorCode.NOT_CONFIGURED)); stopInit = true; } else { - //_token = prefs.getString('hassio-token'); final storage = new FlutterSecureStorage(); try { _token = await storage.read(key: "hacl_llt"); - Logger.e("Long-lived token read successful"); } catch (e) { Logger.e("Cannt read secure storage. Need to relogin."); _token = null; @@ -73,7 +72,7 @@ class ConnectionManager { } else { if ((_domain == null) || (_port == null) || (_domain.isEmpty) || (_port.isEmpty)) { - completer.completeError(HAError.checkConnectionSettings()); + completer.completeError(UserError(code: ErrorCode.NOT_CONFIGURED)); stopInit = true; } } @@ -101,7 +100,7 @@ class ConnectionManager { if (forceReconnect || !isConnected) { _connect().timeout(connectTimeout, onTimeout: () { _disconnect().then((_) { - completer?.completeError(HAError("Connection timeout")); + completer?.completeError(UserError(code: ErrorCode.CONNECTION_TIMEOUT)); }); }).then((_) { completer?.complete(); @@ -146,10 +145,11 @@ class ConnectionManager { } } else if (data["type"] == "auth_invalid") { Logger.d("[Received] <== ${data.toString()}"); - _messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()])); + _messageResolver["auth"]?.completeError(UserError(code: ErrorCode.AUTH_INVALID, message: "${data["message"]}")); _messageResolver.remove("auth"); + //TODO dont logout, show variants to User logout().then((_) { - if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()])); + if (!connecting.isCompleted) connecting.completeError(UserError(code: ErrorCode.AUTH_INVALID, message: "${data["message"]}")); }); } else { _handleMessage(data); @@ -214,14 +214,14 @@ class ConnectionManager { Logger.d("Socket disconnected."); if (!connectionCompleter.isCompleted) { isConnected = false; - connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()])); + connectionCompleter.completeError(UserError(code: ErrorCode.DISCONNECTED)); } else { _disconnect().then((_) { Timer(Duration(seconds: 5), () { Logger.d("Trying to reconnect..."); _connect().catchError((e) { isConnected = false; - eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant"))); + eventBus.fire(UserError(code: ErrorCode.UNABLE_TO_CONNECT)); }); }); }); @@ -232,14 +232,14 @@ class ConnectionManager { Logger.e("Socket stream Error: $e"); if (!connectionCompleter.isCompleted) { isConnected = false; - connectionCompleter.completeError(HAError("Unable to connect to Home Assistant")); + connectionCompleter.completeError(UserError(code: ErrorCode.UNABLE_TO_CONNECT)); } else { _disconnect().then((_) { Timer(Duration(seconds: 5), () { Logger.d("Trying to reconnect..."); _connect().catchError((e) { isConnected = false; - eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant"))); + eventBus.fire(ShowErrorEvent(UserError(code: ErrorCode.UNABLE_TO_CONNECT))); }); }); }); @@ -275,7 +275,7 @@ class ConnectionManager { }); }).catchError((e) => completer.completeError(e)); } else { - completer.completeError(HAError("General login error")); + completer.completeError(UserError(code: ErrorCode.GENERAL_AUTH_ERROR)); } return completer.future; } @@ -309,8 +309,9 @@ class ConnectionManager { throw e; }); }).catchError((e) { + //TODO dont logout, show variants logout(); - completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()])); + completer.completeError(UserError(code: ErrorCode.AUTH_ERROR, message: "$e")); }); return completer.future; } @@ -333,7 +334,7 @@ class ConnectionManager { String rawMessage = json.encode(dataObject); if (!isConnected) { _connect().timeout(connectTimeout, onTimeout: (){ - _completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()])); + _completer.completeError(UserError(code: ErrorCode.UNABLE_TO_CONNECT)); }).then((_) { Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}"); _socket.sink.add(rawMessage); diff --git a/lib/user_error_screen.widget.dart b/lib/user_error_screen.widget.dart new file mode 100644 index 0000000..247cb07 --- /dev/null +++ b/lib/user_error_screen.widget.dart @@ -0,0 +1,214 @@ +part of 'main.dart'; + +class UserErrorScreen extends StatelessWidget { + + final UserError error; + + const UserErrorScreen({Key key, this.error}) : super(key: key); + + void _goToAppSettings(BuildContext context) { + Navigator.pushNamed(context, '/connection-settings'); + } + + void _reload() { + eventBus.fire(ReloadUIEvent(true)); + } + + void _disableLovelace() { + SharedPreferences.getInstance().then((prefs){ + prefs.setBool("use-lovelace", false); + eventBus.fire(ReloadUIEvent(true)); + }); + } + + void _reLogin() { + ConnectionManager().logout().then((_) => eventBus.fire(ReloadUIEvent(true))); + } + + @override + Widget build(BuildContext context) { + String errorText; + List buttons = []; + switch (this.error.code) { + case ErrorCode.AUTH_ERROR: { + errorText = "There was an error logging in to Home Assistant"; + buttons.add(RaisedButton( + onPressed: () => _reload(), + child: Text("Retry"), + )); + buttons.add(RaisedButton( + onPressed: () => _reLogin(), + child: Text("Login again"), + )); + break; + } + case ErrorCode.UNABLE_TO_CONNECT: { + errorText = "Unable to connect to Home Assistant"; + buttons.addAll([ + RaisedButton( + onPressed: () => _reload(), + child: Text("Retry") + ), + Container(width: 15.0,), + RaisedButton( + onPressed: () => _goToAppSettings(context), + child: Text("Check application settings"), + ) + ] + ); + break; + } + case ErrorCode.AUTH_INVALID: { + errorText = "${error.message ?? "Can't login to Home Assistant"}"; + buttons.addAll([ + RaisedButton( + onPressed: () => _reload(), + child: Text("Retry") + ), + Container(width: 15.0,), + RaisedButton( + onPressed: () => _reLogin(), + child: Text("Login again"), + ) + ] + ); + break; + } + case ErrorCode.GENERAL_AUTH_ERROR: { + buttons.addAll([ + RaisedButton( + onPressed: () => _reload(), + child: Text("Retry") + ), + Container(width: 15.0,), + RaisedButton( + onPressed: () => _reLogin(), + child: Text("Login again"), + ) + ] + ); + break; + } + case ErrorCode.DISCONNECTED: { + errorText = "Disconnected"; + buttons.addAll([ + RaisedButton( + onPressed: () => _reload(), + child: Text("Reconnect") + ), + Container(width: 15.0,), + RaisedButton( + onPressed: () => _goToAppSettings(context), + child: Text("Check application settings"), + ) + ] + ); + break; + } + case ErrorCode.CONNECTION_TIMEOUT: { + errorText = "Connection timeout"; + buttons.addAll([ + RaisedButton( + onPressed: () => _reload(), + child: Text("Reconnect") + ), + Container(width: 15.0,), + RaisedButton( + onPressed: () => _goToAppSettings(context), + child: Text("Check application settings"), + ) + ] + ); + break; + } + case ErrorCode.NOT_CONFIGURED: { + errorText = "Looks like HA Client is not configured yet."; + buttons.add(RaisedButton( + onPressed: () => _goToAppSettings(context), + child: Text("Open application settings"), + )); + break; + } + case ErrorCode.ERROR_GETTING_PANELS: + case ErrorCode.ERROR_GETTING_CONFIG: + case ErrorCode.ERROR_GETTING_STATES: { + errorText = "Couldn't get data from Home Assistant. ${error.message ?? ""}"; + buttons.add(RaisedButton( + onPressed: () => _reload(), + child: Text("Try again"), + )); + break; + } + case ErrorCode.ERROR_GETTING_LOVELACE_CONFIG: { + errorText = "Couldn't get Lovelace UI config. You can try to disable it and use group-based UI istead."; + buttons.addAll([ + RaisedButton( + onPressed: () => _reload(), + child: Text("Retry"), + ), + Container(width: 15.0,), + RaisedButton( + onPressed: () => _disableLovelace(), + child: Text("Disable Lovelace UI"), + ) + ]); + break; + } + case ErrorCode.NOT_LOGGED_IN: { + errorText = "You are not logged in yet. Please login."; + buttons.add(RaisedButton( + onPressed: () => _reload(), + child: Text("Login"), + )); + break; + } + default: { + errorText = "???"; + buttons.add(RaisedButton( + onPressed: () => _reload(), + child: Text("Reload"), + )); + } + } + + return Padding( + padding: EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 100.0, bottom: 20.0), + child: Icon( + Icons.error, + color: Colors.redAccent, + size: 48.0 + ) + ), + Text( + errorText, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.black45, fontSize: Sizes.largeFontSize), + softWrap: true, + maxLines: 5, + ), + Container(height: Sizes.rowPadding,), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: buttons.isNotEmpty ? buttons : Container(height: 0.0, width: 0.0,), + ) + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/utils.class.dart b/lib/utils.class.dart index cd19de2..7b4d747 100644 --- a/lib/utils.class.dart +++ b/lib/utils.class.dart @@ -45,7 +45,7 @@ class Logger { } -class HAError { +/*class HAError { String message; final List actions; @@ -87,7 +87,7 @@ class HAErrorActionType { static const LOGOUT = 2; static const URL = 3; static const OPEN_CONNECTION_SETTINGS = 4; -} +}*/ class StateChangedEvent { String entityId; @@ -112,14 +112,16 @@ class RefreshDataFinishedEvent { } class ReloadUIEvent { - ReloadUIEvent(); + final bool full; + + ReloadUIEvent(this.full); } class StartAuthEvent { String oauthUrl; - bool showButton; + bool starting; - StartAuthEvent(this.oauthUrl, this.showButton); + StartAuthEvent(this.oauthUrl, this.starting); } class ServiceCallEvent { @@ -165,7 +167,7 @@ class ShowPageEvent { } class ShowErrorEvent { - final HAError error; + final UserError error; ShowErrorEvent(this.error); } \ No newline at end of file