Resolves #340 Connection refactoring
This commit is contained in:
parent
4a75243994
commit
7c010359c3
44
lib/auth_manager.class.dart
Normal file
44
lib/auth_manager.class.dart
Normal 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
330
lib/connection.class.dart
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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] : [];
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
136
lib/main.dart
136
lib/main.dart
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user