diff --git a/lib/entity_widgets/common/camera_stream_view.dart b/lib/entity_widgets/common/camera_stream_view.dart index 8b7f4e6..b4af066 100644 --- a/lib/entity_widgets/common/camera_stream_view.dart +++ b/lib/entity_widgets/common/camera_stream_view.dart @@ -131,7 +131,7 @@ class _CameraStreamViewState extends State { .of(context) .entityWrapper .entity; - _webHost = HomeAssistantModel.of(context).homeAssistant.httpAPIEndpoint; + _webHost = HomeAssistantModel.of(context).homeAssistant.httpWebHost; _connect(); } diff --git a/lib/home_assistant.class.dart b/lib/home_assistant.class.dart index cca8265..4c3cb32 100644 --- a/lib/home_assistant.class.dart +++ b/lib/home_assistant.class.dart @@ -2,8 +2,10 @@ part of 'main.dart'; class HomeAssistant { String _webSocketAPIEndpoint; - String httpAPIEndpoint; - String _password; + String httpWebHost; + //String _password; + String _token; + String _tempToken; bool _useLovelace = false; bool isSettingsLoaded = false; @@ -57,15 +59,16 @@ class HomeAssistant { String port = prefs.getString('hassio-port'); hostname = "$domain:$port"; _webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; - httpAPIEndpoint = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; - _password = prefs.getString('hassio-password'); + httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; + //_password = prefs.getString('hassio-password'); + _token = prefs.getString('hassio-token'); _useLovelace = prefs.getBool('use-lovelace') ?? true; - if ((domain == null) || (port == null) || (_password == null) || - (domain.length == 0) || (port.length == 0) || (_password.length == 0)) { + if ((domain == null) || (port == null) || + (domain.length == 0) || (port.length == 0)) { throw("Check connection settings"); } else { isSettingsLoaded = true; - entities = EntityCollection(httpAPIEndpoint); + entities = EntityCollection(httpWebHost); } } @@ -80,6 +83,7 @@ class HomeAssistant { if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) { Logger.w("Previous fetch is not complited"); } else { + Logger.d("Fetching..."); _fetchCompleter = new Completer(); _fetchTimer = Timer(fetchTimeout, () { Logger.e( "Data fetching timeout"); @@ -178,6 +182,9 @@ class HomeAssistant { if (_useLovelace) { futures.add(_getLovelace()); } + if (_token == null && _tempToken != null) { + futures.add(_getLongLivedToken()); + } futures.add(_getConfig()); futures.add(_getServices()); futures.add(_getUserInfo()); @@ -226,14 +233,18 @@ class HomeAssistant { _handleMessage(String message) { var data = json.decode(message); if (data["type"] == "auth_required") { - _sendAuthMessage('{"type": "auth","access_token": "$_password"}'); + Logger.d("[Received] <== ${data.toString()}"); + _sendAuth(); } else if (data["type"] == "auth_ok") { + Logger.d("[Received] <== ${data.toString()}"); _completeConnecting(null); _sendSubscribe(); } else if (data["type"] == "auth_invalid") { + Logger.d("[Received] <== ${data.toString()}"); + //TODO remove token and login again _completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"}); } else if (data["type"] == "result") { - Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}"); + Logger.d("[Received] <== ${data.toString()}"); _messageResolver[data["id"]]?.complete(data); _messageResolver.remove(data["id"]); } else if (data["type"] == "event") { @@ -246,40 +257,56 @@ class HomeAssistant { Logger.e("Event is null: $message"); } } else { - Logger.w("Unknown message type: $message"); + Logger.d("[Received] <== ${data.toString()}"); } } void _sendSubscribe() { _incrementMessageId(); _subscriptionMessageId = _currentMessageId; - _send('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false); + _rawSend('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false); } Future _getConfig() async { - await _sendInitialMessage("get_config").then((data) => _instanceConfig = Map.from(data["result"])); + await _sendSocketMessage(type: "get_config").then((data) => _instanceConfig = Map.from(data["result"])); } Future _getStates() async { - await _sendInitialMessage("get_states").then((data) => entities.parse(data["result"])); + await _sendSocketMessage(type: "get_states").then((data) => entities.parse(data["result"])); + } + + Future _getLongLivedToken() async { + await _sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client 3", "client_icon": null, "lifespan": 365}).then((data) { + if (data['success']) { + Logger.d("Got long-lived token: ${data['result']}"); + _token = data['result']; + //TODO save token + } else { + 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 + }); } Future _getLovelace() async { - await _sendInitialMessage("lovelace/config").then((data) => _rawLovelaceData = data["result"]); + await _sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data["result"]); } Future _getUserInfo() async { _userName = null; - await _sendInitialMessage("auth/current_user").then((data) => _userName = data["result"]["name"]); + await _sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["result"]["name"]); } Future _getServices() async { - await _sendInitialMessage("get_services").then((data) => Logger.d("We actually don`t need the list of servcies for now")); + await _sendSocketMessage(type: "get_services").then((data) => Logger.d("We actually don`t need the list of servcies for now")); } Future _getPanels() async { panels.clear(); - await _sendInitialMessage("get_panels").then((data) { + await _sendSocketMessage(type: "get_panels").then((data) { if (data["success"]) { data["result"].forEach((k,v) { String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}"; @@ -301,22 +328,73 @@ class HomeAssistant { _currentMessageId += 1; } - void _sendAuthMessage(String message) { - Logger.d( "[Sending] ==> auth request"); - _hassioChannel.sink.add(message); + void _sendAuth() { + if (_token != null) { + Logger.d( "Long leaved token exist"); + Logger.d( "[Sending] ==> auth request"); + _hassioChannel.sink.add('{"type": "auth","access_token": "$_token"}'); + } else if (_tempToken == null) { + 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..."); + sendHTTPPost( + 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"); + _tempToken = json.decode(response)['access_token']; + Logger.d("Closing webview..."); + flutterWebviewPlugin.close(); + Logger.d("Firing event to reload UI"); + eventBus.fire(ReloadUIEvent()); + }).catchError((e) { + //TODO DO DO something here + 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)); + } 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 + } } - Future _sendInitialMessage(String type) { + Future _sendSocketMessage({String type, Map additionalData, bool noId: false}) { Completer _completer = Completer(); - _incrementMessageId(); + Map dataObject = {"type": "$type"}; + if (!noId) { + _incrementMessageId(); + dataObject["id"] = _currentMessageId; + } + if (additionalData != null) { + dataObject.addAll(additionalData); + } _messageResolver[_currentMessageId] = _completer; - _send('{"id": $_currentMessageId, "type": "$type"}', false); + _rawSend(json.encode(dataObject), false); return _completer.future; } - _send(String message, bool queued) { + _rawSend(String message, bool queued) { var sendCompleter = Completer(); - if (queued) _messageQueue.add(message); + if (queued) { + _messageQueue.add(message); + } _connection().then((r) { _messageQueue.getActualMessages().forEach((message){ Logger.d( "[Sending queued] ==> $message"); @@ -369,7 +447,7 @@ class HomeAssistant { } message += '}'; } - return _send(message, true); + return _rawSend(message, true); } void _handleEntityStateChange(Map eventData) { @@ -583,11 +661,11 @@ class HomeAssistant { 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 = "$httpAPIEndpoint/api/history/period/$startTime?&filter_entity_id=$entityId"; + 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 $_password", + "authorization": "Bearer $_token", "Content-Type": "application/json" }); var history = json.decode(historyResponse.body); @@ -599,20 +677,33 @@ class HomeAssistant { } } - Future sendHTTPRequest(String data) async { - String url = "$httpAPIEndpoint/api/notify.fcm-android"; + Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async { + Completer completer = Completer(); + String url = "$httpWebHost$endPoint"; Logger.d("[Sending] ==> $url"); - http.Response response; - response = await http.post( + Map headers = {}; + if (contentType != null) { + headers["Content-Type"] = contentType; + } + if (includeAuthHeader) { + headers["authorization"] = "Bearer $_token"; + } + http.post( url, - headers: { - "authorization": "Bearer $_password", - "Content-Type": "application/json" - }, + headers: headers, body: data - ); - //var resData = json.decode(response.body); - Logger.d("[Received] <== ${response.statusCode}, ${response.body}"); + ).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; } } diff --git a/lib/main.dart b/lib/main.dart index 961070b..479cd66 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -169,6 +169,8 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker StreamSubscription _serviceCallSubscription; StreamSubscription _showEntityPageSubscription; StreamSubscription _showErrorSubscription; + StreamSubscription _startAuthSubscription; + StreamSubscription _reloadUISubscription; //bool _settingsLoaded = false; bool _accountMenuExpanded = false; //bool _useLovelaceUI; @@ -239,6 +241,11 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker } }); } + if (_reloadUISubscription == null) { + _reloadUISubscription = eventBus.on().listen((event){ + _refreshData(); + }); + } if (_serviceCallSubscription == null) { _serviceCallSubscription = eventBus.on().listen((event) { @@ -260,9 +267,28 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker }); } - _firebaseMessaging.getToken().then((String token) { + if (_startAuthSubscription == null) { + _startAuthSubscription = eventBus.on().listen((event){ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => WebviewScaffold( + url: "${event.oauthUrl}", + appBar: new AppBar( + title: new Text("Login"), + ), + ), + ) + ); + }); + } + + /*_firebaseMessaging.getToken().then((String token) { //Logger.d("FCM token: $token"); - widget.homeAssistant.sendHTTPRequest('{"token": "$token"}'); + widget.homeAssistant.sendHTTPPost( + endPoint: '/api/notify.fcm-android', + jsonData: '{"token": "$token"}' + ); }); _firebaseMessaging.configure( onLaunch: (data) { @@ -274,13 +300,14 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker onResume: (data) { Logger.d("Notification [onResume]: $data"); } - ); + );*/ } _refreshData() async { //widget.homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI); _hideBottomBar(); _showInfoBottomBar(progress: true,); + Logger.d("Calling fetch()"); await widget.homeAssistant.fetch().then((result) { _hideBottomBar(); int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0; @@ -390,7 +417,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker new ListTile( leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")), title: Text("Open Web UI"), - onTap: () => HAUtils.launchURL(widget.homeAssistant.httpAPIEndpoint), + onTap: () => HAUtils.launchURL(widget.homeAssistant.httpWebHost), ), Divider() ]); @@ -715,14 +742,18 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker @override void dispose() { + final flutterWebviewPlugin = new FlutterWebviewPlugin(); + flutterWebviewPlugin.dispose(); WidgetsBinding.instance.removeObserver(this); - _viewsTabController.dispose(); - if (_stateSubscription != null) _stateSubscription.cancel(); - if (_settingsSubscription != null) _settingsSubscription.cancel(); - if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); - if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); - if (_showErrorSubscription != null) _showErrorSubscription.cancel(); - widget.homeAssistant.disconnect(); + _viewsTabController?.dispose(); + _stateSubscription?.cancel(); + _settingsSubscription?.cancel(); + _serviceCallSubscription?.cancel(); + _showEntityPageSubscription?.cancel(); + _showErrorSubscription?.cancel(); + _startAuthSubscription?.cancel(); + _reloadUISubscription?.cancel(); + widget.homeAssistant?.disconnect(); super.dispose(); } } diff --git a/lib/settings.page.dart b/lib/settings.page.dart index 63a4839..a8179df 100644 --- a/lib/settings.page.dart +++ b/lib/settings.page.dart @@ -1,10 +1,9 @@ part of 'main.dart'; class ConnectionSettingsPage extends StatefulWidget { - ConnectionSettingsPage({Key key, this.title, this.homeAssistant}) : super(key: key); + ConnectionSettingsPage({Key key, this.title}) : super(key: key); final String title; - final HomeAssistant homeAssistant; @override _ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState(); @@ -23,20 +22,12 @@ class _ConnectionSettingsPageState extends State { bool _newUseLovelace = true; String oauthUrl; - final flutterWebviewPlugin = new FlutterWebviewPlugin(); @override void initState() { super.initState(); _loadSettings(); - 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("Auth code: $authCode"); - flutterWebviewPlugin.close(); - } - }); + } _loadSettings() async { @@ -79,22 +70,6 @@ class _ConnectionSettingsPageState extends State { @override Widget build(BuildContext context) { - Widget webViewButton = RaisedButton( - color: Colors.blue[200], - onPressed: () { - oauthUrl = "${ _newSocketProtocol == "wss" ? "https" : "http"}://$_newHassioDomain:${_newHassioPort ?? ''}/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"); - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => WebviewScaffold( - url: oauthUrl, - appBar: new AppBar( - title: new Text("Login"), - ) - ) - )); - }, - child: Text("Login with Home Assistant") - ); return new Scaffold( appBar: new AppBar( leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ @@ -177,7 +152,6 @@ class _ConnectionSettingsPageState extends State { "Try ports 80 and 443 if default is not working and you don't know why.", style: TextStyle(color: Colors.grey), ), - webViewButton, new TextField( decoration: InputDecoration( labelText: "Access token" diff --git a/lib/ui_class/panel_class.dart b/lib/ui_class/panel_class.dart index e39a080..e7d29fb 100644 --- a/lib/ui_class/panel_class.dart +++ b/lib/ui_class/panel_class.dart @@ -37,7 +37,7 @@ class Panel { ); } else { HomeAssistantModel haModel = HomeAssistantModel.of(context); - String url = "${haModel.homeAssistant.httpAPIEndpoint}/$urlPath"; + String url = "${haModel.homeAssistant.httpWebHost}/$urlPath"; Logger.d("Launching custom tab with $url"); HAUtils.launchURLInCustomTab(context, url); } diff --git a/lib/utils.class.dart b/lib/utils.class.dart index 8490610..affbc1b 100644 --- a/lib/utils.class.dart +++ b/lib/utils.class.dart @@ -109,6 +109,16 @@ class RefreshDataFinishedEvent { RefreshDataFinishedEvent(); } +class ReloadUIEvent { + ReloadUIEvent(); +} + +class StartAuthEvent { + String oauthUrl; + + StartAuthEvent(this.oauthUrl); +} + class ServiceCallEvent { String domain; String service;