From 5b99ade088a6cad4d6ea5ef3f217b12a0164c91a Mon Sep 17 00:00:00 2001 From: estevez-dev Date: Sat, 15 Jun 2019 18:07:11 +0300 Subject: [PATCH] Resolves #318 add mobile_app integration --- lib/connection.class.dart | 14 ++-- lib/device.class.dart | 29 +++++++ lib/entity.page.dart | 5 +- .../controls/light_controls.dart | 7 +- lib/entity_widgets/model_widgets.dart | 20 ----- lib/home_assistant.class.dart | 75 ++++++++++++++++++- lib/main.dart | 26 ++++--- lib/ui_widgets/config_panel_widget.dart | 39 ++++++++++ 8 files changed, 168 insertions(+), 47 deletions(-) create mode 100644 lib/device.class.dart diff --git a/lib/connection.class.dart b/lib/connection.class.dart index 148858c..3deadb1 100644 --- a/lib/connection.class.dart +++ b/lib/connection.class.dart @@ -18,7 +18,8 @@ class Connection { String _token; String _tempToken; String oauthUrl; - String deviceName; + String webhookId; + String registeredAppVersion; bool useLovelace = true; bool settingsLoaded = false; bool get isAuthenticated => _token != null; @@ -43,11 +44,13 @@ class Connection { useLovelace = prefs.getBool('use-lovelace') ?? true; _domain = prefs.getString('hassio-domain'); _port = prefs.getString('hassio-port'); + webhookId = prefs.getString('app-webhook-id'); + registeredAppVersion = prefs.getString('registered-app-version'); displayHostname = "$_domain:$_port"; _webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket"; httpWebHost = - "${prefs.getString('hassio-res-protocol')}://$_domain${(_port == '433' || _port == '80') ? '' : ':'+_port}"; + "${prefs.getString('hassio-res-protocol')}://$_domain:$_port"; if ((_domain == null) || (_port == null) || (_domain.isEmpty) || (_port.isEmpty)) { completer.completeError(HAError.checkConnectionSettings()); @@ -57,15 +60,12 @@ class Connection { final storage = new FlutterSecureStorage(); try { _token = await storage.read(key: "hacl_llt"); - Logger.e("Long-lived token read successful: $_token"); + Logger.e("Long-lived token read successful"); } catch (e) { Logger.e("Cannt read secure storage. Need to relogin."); _token = null; await storage.delete(key: "hacl_llt"); } - DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - deviceName = androidInfo.model; oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent( 'http://ha-client.homemade.systems/')}&redirect_uri=${Uri .encodeComponent( @@ -393,7 +393,7 @@ class Connection { body: data ).then((response) { Logger.d("[Received] <== ${response.statusCode}, ${response.body}"); - if (response.statusCode == 200) { + if (response.statusCode >= 200 && response.statusCode < 300 ) { completer.complete(response.body); } else { completer.completeError({"code": response.statusCode, "message": "${response.body}"}); diff --git a/lib/device.class.dart b/lib/device.class.dart new file mode 100644 index 0000000..2cd0d05 --- /dev/null +++ b/lib/device.class.dart @@ -0,0 +1,29 @@ +part of 'main.dart'; + +class Device { + + static final Device _instance = Device._internal(); + + factory Device() { + return _instance; + } + + String unicDeviceId; + String manufacturer; + String model; + String osName; + String osVersion; + + Device._internal(); + + loadDeviceInfo() { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + deviceInfo.androidInfo.then((androidInfo) { + unicDeviceId = "${androidInfo.model.toLowerCase().replaceAll(' ', '_')}_${androidInfo.androidId}"; + manufacturer = "${androidInfo.manufacturer}"; + model = "${androidInfo.model}"; + osName = "Android"; + osVersion = "${androidInfo.version.release}"; + }); + } +} \ No newline at end of file diff --git a/lib/entity.page.dart b/lib/entity.page.dart index b537167..2b19124 100644 --- a/lib/entity.page.dart +++ b/lib/entity.page.dart @@ -46,10 +46,7 @@ class _EntityViewPageState extends State { // the App.build method, and use it to set our appbar title. title: new Text(_title), ), - body: HomeAssistantModel( - homeAssistant: widget.homeAssistant, - child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context) - ), + body: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context), ); } diff --git a/lib/entity_widgets/controls/light_controls.dart b/lib/entity_widgets/controls/light_controls.dart index 8fbadf6..1017251 100644 --- a/lib/entity_widgets/controls/light_controls.dart +++ b/lib/entity_widgets/controls/light_controls.dart @@ -171,7 +171,7 @@ class _LightControlsWidgetState extends State { Widget _buildColorControl(LightEntity entity) { if (entity.supportColor) { - HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor; + HSVColor savedColor = HomeAssistant().savedColor; return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -187,10 +187,7 @@ class _LightControlsWidgetState extends State { child: Text('Copy color'), onPressed: _tmpColor == null ? null : () { setState(() { - HomeAssistantModel - .of(context) - .homeAssistant - .savedColor = _tmpColor; + HomeAssistant().savedColor = _tmpColor; }); }, ), diff --git a/lib/entity_widgets/model_widgets.dart b/lib/entity_widgets/model_widgets.dart index aef383f..1b5477a 100644 --- a/lib/entity_widgets/model_widgets.dart +++ b/lib/entity_widgets/model_widgets.dart @@ -15,26 +15,6 @@ class EntityModel extends InheritedWidget { return context.inheritFromWidgetOfExactType(EntityModel); } - @override - bool updateShouldNotify(InheritedWidget oldWidget) { - return true; - } -} - -class HomeAssistantModel extends InheritedWidget { - - const HomeAssistantModel({ - Key key, - @required this.homeAssistant, - @required Widget child, - }) : super(key: key, child: child); - - final HomeAssistant homeAssistant; - - static HomeAssistantModel of(BuildContext context) { - return context.inheritFromWidgetOfExactType(HomeAssistantModel); - } - @override bool updateShouldNotify(InheritedWidget oldWidget) { return true; diff --git a/lib/home_assistant.class.dart b/lib/home_assistant.class.dart index e427245..d3cf7b0 100644 --- a/lib/home_assistant.class.dart +++ b/lib/home_assistant.class.dart @@ -2,6 +2,12 @@ part of 'main.dart'; class HomeAssistant { + static final HomeAssistant _instance = HomeAssistant._internal(); + + factory HomeAssistant() { + return _instance; + } + EntityCollection entities; HomeAssistantUI ui; Map _instanceConfig = {}; @@ -27,8 +33,9 @@ class HomeAssistant { bool get isNoViews => ui == null || ui.isEmpty; bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app"); - HomeAssistant() { + HomeAssistant._internal() { Connection().onStateChangeCallback = _handleEntityStateChange; + Device().loadDeviceInfo(); } Completer _fetchCompleter; @@ -49,6 +56,7 @@ class HomeAssistant { futures.add(_getServices()); futures.add(_getUserInfo()); futures.add(_getPanels()); + futures.add(checkAppRegistration()); futures.add(Connection().sendSocketMessage( type: "subscribe_events", additionalData: {"event_type": "state_changed"}, @@ -75,6 +83,71 @@ class HomeAssistant { }); } + Future checkAppRegistration({bool forceRegister: false, bool forceUpdate: false}) { + Completer completer = Completer(); + var registrationData = { + "app_version": "$appVersion", + "device_name": "$userName's ${Device().model}", + "manufacturer": Device().manufacturer, + "model": Device().model, + "os_name": Device().osName, + "os_version": Device().osVersion, + "app_data": { + "push_notification_key": "d" + } + }; + if (Connection().webhookId == null || forceRegister) { + Logger.d("Mobile app was not registered yet or need to be reseted. Registering..."); + registrationData.addAll({ + "app_id": "ha_client", + "app_name": "$appName", + "supports_encryption": false, + }); + Connection().sendHTTPPost( + endPoint: "/api/mobile_app/registrations", + includeAuthHeader: true, + data: json.encode(registrationData) + ).then((response) { + Logger.d("Processing registration responce..."); + var responseObject = json.decode(response); + Logger.d(responseObject.toString()); + SharedPreferences.getInstance().then((prefs) { + prefs.setString("app-webhook-id", responseObject["webhook_id"]); + prefs.setString("registered-app-version", "$appVersion"); + completer.complete(); + }); + }).catchError((e) { + completer.complete(); + Logger.e("Error registering the app: ${e.toString()}"); + }); + return completer.future; + } else if (Connection().registeredAppVersion != appVersion || forceUpdate) { + Logger.d("Registered app version is old. Registration need to be updated"); + var updateData = { + "type": "update_registration", + "data": registrationData + }; + Connection().sendHTTPPost( + endPoint: "/api/webhook/${Connection().webhookId}", + includeAuthHeader: false, + data: json.encode(updateData) + ).then((response) { + Logger.d("App registration updated"); + SharedPreferences.getInstance().then((prefs) { + prefs.setString("registered-app-version", "$appVersion"); + completer.complete(); + }); + }).catchError((e) { + completer.complete(); + Logger.e("Error updating app registering: ${e.toString()}"); + }); + return completer.future; + } else { + Logger.d("App is registered"); + return Future.value(); + } + } + Future _getConfig() async { await Connection().sendSocketMessage(type: "get_config").then((data) { _instanceConfig = Map.from(data); diff --git a/lib/main.dart b/lib/main.dart index fb93f36..8329675 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -93,6 +93,7 @@ part 'mdi.class.dart'; part 'entity_collection.class.dart'; part 'auth_manager.class.dart'; part 'connection.class.dart'; +part 'device.class.dart'; part 'ui_class/ui.dart'; part 'ui_class/view.class.dart'; part 'ui_class/card.class.dart'; @@ -230,7 +231,11 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker _previousViewCount = currentViewCount; } }).catchError((e) { - _setErrorState(e); + if (e is HAError) { + _setErrorState(e); + } else { + _setErrorState(HAError(e.toString())); + } }); eventBus.fire(RefreshDataFinishedEvent()); } @@ -289,11 +294,11 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker }); } - _firebaseMessaging.getToken().then((String token) { - Logger.d("Device name: ${json.encode(Connection().deviceName)}"); + /*_firebaseMessaging.getToken().then((String token) { + Logger.d("Device name: ${json.encode(Connection().unicDeviceName)}"); Connection().sendHTTPPost( endPoint: '/api/notify.ha-client', - data: '{"token": "$token", "device": ${json.encode(Connection().deviceName)}}' + data: '{"token": "$token", "device": ${json.encode(Connection().unicDeviceName)}}' ).then((_) { Logger.d("Notificatin listener registered."); completer.complete(); @@ -304,7 +309,8 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker }).catchError((e) { Logger.e("Error registering notification listener: ${e.toString()}"); completer.complete(); - }); + });*/ + completer.complete(); return completer.future; } @@ -394,7 +400,10 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker new ListTile( leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)), title: Text("${panel.title}"), - onTap: () => panel.handleOpen(context) + onTap: () { + Navigator.of(context).pop(); + panel.handleOpen(context); + } ) ); } @@ -752,10 +761,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker drawer: _buildAppDrawer(), primary: false, bottomNavigationBar: bottomBar, - body: HomeAssistantModel( - child: _buildScaffoldBody(false), - homeAssistant: widget.homeAssistant - ), + body: _buildScaffoldBody(false), ); } } diff --git a/lib/ui_widgets/config_panel_widget.dart b/lib/ui_widgets/config_panel_widget.dart index 407d60c..004f9b1 100644 --- a/lib/ui_widgets/config_panel_widget.dart +++ b/lib/ui_widgets/config_panel_widget.dart @@ -55,10 +55,49 @@ class _ConfigPanelWidgetState extends State { ], ), ) + ), + ConfigurationItem( + header: 'Mobile app', + body: Padding( + padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text("Registration", style: TextStyle(fontSize: Sizes.largeFontSize)), + Container(height: Sizes.rowPadding,), + Text("${HomeAssistant().userName}'s ${Device().model}, ${Device().osName} ${Device().osVersion}"), + Container(height: 6.0,), + Text("Reseting mobile app registration will not remove integration from Home Assistant but creates a new one with different device. If you want to reset mobile app registration completally you need to remove MobileApp from Configuretion -> Integrations of your Home Assistant."), + Divider(), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlatButton( + onPressed: () => resetRegistration(), + child: Text("Reset registration") + ), + FlatButton( + onPressed: () => updateRegistration(), + child: Text("Update registration") + ) + ], + ) + ], + ), + ) ) ]; } + resetRegistration() { + HomeAssistant().checkAppRegistration(forceRegister: true).then((_) => Navigator.of(context).pop()); + } + + updateRegistration() { + HomeAssistant().checkAppRegistration(forceUpdate: true).then((_) => Navigator.of(context).pop()); + } + @override Widget build(BuildContext context) {