Resolves #340 Connection refactoring

This commit is contained in:
estevez-dev 2019-03-26 00:18:30 +02:00
parent 4a75243994
commit 7c010359c3
7 changed files with 495 additions and 520 deletions

View File

@ -0,0 +1,44 @@
part of 'main.dart';
class AuthManager {
static final AuthManager _instance = AuthManager._internal();
factory AuthManager() {
return _instance;
}
AuthManager._internal();
Future getTempToken({String httpWebHost, String oauthUrl}) {
Completer completer = Completer();
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.onUrlChanged.listen((String 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...");
Connection().sendHTTPPost(
host: httpWebHost,
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");
String tempToken = json.decode(response)['access_token'];
Logger.d("Closing webview...");
flutterWebviewPlugin.close();
completer.complete(tempToken);
}).catchError((e) {
flutterWebviewPlugin.close();
completer.completeError({"errorCode": 61, "errorMessage": "Error getting temp token"});
Logger.e("Error getting temp token: ${e.toString()}");
});
}
});
Logger.d("Launching OAuth...");
eventBus.fire(StartAuthEvent(oauthUrl));
return completer.future;
}
}

330
lib/connection.class.dart Normal file
View File

@ -0,0 +1,330 @@
part of 'main.dart';
class Connection {
static final Connection _instance = Connection._internal();
factory Connection() {
return _instance;
}
Connection._internal();
String displayHostname;
String _webSocketAPIEndpoint;
String httpWebHost;
String _token;
String _tempToken;
String oauthUrl;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
Duration connectTimeout = Duration(seconds: 15);
bool isConnected = false;
var onStateChangeCallback;
IOWebSocketChannel _socket;
int _currentMessageId = 0;
Map<String, Completer> _messageResolver = {};
Future init(onStateChange) async {
Completer completer = Completer();
onStateChangeCallback = onStateChange;
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port');
displayHostname = "$domain:$port";
_webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
//_token = prefs.getString('hassio-token');
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
} catch (e) {
Logger.e("Cannt read secure storage. Need to relogin.");
_token = null;
await storage.delete(key: "hacl_llt");
}
if ((domain == null) || (port == null) ||
(domain.length == 0) || (port.length == 0)) {
completer.completeError({"errorCode": 5, "errorMessage": "Check connection settings"});
} else {
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')}";
if (_token == null) {
await AuthManager().getTempToken(
httpWebHost: httpWebHost,
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
_tempToken = token;
});
}
completer.complete(_connect());
}
return completer.future;
}
Future _connect() async {
Completer completer = Completer();
Timer connectionTimer = Timer(connectTimeout, () {
if (!completer.isCompleted) completer.completeError({"errorCode": 1, "errorMessage": "Connection timeout"});
});
await _disconnect();
Logger.d( "Socket connecting...");
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
(message) {
isConnected = true;
connectionTimer.cancel();
var data = json.decode(message);
if (data["type"] == "auth_required") {
Logger.d("[Received] <== ${data.toString()}");
_authenticate().then((_) => completer.complete()).catchError((e) {
if (!completer.isCompleted) completer.completeError(e);
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.complete();
_messageResolver.remove("auth");
if (!completer.isCompleted) completer.complete(sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
));
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"});
_messageResolver.remove("auth");
logout().then((_) {
if (!completer.isCompleted) completer.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"});
});
} else {
_handleMessage(data);
}
},
cancelOnError: true,
onDone: () => _handleSocketClose(completer),
onError: (e) => _handleSocketError(e, completer)
);
return completer.future;
}
Future _disconnect() async {
Logger.d( "Socket disconnecting...");
await _socketSubscription?.cancel();
await _socket?.sink?.close()?.timeout(Duration(seconds: 4),
onTimeout: () => Logger.d( "Socket sink close timeout")
);
Logger.d( "..Disconnected");
}
_handleMessage(data) {
if (data["type"] == "result") {
if (data["id"] != null && data["success"]) {
Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError({"errorMessage": "${data['error']["message"]}"});
}
_messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]);
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $data");
}
} else {
Logger.d("[Received unhandled] <== ${data.toString()}");
}
}
void _handleSocketClose(Completer connectionCompleter) {
isConnected = false;
Logger.d("Socket disconnected.");
if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError({"errorCode": 82, "errorMessage": "Disconnected"});
} else {
//TODO improve
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect();
});
});
}
}
void _handleSocketError(e, Completer connectionCompleter) {
isConnected = false;
Logger.e("Socket stream Error: $e");
if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError({"errorCode": 81, "errorMessage": "Unable to connect to Home Assistant"});
} else {
//TODO improve
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect();
});
});
}
}
Future _authenticate() {
Completer completer = Completer();
if (_token != null) {
Logger.d( "Long-lived token exist");
Logger.d( "[Sending] ==> auth request");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_token"},
auth: true
).then((_) {
completer.complete();
}).catchError((e) => completer.completeError(e));
} else if (_tempToken != null) {
Logger.d("We have temp token. Loging in...");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_tempToken"},
auth: true
).then((_) {
Logger.d("Requesting long-lived token...");
_getLongLivedToken().then((_) {
completer.complete();
}).catchError((e) {
Logger.e("Can't get long-lived token: $e");
throw e;
});
}).catchError((e) => completer.completeError(e));
} else {
completer.completeError({"errorCode": 63, "errorMessage": "General login error"});
}
return completer.future;
}
Future logout() {
_token = null;
_tempToken = null;
final storage = new FlutterSecureStorage();
return storage.delete(key: "hacl_llt");
}
Future _getLongLivedToken() {
Completer completer = Completer();
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
Logger.d("Got long-lived token.");
_token = data;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: "$_token").then((_) {
completer.complete();
}).catchError((e) {
throw e;
});
}).catchError((e) {
logout();
completer.completeError({"errorCode": 63, "errorMessage": "Authentication error: $e"});
});
return completer.future;
}
Future sendSocketMessage({String type, Map additionalData, bool auth: false}) {
Completer _completer = Completer();
if (!isConnected) {
_completer.completeError({"errorCode": 8, "errorMessage": "No connection to Home Assistant"});
}
Map dataObject = {"type": "$type"};
String callbackName;
if (!auth) {
_incrementMessageId();
dataObject["id"] = _currentMessageId;
callbackName = "$_currentMessageId";
} else {
callbackName = "auth";
}
if (additionalData != null) {
dataObject.addAll(additionalData);
}
_messageResolver[callbackName] = _completer;
//TODO add message to q and send after reconnect
String rawMessage = json.encode(dataObject);
Logger.d("[Sending] ==> $rawMessage");
_socket.sink.add(rawMessage);
return _completer.future;
}
void _incrementMessageId() {
_currentMessageId += 1;
}
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
Map serviceData = {};
if (entityId != null) {
serviceData["entity_id"] = entityId;
}
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
serviceData.addAll(additionalServiceData);
}
if (serviceData.isNotEmpty)
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
}
Future<List> getHistory(String entityId) async {
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 = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> $url");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_token",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
if (history is List) {
Logger.d( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];
}
}
Future sendHTTPPost({String host, String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true, String authToken}) async {
Completer completer = Completer();
String url = "$host$endPoint";
Logger.d("[Sending] ==> $url");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $authToken";
}
http.post(
url,
headers: headers,
body: data
).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;
}
}

