Resolves #318 add mobile_app integration

This commit is contained in:
estevez-dev 2019-06-15 18:07:11 +03:00
parent e1d9d9f304
commit 5b99ade088
8 changed files with 168 additions and 47 deletions

View File

@ -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}"});

29
lib/device.class.dart Normal file
View File

@ -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}";
});
}
}

View File

@ -46,10 +46,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
// 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),
);
}

View File

@ -171,7 +171,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildColorControl(LightEntity entity) {
if (entity.supportColor) {
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
HSVColor savedColor = HomeAssistant().savedColor;
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -187,10 +187,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
child: Text('Copy color'),
onPressed: _tmpColor == null ? null : () {
setState(() {
HomeAssistantModel
.of(context)
.homeAssistant
.savedColor = _tmpColor;
HomeAssistant().savedColor = _tmpColor;
});
},
),

View File

@ -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;

View File

@ -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);

View File

@ -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<MainPage> 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<MainPage> 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<MainPage> 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<MainPage> 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<MainPage> with WidgetsBindingObserver, Ticker
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
body: HomeAssistantModel(
child: _buildScaffoldBody(false),
homeAssistant: widget.homeAssistant
),
body: _buildScaffoldBody(false),
);
}
}

View File

@ -55,10 +55,49 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
],
),
)
),
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: <Widget>[
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: <Widget>[
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) {