Resolves #318 add mobile_app integration
This commit is contained in:
parent
e1d9d9f304
commit
5b99ade088
@ -18,7 +18,8 @@ class Connection {
|
|||||||
String _token;
|
String _token;
|
||||||
String _tempToken;
|
String _tempToken;
|
||||||
String oauthUrl;
|
String oauthUrl;
|
||||||
String deviceName;
|
String webhookId;
|
||||||
|
String registeredAppVersion;
|
||||||
bool useLovelace = true;
|
bool useLovelace = true;
|
||||||
bool settingsLoaded = false;
|
bool settingsLoaded = false;
|
||||||
bool get isAuthenticated => _token != null;
|
bool get isAuthenticated => _token != null;
|
||||||
@ -43,11 +44,13 @@ class Connection {
|
|||||||
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
||||||
_domain = prefs.getString('hassio-domain');
|
_domain = prefs.getString('hassio-domain');
|
||||||
_port = prefs.getString('hassio-port');
|
_port = prefs.getString('hassio-port');
|
||||||
|
webhookId = prefs.getString('app-webhook-id');
|
||||||
|
registeredAppVersion = prefs.getString('registered-app-version');
|
||||||
displayHostname = "$_domain:$_port";
|
displayHostname = "$_domain:$_port";
|
||||||
_webSocketAPIEndpoint =
|
_webSocketAPIEndpoint =
|
||||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||||
httpWebHost =
|
httpWebHost =
|
||||||
"${prefs.getString('hassio-res-protocol')}://$_domain${(_port == '433' || _port == '80') ? '' : ':'+_port}";
|
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||||
if ((_domain == null) || (_port == null) ||
|
if ((_domain == null) || (_port == null) ||
|
||||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||||
completer.completeError(HAError.checkConnectionSettings());
|
completer.completeError(HAError.checkConnectionSettings());
|
||||||
@ -57,15 +60,12 @@ class Connection {
|
|||||||
final storage = new FlutterSecureStorage();
|
final storage = new FlutterSecureStorage();
|
||||||
try {
|
try {
|
||||||
_token = await storage.read(key: "hacl_llt");
|
_token = await storage.read(key: "hacl_llt");
|
||||||
Logger.e("Long-lived token read successful: $_token");
|
Logger.e("Long-lived token read successful");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.e("Cannt read secure storage. Need to relogin.");
|
Logger.e("Cannt read secure storage. Need to relogin.");
|
||||||
_token = null;
|
_token = null;
|
||||||
await storage.delete(key: "hacl_llt");
|
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(
|
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||||
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
||||||
.encodeComponent(
|
.encodeComponent(
|
||||||
@ -393,7 +393,7 @@ class Connection {
|
|||||||
body: data
|
body: data
|
||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
|
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
||||||
completer.complete(response.body);
|
completer.complete(response.body);
|
||||||
} else {
|
} else {
|
||||||
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
|
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
|
||||||
|
29
lib/device.class.dart
Normal file
29
lib/device.class.dart
Normal 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}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -46,10 +46,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
|||||||
// the App.build method, and use it to set our appbar title.
|
// the App.build method, and use it to set our appbar title.
|
||||||
title: new Text(_title),
|
title: new Text(_title),
|
||||||
),
|
),
|
||||||
body: HomeAssistantModel(
|
body: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||||
homeAssistant: widget.homeAssistant,
|
|
||||||
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
Widget _buildColorControl(LightEntity entity) {
|
Widget _buildColorControl(LightEntity entity) {
|
||||||
if (entity.supportColor) {
|
if (entity.supportColor) {
|
||||||
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
|
HSVColor savedColor = HomeAssistant().savedColor;
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -187,10 +187,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
child: Text('Copy color'),
|
child: Text('Copy color'),
|
||||||
onPressed: _tmpColor == null ? null : () {
|
onPressed: _tmpColor == null ? null : () {
|
||||||
setState(() {
|
setState(() {
|
||||||
HomeAssistantModel
|
HomeAssistant().savedColor = _tmpColor;
|
||||||
.of(context)
|
|
||||||
.homeAssistant
|
|
||||||
.savedColor = _tmpColor;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -15,26 +15,6 @@ class EntityModel extends InheritedWidget {
|
|||||||
return context.inheritFromWidgetOfExactType(EntityModel);
|
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
|
@override
|
||||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -2,6 +2,12 @@ part of 'main.dart';
|
|||||||
|
|
||||||
class HomeAssistant {
|
class HomeAssistant {
|
||||||
|
|
||||||
|
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||||
|
|
||||||
|
factory HomeAssistant() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
EntityCollection entities;
|
EntityCollection entities;
|
||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
@ -27,8 +33,9 @@ class HomeAssistant {
|
|||||||
bool get isNoViews => ui == null || ui.isEmpty;
|
bool get isNoViews => ui == null || ui.isEmpty;
|
||||||
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
||||||
|
|
||||||
HomeAssistant() {
|
HomeAssistant._internal() {
|
||||||
Connection().onStateChangeCallback = _handleEntityStateChange;
|
Connection().onStateChangeCallback = _handleEntityStateChange;
|
||||||
|
Device().loadDeviceInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Completer _fetchCompleter;
|
Completer _fetchCompleter;
|
||||||
@ -49,6 +56,7 @@ class HomeAssistant {
|
|||||||
futures.add(_getServices());
|
futures.add(_getServices());
|
||||||
futures.add(_getUserInfo());
|
futures.add(_getUserInfo());
|
||||||
futures.add(_getPanels());
|
futures.add(_getPanels());
|
||||||
|
futures.add(checkAppRegistration());
|
||||||
futures.add(Connection().sendSocketMessage(
|
futures.add(Connection().sendSocketMessage(
|
||||||
type: "subscribe_events",
|
type: "subscribe_events",
|
||||||
additionalData: {"event_type": "state_changed"},
|
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 {
|
Future _getConfig() async {
|
||||||
await Connection().sendSocketMessage(type: "get_config").then((data) {
|
await Connection().sendSocketMessage(type: "get_config").then((data) {
|
||||||
_instanceConfig = Map.from(data);
|
_instanceConfig = Map.from(data);
|
||||||
|
@ -93,6 +93,7 @@ part 'mdi.class.dart';
|
|||||||
part 'entity_collection.class.dart';
|
part 'entity_collection.class.dart';
|
||||||
part 'auth_manager.class.dart';
|
part 'auth_manager.class.dart';
|
||||||
part 'connection.class.dart';
|
part 'connection.class.dart';
|
||||||
|
part 'device.class.dart';
|
||||||
part 'ui_class/ui.dart';
|
part 'ui_class/ui.dart';
|
||||||
part 'ui_class/view.class.dart';
|
part 'ui_class/view.class.dart';
|
||||||
part 'ui_class/card.class.dart';
|
part 'ui_class/card.class.dart';
|
||||||
@ -230,7 +231,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_previousViewCount = currentViewCount;
|
_previousViewCount = currentViewCount;
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_setErrorState(e);
|
if (e is HAError) {
|
||||||
|
_setErrorState(e);
|
||||||
|
} else {
|
||||||
|
_setErrorState(HAError(e.toString()));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
eventBus.fire(RefreshDataFinishedEvent());
|
eventBus.fire(RefreshDataFinishedEvent());
|
||||||
}
|
}
|
||||||
@ -289,11 +294,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_firebaseMessaging.getToken().then((String token) {
|
/*_firebaseMessaging.getToken().then((String token) {
|
||||||
Logger.d("Device name: ${json.encode(Connection().deviceName)}");
|
Logger.d("Device name: ${json.encode(Connection().unicDeviceName)}");
|
||||||
Connection().sendHTTPPost(
|
Connection().sendHTTPPost(
|
||||||
endPoint: '/api/notify.ha-client',
|
endPoint: '/api/notify.ha-client',
|
||||||
data: '{"token": "$token", "device": ${json.encode(Connection().deviceName)}}'
|
data: '{"token": "$token", "device": ${json.encode(Connection().unicDeviceName)}}'
|
||||||
).then((_) {
|
).then((_) {
|
||||||
Logger.d("Notificatin listener registered.");
|
Logger.d("Notificatin listener registered.");
|
||||||
completer.complete();
|
completer.complete();
|
||||||
@ -304,7 +309,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
Logger.e("Error registering notification listener: ${e.toString()}");
|
Logger.e("Error registering notification listener: ${e.toString()}");
|
||||||
completer.complete();
|
completer.complete();
|
||||||
});
|
});*/
|
||||||
|
completer.complete();
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,7 +400,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
||||||
title: Text("${panel.title}"),
|
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(),
|
drawer: _buildAppDrawer(),
|
||||||
primary: false,
|
primary: false,
|
||||||
bottomNavigationBar: bottomBar,
|
bottomNavigationBar: bottomBar,
|
||||||
body: HomeAssistantModel(
|
body: _buildScaffoldBody(false),
|
||||||
child: _buildScaffoldBody(false),
|
|
||||||
homeAssistant: widget.homeAssistant
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user