View File

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

View File

@ -47,7 +47,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
} }
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) { if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now; _historyLastUpdated = now;
ha.getHistory(entityId).then((history){ ha.connection.getHistory(entityId).then((history){
if (!_disposed) { if (!_disposed) {
setState(() { setState(() {
_history = history.isNotEmpty ? history[0] : []; _history = history.isNotEmpty ? history[0] : [];

View File

@ -1,20 +1,15 @@
part of 'main.dart'; part of 'main.dart';
class HomeAssistant { class HomeAssistant {
String _webSocketAPIEndpoint;
String httpWebHost; final Connection connection = Connection();
String oauthUrl;
//String _password;
String _token;
String _tempToken;
bool _useLovelace = false; bool _useLovelace = false;
bool isSettingsLoaded = false; //bool isSettingsLoaded = false;
IOWebSocketChannel _hassioChannel;
SendMessageQueue _messageQueue;
int _currentMessageId = 0;
Map<int, Completer> _messageResolver = {};
EntityCollection entities; EntityCollection entities;
HomeAssistantUI ui; HomeAssistantUI ui;
Map _instanceConfig = {}; Map _instanceConfig = {};
@ -26,17 +21,7 @@ class HomeAssistant {
List<Panel> panels = []; List<Panel> panels = [];
Completer _fetchCompleter;
Completer _connectionCompleter;
Timer _connectionTimer;
Timer _fetchTimer;
bool autoReconnect = false;
StreamSubscription _socketSubscription;
int messageExpirationTime = 30; //seconds
Duration fetchTimeout = Duration(seconds: 30); Duration fetchTimeout = Duration(seconds: 30);
Duration connectTimeout = Duration(seconds: 15);
String get locationName { String get locationName {
if (_useLovelace) { if (_useLovelace) {
@ -49,253 +34,65 @@ class HomeAssistant {
String get userAvatarText => userName.length > 0 ? userName[0] : ""; String get userAvatarText => userName.length > 0 ? userName[0] : "";
bool get isNoEntities => entities == null || entities.isEmpty; bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty; bool get isNoViews => ui == null || ui.isEmpty;
bool get isAuthenticated => _token != null;
//int get viewsCount => entities.views.length ?? 0; //int get viewsCount => entities.views.length ?? 0;
HomeAssistant() { HomeAssistant();
_messageQueue = SendMessageQueue(messageExpirationTime);
}
Future loadConnectionSettings() async { Completer _connectCompleter;
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain'); Future init() {
String port = prefs.getString('hassio-port'); if (_connectCompleter != null && !_connectCompleter.isCompleted) {
hostname = "$domain:$port"; Logger.w("Previous connection pending...");
_webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; return _connectCompleter.future;
httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
_token = prefs.getString('hassio-token');
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
} catch (e) {
Logger.e("Cannt read secure storage. Need to relogin.");
_token = null;
await storage.delete(key: "hacl_llt");
} }
Logger.d("init...");
_connectCompleter = Completer();
connection.init(_handleEntityStateChange).then((_) {
SharedPreferences.getInstance().then((prefs) {
if (entities == null) entities = EntityCollection(connection.httpWebHost);
_useLovelace = prefs.getBool('use-lovelace') ?? true; _useLovelace = prefs.getBool('use-lovelace') ?? true;
if ((domain == null) || (port == null) || _connectCompleter.complete();
(domain.length == 0) || (port.length == 0)) { }).catchError((e) => _connectCompleter.completeError(e));
throw("Check connection settings"); }).catchError((e) => _connectCompleter.completeError(e));
} else { return _connectCompleter.future;
isSettingsLoaded = true;
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')}";
entities = EntityCollection(httpWebHost);
}
} }
/*void updateSettings(String url, String password, bool useLovelace) { Completer _fetchCompleter;
_webSocketAPIEndpoint = url;
_password = password;
_useLovelace = useLovelace;
Logger.d( "Use lovelace is $_useLovelace");
}*/
Future fetch() { Future fetch() {
//return _connection().then((_) => _getData()); if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
Logger.w("Previous data fetch is not completed yet");
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
Logger.w("Previous fetch is not complited");
} else {
Logger.d("Fetching...");
_fetchCompleter = new Completer();
_fetchTimer?.cancel();
_fetchTimer = Timer(fetchTimeout, () {
Logger.e( "Data fetching timeout");
disconnect().then((_) {
_completeFetching({
"errorCode": 9,
"errorMessage": "Couldn't get data from server"
});
});
});
_connection().then((r) {
_getData();
}).catchError((e) {
_completeFetching(e);
});
}
return _fetchCompleter.future; return _fetchCompleter.future;
} }
_fetchCompleter = Completer();
Future disconnect() async {
Logger.d( "Socket disconnecting...");
await _socketSubscription?.cancel();
await _hassioChannel?.sink?.close()?.timeout(Duration(seconds: 4),
onTimeout: () => Logger.d( "Socket sink close timeout")
);
_hassioChannel = null;
Logger.d( "..Disconnected");
}
Future _connection() {
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
Logger.d("Previous connection is not complited");
} else {
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionTimer?.cancel();
_connectionCompleter = new Completer();
autoReconnect = false;
disconnect().then((_){
Logger.d( "Socket connecting...");
_connectionTimer = Timer(connectTimeout, () {
Logger.e( "Socket connection timeout");
_handleSocketError(null);
});
_socketSubscription?.cancel();
_hassioChannel = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
_socketSubscription = _hassioChannel.stream.listen(
(message) => _handleMessage(message),
cancelOnError: true,
onDone: () => _handleSocketClose(),
onError: (e) => _handleSocketError(e)
);
});
} else {
_completeConnecting(null);
}
}
return _connectionCompleter.future;
}
void _handleSocketClose() {
Logger.d("Socket disconnected. Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
}
}
void _handleSocketError(e) {
Logger.e("Socket stream Error: $e");
Logger.d("Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
} else {
disconnect().then((_) {
_completeConnecting({
"errorCode": 1,
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
});
});
}
}
void _reconnect() {
disconnect().then((_) {
_connection().catchError((e){
_completeConnecting(e);
});
});
}
_getData() async {
List<Future> futures = []; List<Future> futures = [];
futures.add(_getStates()); futures.add(_getStates());
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());
futures.add(_getPanels()); futures.add(_getPanels());
futures.add( Future.wait(futures).then((_) {
_sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
)
);
await Future.wait(futures).then((_) {
_createUI(); _createUI();
_completeFetching(null);
}).catchError((e) {
disconnect().then((_) =>
_completeFetching(e)
);
});
}
void _completeFetching(error) {
_fetchTimer?.cancel();
_completeConnecting(error);
if (!_fetchCompleter.isCompleted) {
if (error != null) {
_fetchCompleter.completeError(error);
} else {
autoReconnect = true;
Logger.d( "Fetch complete successful");
_fetchCompleter.complete(); _fetchCompleter.complete();
} }).catchError((e) {
} _fetchCompleter.completeError(e);
} });
return _fetchCompleter.future;
void _completeConnecting(error) {
_connectionTimer?.cancel();
if (!_connectionCompleter.isCompleted) {
if (error != null) {
_connectionCompleter.completeError(error);
} else {
_connectionCompleter.complete();
}
} else if (error != null) {
if (error is Error) {
eventBus.fire(ShowErrorEvent(error.toString(), 12));
} else {
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
}
}
}
_handleMessage(String message) {
var data = json.decode(message);
if (data["type"] == "auth_required") {
Logger.d("[Received] <== ${data.toString()}");
_sendAuth();
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
_completeConnecting(null);
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
logout();
_completeConnecting({"errorCode": 62, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
if (data["success"]) {
Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver[data["id"]]?.complete(data["result"]);
} else {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver[data["id"]]?.completeError(data['error']["message"]);
}
_messageResolver.remove(data["id"]);
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
_handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $message");
}
} else {
Logger.d("[Received] <== ${data.toString()}");
}
} }
Future logout() async { Future logout() async {
Logger.d("Logging out..."); Logger.d("Logging out...");
_token = null; await connection.logout().then((_) {
_tempToken = null;
final storage = new FlutterSecureStorage();
await storage.delete(key: "hacl_llt");
ui?.clear(); ui?.clear();
entities?.clear(); entities?.clear();
});
} }
Future _getConfig() async { Future _getConfig() async {
return _sendSocketMessage(type: "get_config").then((data) { await connection.sendSocketMessage(type: "get_config").then((data) {
_instanceConfig = Map.from(data); _instanceConfig = Map.from(data);
}).catchError((e) { }).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting config: $e"}; throw {"errorCode": 1, "errorMessage": "Error getting config: $e"};
@ -303,48 +100,35 @@ class HomeAssistant {
} }
Future _getStates() async { Future _getStates() async {
return _sendSocketMessage(type: "get_states").then( await connection.sendSocketMessage(type: "get_states").then(
(data) => entities.parse(data) (data) => entities.parse(data)
).catchError((e) { ).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting states: $e"}; throw {"errorCode": 1, "errorMessage": "Error getting states: $e"};
}); });
} }
Future _getLongLivedToken() async {
return _sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
Logger.d("Got long-lived token.");
_token = data;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: _token);
}).catchError((e) {
logout();
throw {"errorCode": 63, "errorMessage": "Authentication error: $e"};
});
}
Future _getLovelace() async { Future _getLovelace() async {
return _sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) { await connection.sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"}; throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"};
}); });
} }
Future _getUserInfo() async { Future _getUserInfo() async {
_userName = null; _userName = null;
return _sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) { await connection.sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
Logger.w("Can't get user info: ${e}"); Logger.w("Can't get user info: ${e}");
}); });
} }
Future _getServices() async { Future _getServices() async {
return _sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) { await connection.sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
Logger.w("Can't get services: ${e}"); Logger.w("Can't get services: ${e}");
}); });
} }
Future _getPanels() async { Future _getPanels() async {
panels.clear(); panels.clear();
return _sendSocketMessage(type: "get_panels").then((data) { await connection.sendSocketMessage(type: "get_panels").then((data) {
data.forEach((k,v) { data.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)}";
panels.add(Panel( panels.add(Panel(
@ -359,139 +143,8 @@ class HomeAssistant {
}); });
}).catchError((e) { }).catchError((e) {
throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"}; throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"};
});;
}
_incrementMessageId() {
_currentMessageId += 1;
}
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) {
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) {
logout();
disconnect();
flutterWebviewPlugin.close();
_completeFetching({"errorCode": 61, "errorMessage": "Error getting temp token"});
Logger.e("Error getting temp token: ${e.toString()}");
}); });
} }
});
disconnect();
_completeFetching({"errorCode": 60, "errorMessage": "Not authenticated"});
_requestOAuth();
} 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");
logout();
disconnect();
_completeFetching({"errorCode": 61, "errorMessage": "General login error"});
}
}
void _requestOAuth() {
Logger.d("OAuth url: $oauthUrl");
eventBus.fire(StartAuthEvent(oauthUrl));
}
Future _sendSocketMessage({String type, Map additionalData, bool noId: false}) {
Completer _completer = Completer();
Map dataObject = {"type": "$type"};
if (!noId) {
_incrementMessageId();
dataObject["id"] = _currentMessageId;
}
if (additionalData != null) {
dataObject.addAll(additionalData);
}
_messageResolver[_currentMessageId] = _completer;
_rawSend(json.encode(dataObject), false);
return _completer.future;
}
_rawSend(String message, bool queued) {
var sendCompleter = Completer();
if (queued) {
_messageQueue.add(message);
}
_connection().then((r) {
_messageQueue.getActualMessages().forEach((message){
Logger.d( "[Sending queued] ==> $message");
_hassioChannel.sink.add(message);
});
if (!queued) {
Logger.d( "[Sending] ==> $message");
_hassioChannel.sink.add(message);
}
sendCompleter.complete();
}).catchError((e){
sendCompleter.completeError(e);
});
return sendCompleter.future;
}
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_incrementMessageId();
String message = "";
if (entityId != null) {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value) {
if ((value is double) || (value is int) || (value is List)) {
message += ', "$name" : $value';
} else {
message += ', "$name" : "$value"';
}
});
}
message += '}}';
} else {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"';
if (additionalParams != null && additionalParams.isNotEmpty) {
message += ', "service_data": {';
bool first = true;
additionalParams.forEach((name, value) {
if (!first) {
message += ', ';
}
if ((value is double) || (value is int) || (value is List)) {
message += '"$name" : $value';
} else {
message += '"$name" : "$value"';
}
first = false;
});
message += '}';
}
message += '}';
}
return _rawSend(message, true);
}
void _handleEntityStateChange(Map eventData) { void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}"); //TheLogger.debug( "New state for ${eventData['entity_id']}");
@ -699,57 +352,9 @@ class HomeAssistant {
Widget buildViews(BuildContext context, TabController tabController) { Widget buildViews(BuildContext context, TabController tabController) {
return ui.build(context, tabController); return ui.build(context, tabController);
} }
Future<List> getHistory(String entityId) async {
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 = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> $url");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_token",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
if (history is List) {
Logger.d( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];
}
}
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");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $_token";
}
http.post(
url,
headers: headers,
body: data
).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;
}
} }
/*
class SendMessageQueue { class SendMessageQueue {
int _messageTimeout; int _messageTimeout;
List<HAMessage> _queue = []; List<HAMessage> _queue = [];
@ -788,4 +393,4 @@ class HAMessage {
bool isExpired() { bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout; return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
} }
} }*/

