This commit is contained in:
estevez-dev
2019-03-20 19:01:30 +02:00
parent 5ae580ecf1
commit 6a03105d01
6 changed files with 186 additions and 80 deletions

View File

@ -131,7 +131,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
.of(context) .of(context)
.entityWrapper .entityWrapper
.entity; .entity;
_webHost = HomeAssistantModel.of(context).homeAssistant.httpAPIEndpoint; _webHost = HomeAssistantModel.of(context).homeAssistant.httpWebHost;
_connect(); _connect();
} }

View File

@ -2,8 +2,10 @@ part of 'main.dart';
class HomeAssistant { class HomeAssistant {
String _webSocketAPIEndpoint; String _webSocketAPIEndpoint;
String httpAPIEndpoint; String httpWebHost;
String _password; //String _password;
String _token;
String _tempToken;
bool _useLovelace = false; bool _useLovelace = false;
bool isSettingsLoaded = false; bool isSettingsLoaded = false;
@ -57,15 +59,16 @@ class HomeAssistant {
String port = prefs.getString('hassio-port'); String port = prefs.getString('hassio-port');
hostname = "$domain:$port"; hostname = "$domain:$port";
_webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; _webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
httpAPIEndpoint = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
_password = prefs.getString('hassio-password'); //_password = prefs.getString('hassio-password');
_token = prefs.getString('hassio-token');
_useLovelace = prefs.getBool('use-lovelace') ?? true; _useLovelace = prefs.getBool('use-lovelace') ?? true;
if ((domain == null) || (port == null) || (_password == null) || if ((domain == null) || (port == null) ||
(domain.length == 0) || (port.length == 0) || (_password.length == 0)) { (domain.length == 0) || (port.length == 0)) {
throw("Check connection settings"); throw("Check connection settings");
} else { } else {
isSettingsLoaded = true; isSettingsLoaded = true;
entities = EntityCollection(httpAPIEndpoint); entities = EntityCollection(httpWebHost);
} }
} }
@ -80,6 +83,7 @@ class HomeAssistant {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) { if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
Logger.w("Previous fetch is not complited"); Logger.w("Previous fetch is not complited");
} else { } else {
Logger.d("Fetching...");
_fetchCompleter = new Completer(); _fetchCompleter = new Completer();
_fetchTimer = Timer(fetchTimeout, () { _fetchTimer = Timer(fetchTimeout, () {
Logger.e( "Data fetching timeout"); Logger.e( "Data fetching timeout");
@ -178,6 +182,9 @@ class HomeAssistant {
if (_useLovelace) { if (_useLovelace) {
futures.add(_getLovelace()); futures.add(_getLovelace());
} }
if (_token == null && _tempToken != null) {
futures.add(_getLongLivedToken());
}
futures.add(_getConfig()); futures.add(_getConfig());
futures.add(_getServices()); futures.add(_getServices());
futures.add(_getUserInfo()); futures.add(_getUserInfo());
@ -226,14 +233,18 @@ class HomeAssistant {
_handleMessage(String message) { _handleMessage(String message) {
var data = json.decode(message); var data = json.decode(message);
if (data["type"] == "auth_required") { if (data["type"] == "auth_required") {
_sendAuthMessage('{"type": "auth","access_token": "$_password"}'); Logger.d("[Received] <== ${data.toString()}");
_sendAuth();
} else if (data["type"] == "auth_ok") { } else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
_completeConnecting(null); _completeConnecting(null);
_sendSubscribe(); _sendSubscribe();
} else if (data["type"] == "auth_invalid") { } else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
//TODO remove token and login again
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"}); _completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") { } 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[data["id"]]?.complete(data);
_messageResolver.remove(data["id"]); _messageResolver.remove(data["id"]);
} else if (data["type"] == "event") { } else if (data["type"] == "event") {
@ -246,40 +257,56 @@ class HomeAssistant {
Logger.e("Event is null: $message"); Logger.e("Event is null: $message");
} }
} else { } else {
Logger.w("Unknown message type: $message"); Logger.d("[Received] <== ${data.toString()}");
} }
} }
void _sendSubscribe() { void _sendSubscribe() {
_incrementMessageId(); _incrementMessageId();
_subscriptionMessageId = _currentMessageId; _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 { 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 { 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 { 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 { Future _getUserInfo() async {
_userName = null; _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 { 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 { Future _getPanels() async {
panels.clear(); panels.clear();
await _sendInitialMessage("get_panels").then((data) { await _sendSocketMessage(type: "get_panels").then((data) {
if (data["success"]) { if (data["success"]) {
data["result"].forEach((k,v) { data["result"].forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}"; 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; _currentMessageId += 1;
} }
void _sendAuthMessage(String message) { void _sendAuth() {
if (_token != null) {
Logger.d( "Long leaved token exist");
Logger.d( "[Sending] ==> auth request"); Logger.d( "[Sending] ==> auth request");
_hassioChannel.sink.add(message); _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(); Completer _completer = Completer();
Map dataObject = {"type": "$type"};
if (!noId) {
_incrementMessageId(); _incrementMessageId();
dataObject["id"] = _currentMessageId;
}
if (additionalData != null) {
dataObject.addAll(additionalData);
}
_messageResolver[_currentMessageId] = _completer; _messageResolver[_currentMessageId] = _completer;
_send('{"id": $_currentMessageId, "type": "$type"}', false); _rawSend(json.encode(dataObject), false);
return _completer.future; return _completer.future;
} }
_send(String message, bool queued) { _rawSend(String message, bool queued) {
var sendCompleter = Completer(); var sendCompleter = Completer();
if (queued) _messageQueue.add(message); if (queued) {
_messageQueue.add(message);
}
_connection().then((r) { _connection().then((r) {
_messageQueue.getActualMessages().forEach((message){ _messageQueue.getActualMessages().forEach((message){
Logger.d( "[Sending queued] ==> $message"); Logger.d( "[Sending queued] ==> $message");
@ -369,7 +447,7 @@ class HomeAssistant {
} }
message += '}'; message += '}';
} }
return _send(message, true); return _rawSend(message, true);
} }
void _handleEntityStateChange(Map eventData) { void _handleEntityStateChange(Map eventData) {
@ -583,11 +661,11 @@ class HomeAssistant {
DateTime now = DateTime.now(); DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]); //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 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"); Logger.d("[Sending] ==> $url");
http.Response historyResponse; http.Response historyResponse;
historyResponse = await http.get(url, headers: { historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_password", "authorization": "Bearer $_token",
"Content-Type": "application/json" "Content-Type": "application/json"
}); });
var history = json.decode(historyResponse.body); var history = json.decode(historyResponse.body);
@ -599,20 +677,33 @@ class HomeAssistant {
} }
} }
Future sendHTTPRequest(String data) async { Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
String url = "$httpAPIEndpoint/api/notify.fcm-android"; Completer completer = Completer();
String url = "$httpWebHost$endPoint";
Logger.d("[Sending] ==> $url"); Logger.d("[Sending] ==> $url");
http.Response response; Map<String, String> headers = {};
response = await http.post( if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $_token";
}
http.post(
url, url,
headers: { headers: headers,
"authorization": "Bearer $_password",
"Content-Type": "application/json"
},
body: data body: data
); ).then((response) {
//var resData = json.decode(response.body);
Logger.d("[Received] <== ${response.statusCode}, ${response.body}"); 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;
} }
} }

View File

@ -169,6 +169,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
StreamSubscription _serviceCallSubscription; StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription; StreamSubscription _showEntityPageSubscription;
StreamSubscription _showErrorSubscription; StreamSubscription _showErrorSubscription;
StreamSubscription _startAuthSubscription;
StreamSubscription _reloadUISubscription;
//bool _settingsLoaded = false; //bool _settingsLoaded = false;
bool _accountMenuExpanded = false; bool _accountMenuExpanded = false;
//bool _useLovelaceUI; //bool _useLovelaceUI;
@ -239,6 +241,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
}); });
} }
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_refreshData();
});
}
if (_serviceCallSubscription == null) { if (_serviceCallSubscription == null) {
_serviceCallSubscription = _serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) { eventBus.on<ServiceCallEvent>().listen((event) {
@ -260,9 +267,28 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}); });
} }
_firebaseMessaging.getToken().then((String token) { if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().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"); //Logger.d("FCM token: $token");
widget.homeAssistant.sendHTTPRequest('{"token": "$token"}'); widget.homeAssistant.sendHTTPPost(
endPoint: '/api/notify.fcm-android',
jsonData: '{"token": "$token"}'
);
}); });
_firebaseMessaging.configure( _firebaseMessaging.configure(
onLaunch: (data) { onLaunch: (data) {
@ -274,13 +300,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
onResume: (data) { onResume: (data) {
Logger.d("Notification [onResume]: $data"); Logger.d("Notification [onResume]: $data");
} }
); );*/
} }
_refreshData() async { _refreshData() async {
//widget.homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI); //widget.homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
_hideBottomBar(); _hideBottomBar();
_showInfoBottomBar(progress: true,); _showInfoBottomBar(progress: true,);
Logger.d("Calling fetch()");
await widget.homeAssistant.fetch().then((result) { await widget.homeAssistant.fetch().then((result) {
_hideBottomBar(); _hideBottomBar();
int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0; int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0;
@ -390,7 +417,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
new ListTile( new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")), leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
title: Text("Open Web UI"), title: Text("Open Web UI"),
onTap: () => HAUtils.launchURL(widget.homeAssistant.httpAPIEndpoint), onTap: () => HAUtils.launchURL(widget.homeAssistant.httpWebHost),
), ),
Divider() Divider()
]); ]);
@ -715,14 +742,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
@override @override
void dispose() { void dispose() {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.dispose();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_viewsTabController.dispose(); _viewsTabController?.dispose();
if (_stateSubscription != null) _stateSubscription.cancel(); _stateSubscription?.cancel();
if (_settingsSubscription != null) _settingsSubscription.cancel(); _settingsSubscription?.cancel();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); _serviceCallSubscription?.cancel();
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); _showEntityPageSubscription?.cancel();
if (_showErrorSubscription != null) _showErrorSubscription.cancel(); _showErrorSubscription?.cancel();
widget.homeAssistant.disconnect(); _startAuthSubscription?.cancel();
_reloadUISubscription?.cancel();
widget.homeAssistant?.disconnect();
super.dispose(); super.dispose();
} }
} }

View File

@ -1,10 +1,9 @@
part of 'main.dart'; part of 'main.dart';
class ConnectionSettingsPage extends StatefulWidget { 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 String title;
final HomeAssistant homeAssistant;
@override @override
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState(); _ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
@ -23,20 +22,12 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
bool _newUseLovelace = true; bool _newUseLovelace = true;
String oauthUrl; String oauthUrl;
final flutterWebviewPlugin = new FlutterWebviewPlugin();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadSettings(); _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 { _loadSettings() async {
@ -79,22 +70,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
@override @override
Widget build(BuildContext context) { 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( return new Scaffold(
appBar: new AppBar( appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
@ -177,7 +152,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
"Try ports 80 and 443 if default is not working and you don't know why.", "Try ports 80 and 443 if default is not working and you don't know why.",
style: TextStyle(color: Colors.grey), style: TextStyle(color: Colors.grey),
), ),
webViewButton,
new TextField( new TextField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: "Access token" labelText: "Access token"

View File

@ -37,7 +37,7 @@ class Panel {
); );
} else { } else {
HomeAssistantModel haModel = HomeAssistantModel.of(context); HomeAssistantModel haModel = HomeAssistantModel.of(context);
String url = "${haModel.homeAssistant.httpAPIEndpoint}/$urlPath"; String url = "${haModel.homeAssistant.httpWebHost}/$urlPath";
Logger.d("Launching custom tab with $url"); Logger.d("Launching custom tab with $url");
HAUtils.launchURLInCustomTab(context, url); HAUtils.launchURLInCustomTab(context, url);
} }

View File

@ -109,6 +109,16 @@ class RefreshDataFinishedEvent {
RefreshDataFinishedEvent(); RefreshDataFinishedEvent();
} }
class ReloadUIEvent {
ReloadUIEvent();
}
class StartAuthEvent {
String oauthUrl;
StartAuthEvent(this.oauthUrl);
}
class ServiceCallEvent { class ServiceCallEvent {
String domain; String domain;
String service; String service;