View File

@ -90,6 +90,8 @@ part 'entity.page.dart';
part 'utils.class.dart'; part 'utils.class.dart';
part 'mdi.class.dart'; part 'mdi.class.dart';
part 'entity_collection.class.dart'; part 'entity_collection.class.dart';
part 'auth_manager.class.dart';
part 'connection.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';
@ -178,9 +180,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}"); Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) { if (event.reconnect) {
widget.homeAssistant.disconnect().then((_){ _reLoad();
_initialLoad();
});
} }
}); });
@ -188,19 +188,45 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
void _initialLoad() { void _initialLoad() {
widget.homeAssistant.loadConnectionSettings().then((_){ _showInfoBottomBar(progress: true,);
_subscribe(); _subscribe();
_refreshData(); widget.homeAssistant.init().then((_){
}, onError: (_) { _fetchData();
_showErrorBottomBar(message: _, errorCode: 5); }, onError: (e) {
_setErrorState(e);
}); });
} }
void _reLoad() {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
widget.homeAssistant.init().then((_){
_fetchData();
}, onError: (e) {
_setErrorState(e);
});
}
_fetchData() async {
await widget.homeAssistant.fetch().then((_) {
_hideBottomBar();
int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
}).catchError((e) {
_setErrorState(e);
});
eventBus.fire(RefreshDataFinishedEvent());
}
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
//Logger.d("$state"); Logger.d("$state");
if (state == AppLifecycleState.resumed && widget.homeAssistant.isSettingsLoaded) { if (state == AppLifecycleState.resumed) {
_refreshData(); _reLoad();
} }
} }
@ -209,7 +235,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) { if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI"); Logger.d("New entity. Need to rebuild UI");
_refreshData(); _reLoad();
} else { } else {
setState(() {}); setState(() {});
} }
@ -217,7 +243,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
if (_reloadUISubscription == null) { if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){ _reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_refreshData(); _reLoad();
}); });
} }
if (_serviceCallSubscription == null) { if (_serviceCallSubscription == null) {
@ -274,34 +300,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => WebviewScaffold( builder: (context) => WebviewScaffold(
url: "${widget.homeAssistant.oauthUrl}", url: "${widget.homeAssistant.connection.oauthUrl}",
appBar: new AppBar( appBar: new AppBar(
title: new Text("Login"), leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/docs#authentication")
),
title: new Text("Login to your Home Assistant"),
), ),
), ),
) )
); );
} }
_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;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
}).catchError((e) {
_setErrorState(e);
});
eventBus.fire(RefreshDataFinishedEvent());
}
_setErrorState(e) { _setErrorState(e) {
if (e is Error) { if (e is Error) {
Logger.e(e.toString()); Logger.e(e.toString());
@ -318,12 +329,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
} }
void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) { void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar( _showInfoBottomBar(
message: "Calling $domain.$service", message: "Calling $domain.$service",
duration: Duration(seconds: 3) duration: Duration(seconds: 3)
); );
widget.homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e)); widget.homeAssistant.connection.callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
} }
void _showEntityPage(String entityId) { void _showEntityPage(String entityId) {
@ -381,15 +392,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
}); });
} }
if (widget.homeAssistant.isSettingsLoaded) { //TODO check for loaded
menuItems.add( menuItems.add(
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.httpWebHost), onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost),
) )
); );
}
menuItems.addAll([ menuItems.addAll([
Divider(), Divider(),
ListTile( ListTile(
@ -500,7 +510,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
child: Text("Retry", style: textStyle), child: Text("Retry", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); //_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData(); _reLoad();
}, },
); );
break; break;
@ -522,7 +532,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_bottomBarAction = FlatButton( _bottomBarAction = FlatButton(
child: Text("Login", style: textStyle), child: Text("Login", style: textStyle),
onPressed: () { onPressed: () {
_refreshData(); _reLoad();
}, },
); );
break; break;
@ -533,7 +543,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_bottomBarAction = FlatButton( _bottomBarAction = FlatButton(
child: Text("Try again", style: textStyle), child: Text("Try again", style: textStyle),
onPressed: () { onPressed: () {
_refreshData(); _reLoad();
}, },
); );
break; break;
@ -543,7 +553,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_bottomBarAction = FlatButton( _bottomBarAction = FlatButton(
child: Text("Login again", style: textStyle), child: Text("Login again", style: textStyle),
onPressed: () { onPressed: () {
_refreshData(); _reLoad();
}, },
); );
break; break;
@ -554,31 +564,26 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
child: Text("Refresh", style: textStyle), child: Text("Refresh", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); //_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData(); _reLoad();
}, },
); );
break; break;
} }
case 82:
case 81:
case 8: { case 8: {
_bottomBarAction = FlatButton( _bottomBarAction = FlatButton(
child: Text("Reconnect", style: textStyle), child: Text("Reconnect", style: textStyle),
onPressed: () { onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar(); _reLoad();
_refreshData();
}, },
); );
break; break;
} }
default: { default: {
_bottomBarAction = FlatButton( _bottomBarAction = Container(width: 0.0, height: 0.0,);
child: Text("Reload", style: textStyle),
onPressed: () {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
break; break;
} }
} }
@ -593,22 +598,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
Widget _buildScaffoldBody(bool empty) { Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> popupMenuItems = []; List<PopupMenuItem<String>> popupMenuItems = [];
if (widget.homeAssistant.isAuthenticated) { popupMenuItems.add(PopupMenuItem<String>(
popupMenuItems.addAll([
PopupMenuItem<String>(
child: new Text("Reload"), child: new Text("Reload"),
value: "reload", value: "reload",
), ));
if (widget.homeAssistant.connection.isAuthenticated) {
popupMenuItems.add(
PopupMenuItem<String>( PopupMenuItem<String>(
child: new Text("Logout"), child: new Text("Logout"),
value: "logout", value: "logout",
)]); ));
} else {
popupMenuItems.addAll([
PopupMenuItem<String>(
child: new Text("Connect"),
value: "reload",
)]);
} }
return NestedScrollView( return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
@ -629,14 +628,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
items: popupMenuItems items: popupMenuItems
).then((String val) { ).then((String val) {
if (val == "reload") { if (val == "reload") {
_refreshData(); _reLoad();
} else if (val == "logout") { } else if (val == "logout") {
widget.homeAssistant.disconnect().then((_) {
widget.homeAssistant.logout().then((_) { widget.homeAssistant.logout().then((_) {
setState(() { _reLoad();
_refreshData();
});
});
}); });
} }
}); });
@ -764,7 +759,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_showErrorSubscription?.cancel(); _showErrorSubscription?.cancel();
_startAuthSubscription?.cancel(); _startAuthSubscription?.cancel();
_reloadUISubscription?.cancel(); _reloadUISubscription?.cancel();
widget.homeAssistant?.disconnect(); //TODO disconnect
//widget.homeAssistant?.disconnect();
super.dispose(); super.dispose();
} }
} }

View File

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