Compare commits
17 Commits
0.5.4
...
0.6.0-alph
Author | SHA1 | Date | |
---|---|---|---|
4e96b9adbb | |||
b9581d3762 | |||
7c010359c3 | |||
4a75243994 | |||
d29d7e5b3b | |||
5ebd25e0d1 | |||
b7d5a53e86 | |||
20d3498bfd | |||
67d7bb45f5 | |||
6a03105d01 | |||
5ae580ecf1 | |||
0efef33e53 | |||
ccb88884a7 | |||
d70ba0a55a | |||
5140840d3a | |||
14759fd3c9 | |||
fed35be517 |
@ -70,7 +70,10 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-core:16.0.8'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
42
android/app/google-services.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "441874387819",
|
||||
"firebase_url": "https://ha-client-c73c4.firebaseio.com",
|
||||
"project_id": "ha-client-c73c4",
|
||||
"storage_bucket": "ha-client-c73c4.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45",
|
||||
"android_client_info": {
|
||||
"package_name": "com.keyboardcrumbs.haclient"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
@ -15,7 +15,8 @@
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
@ -30,10 +31,14 @@
|
||||
<meta-data
|
||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||
android:value="true" />
|
||||
<intent-filter>
|
||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 11 KiB |
@ -5,7 +5,8 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.1.2'
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx2g
|
||||
org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=false
|
||||
android.enableJetifier=true
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 24 KiB |
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;
|
||||
}
|
||||
|
||||
}
|
343
lib/connection.class.dart
Normal file
@ -0,0 +1,343 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
_connect().timeout(connectTimeout, onTimeout: () {
|
||||
_disconnect().then((_) {
|
||||
completer.completeError(
|
||||
{"errorCode": 1, "errorMessage": "Connection timeout"});
|
||||
});
|
||||
}).then((_) => completer.complete()).catchError((e) {
|
||||
completer.completeError(e);
|
||||
});
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Completer connecting;
|
||||
|
||||
Future _connect() async {
|
||||
if (connecting != null && !connecting.isCompleted) {
|
||||
Logger.w("");
|
||||
return connecting.future;
|
||||
}
|
||||
connecting = Completer();
|
||||
await _disconnect();
|
||||
Logger.d( "Socket connecting...");
|
||||
_socket = IOWebSocketChannel.connect(
|
||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||
_socketSubscription = _socket.stream.listen(
|
||||
(message) {
|
||||
isConnected = true;
|
||||
var data = json.decode(message);
|
||||
if (data["type"] == "auth_required") {
|
||||
Logger.d("[Received] <== ${data.toString()}");
|
||||
_authenticate().then((_) => connecting.complete()).catchError((e) {
|
||||
if (!connecting.isCompleted) connecting.completeError(e);
|
||||
});
|
||||
} else if (data["type"] == "auth_ok") {
|
||||
Logger.d("[Received] <== ${data.toString()}");
|
||||
_messageResolver["auth"]?.complete();
|
||||
_messageResolver.remove("auth");
|
||||
if (!connecting.isCompleted) connecting.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 (!connecting.isCompleted) connecting.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"});
|
||||
});
|
||||
} else {
|
||||
_handleMessage(data);
|
||||
}
|
||||
},
|
||||
cancelOnError: true,
|
||||
onDone: () => _handleSocketClose(connecting),
|
||||
onError: (e) => _handleSocketError(e, connecting)
|
||||
);
|
||||
return connecting.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 {
|
||||
_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 {
|
||||
_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();
|
||||
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;
|
||||
String rawMessage = json.encode(dataObject);
|
||||
Logger.d("[Sending] ==> $rawMessage");
|
||||
if (!isConnected) {
|
||||
_connect().timeout(connectTimeout, onTimeout: (){
|
||||
_completer.completeError({"errorCode": 8, "errorMessage": "No connection to Home Assistant"});
|
||||
}).then((_) {
|
||||
_socket.sink.add(rawMessage);
|
||||
}).catchError((e) {
|
||||
_completer.completeError(e);
|
||||
});
|
||||
} else {
|
||||
_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;
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class AlarmControlPanelEntity extends Entity {
|
||||
AlarmControlPanelEntity(Map rawData) : super(rawData);
|
||||
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class AutomationEntity extends Entity {
|
||||
AutomationEntity(Map rawData) : super(rawData);
|
||||
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ButtonEntity extends Entity {
|
||||
ButtonEntity(Map rawData) : super(rawData);
|
||||
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
|
@ -4,7 +4,7 @@ class CameraEntity extends Entity {
|
||||
|
||||
static const SUPPORT_ON_OFF = 1;
|
||||
|
||||
CameraEntity(Map rawData) : super(rawData);
|
||||
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportOnOff => ((supportedFeatures &
|
||||
CameraEntity.SUPPORT_ON_OFF) ==
|
||||
|
@ -23,6 +23,8 @@ class ClimateEntity extends Entity {
|
||||
static const SUPPORT_AUX_HEAT = 2048;
|
||||
static const SUPPORT_ON_OFF = 4096;
|
||||
|
||||
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportTargetTemperature => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
||||
@ -88,11 +90,9 @@ class ClimateEntity extends Entity {
|
||||
bool get isOff => state == EntityState.off;
|
||||
bool get auxHeat => attributes['aux_heat'] == "on";
|
||||
|
||||
ClimateEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
void update(Map rawData) {
|
||||
super.update(rawData);
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
if (supportTargetTemperature) {
|
||||
historyConfig.numericAttributesToShow.add("temperature");
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ class CoverEntity extends Entity {
|
||||
static const SUPPORT_STOP_TILT = 64;
|
||||
static const SUPPORT_SET_TILT_POSITION = 128;
|
||||
|
||||
CoverEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportOpen => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_OPEN) ==
|
||||
CoverEntity.SUPPORT_OPEN);
|
||||
@ -45,8 +47,6 @@ class CoverEntity extends Entity {
|
||||
bool get canTiltBeOpened => currentTiltPosition < 100;
|
||||
bool get canTiltBeClosed => currentTiltPosition > 0;
|
||||
|
||||
CoverEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return CoverStateWidget();
|
||||
|
@ -1,6 +1,8 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class DateTimeEntity extends Entity {
|
||||
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get hasDate => attributes["has_date"] ?? false;
|
||||
bool get hasTime => attributes["has_time"] ?? false;
|
||||
int get year => attributes["year"] ?? 1970;
|
||||
@ -12,8 +14,6 @@ class DateTimeEntity extends Entity {
|
||||
String get formattedState => _getFormattedState();
|
||||
DateTime get dateTimeState => _getDateTimeState();
|
||||
|
||||
DateTimeEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return DateTimeStateWidget();
|
||||
|
@ -73,6 +73,7 @@ class Entity {
|
||||
Map attributes;
|
||||
String domain;
|
||||
String entityId;
|
||||
String entityPicture;
|
||||
String state;
|
||||
String displayState;
|
||||
DateTime _lastUpdated;
|
||||
@ -94,7 +95,6 @@ class Entity {
|
||||
bool get isBadge => Entity.badgeDomains.contains(domain);
|
||||
String get icon => attributes["icon"] ?? "";
|
||||
bool get isOn => state == EntityState.on;
|
||||
String get entityPicture => _getEntityPictureUrl();
|
||||
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
||||
List get childEntityIds => attributes["entity_id"] ?? [];
|
||||
String get lastUpdated => _getLastUpdatedFormatted();
|
||||
@ -102,21 +102,21 @@ class Entity {
|
||||
double get doubleState => double.tryParse(state) ?? 0.0;
|
||||
int get supportedFeatures => attributes["supported_features"] ?? 0;
|
||||
|
||||
String _getEntityPictureUrl() {
|
||||
String _getEntityPictureUrl(String webHost) {
|
||||
String result = attributes["entity_picture"];
|
||||
if (result == null) return result;
|
||||
if (!result.startsWith("http")) {
|
||||
if (result.startsWith("/")) {
|
||||
result = "$homeAssistantWebHost$result";
|
||||
result = "$webHost$result";
|
||||
} else {
|
||||
result = "$homeAssistantWebHost/$result";
|
||||
result = "$webHost/$result";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Entity(Map rawData) {
|
||||
update(rawData);
|
||||
Entity(Map rawData, String webHost) {
|
||||
update(rawData, webHost);
|
||||
}
|
||||
|
||||
Entity.missed(String entityId) {
|
||||
@ -148,7 +148,7 @@ class Entity {
|
||||
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||
}
|
||||
|
||||
void update(Map rawData) {
|
||||
void update(Map rawData, String webHost) {
|
||||
attributes = rawData["attributes"] ?? {};
|
||||
domain = rawData["entity_id"].split(".")[0];
|
||||
entityId = rawData["entity_id"];
|
||||
@ -156,6 +156,7 @@ class Entity {
|
||||
state = rawData["state"];
|
||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
|
||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||
entityPicture = _getEntityPictureUrl(webHost);
|
||||
}
|
||||
|
||||
double _getDoubleAttributeValue(String attributeName) {
|
||||
|
@ -6,7 +6,7 @@ class FanEntity extends Entity {
|
||||
static const SUPPORT_OSCILLATE = 2;
|
||||
static const SUPPORT_DIRECTION = 4;
|
||||
|
||||
FanEntity(Map rawData) : super(rawData);
|
||||
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportSetSpeed => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_SET_SPEED) ==
|
||||
|
@ -1,12 +1,13 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class GroupEntity extends Entity {
|
||||
GroupEntity(Map rawData) : super(rawData);
|
||||
|
||||
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
|
||||
String mutualDomain;
|
||||
bool switchable = false;
|
||||
|
||||
GroupEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
if (switchable) {
|
||||
@ -19,8 +20,8 @@ class GroupEntity extends Entity {
|
||||
}
|
||||
|
||||
@override
|
||||
void update(Map rawData) {
|
||||
super.update(rawData);
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
if (_isOneDomain()) {
|
||||
mutualDomain = attributes['entity_id'][0].split(".")[0];
|
||||
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
|
||||
|
@ -42,7 +42,7 @@ class LightEntity extends Entity {
|
||||
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
|
||||
List<String> get effectList => getStringListAttributeValue("effect_list");
|
||||
|
||||
LightEntity(Map rawData) : super(rawData);
|
||||
LightEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
HSVColor _getColor() {
|
||||
List hs = attributes["hs_color"];
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class LockEntity extends Entity {
|
||||
LockEntity(Map rawData) : super(rawData);
|
||||
LockEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get isLocked => state == "locked";
|
||||
|
||||
|
@ -20,7 +20,7 @@ class MediaPlayerEntity extends Entity {
|
||||
static const SUPPORT_SHUFFLE_SET = 32768;
|
||||
static const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||
|
||||
MediaPlayerEntity(Map rawData) : super(rawData);
|
||||
MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportPause => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PAUSE) ==
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SunEntity extends Entity {
|
||||
SunEntity(Map rawData) : super(rawData);
|
||||
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
}
|
||||
|
||||
class SensorEntity extends Entity {
|
||||
@ -12,6 +12,6 @@ class SensorEntity extends Entity {
|
||||
numericState: true
|
||||
);
|
||||
|
||||
SensorEntity(Map rawData) : super(rawData);
|
||||
SensorEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
}
|
@ -5,7 +5,7 @@ class SelectEntity extends Entity {
|
||||
? (attributes["options"] as List).cast<String>()
|
||||
: [];
|
||||
|
||||
SelectEntity(Map rawData) : super(rawData);
|
||||
SelectEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SliderEntity extends Entity {
|
||||
SliderEntity(Map rawData) : super(rawData);
|
||||
SliderEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
double get minValue => _getDoubleAttributeValue("min") ?? 0.0;
|
||||
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SwitchEntity extends Entity {
|
||||
SwitchEntity(Map rawData) : super(rawData);
|
||||
SwitchEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class TextEntity extends Entity {
|
||||
TextEntity(Map rawData) : super(rawData);
|
||||
TextEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
int get valueMinLength => attributes["min"] ?? -1;
|
||||
int get valueMaxLength => attributes["max"] ?? -1;
|
||||
|
@ -1,30 +1,39 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class TimerEntity extends Entity {
|
||||
TimerEntity(Map rawData) : super(rawData);
|
||||
TimerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
Duration duration;
|
||||
|
||||
@override
|
||||
void update(Map rawData) {
|
||||
super.update(rawData);
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
String durationSource = "${attributes["duration"]}";
|
||||
List<String> durationList = durationSource.split(":");
|
||||
if (durationList.length == 1) {
|
||||
duration = Duration(seconds: int.tryParse(durationList[0] ?? 0));
|
||||
} else if (durationList.length == 2) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0
|
||||
);
|
||||
} else if (durationList.length == 3) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0,
|
||||
seconds: int.tryParse(durationList[2]) ?? 0
|
||||
);
|
||||
if (durationSource != null && durationSource.isNotEmpty) {
|
||||
try {
|
||||
List<String> durationList = durationSource.split(":");
|
||||
if (durationList.length == 1) {
|
||||
duration = Duration(seconds: int.tryParse(durationList[0] ?? 0));
|
||||
} else if (durationList.length == 2) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0
|
||||
);
|
||||
} else if (durationList.length == 3) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0,
|
||||
seconds: int.tryParse(durationList[2]) ?? 0
|
||||
);
|
||||
} else {
|
||||
Logger.e("Strange $entityId duration format: $durationSource");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("Error parsing duration for $entityId: ${e.toString()}");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} else {
|
||||
Logger.e("Cann't parse $entityId duration: $durationSource");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,15 @@ part of 'main.dart';
|
||||
|
||||
class EntityCollection {
|
||||
|
||||
final homeAssistantWebHost;
|
||||
|
||||
Map<String, Entity> _allEntities;
|
||||
//Map<String, Entity> views;
|
||||
|
||||
bool get isEmpty => _allEntities.isEmpty;
|
||||
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
||||
|
||||
EntityCollection() {
|
||||
EntityCollection(this.homeAssistantWebHost) {
|
||||
_allEntities = {};
|
||||
//views = {};
|
||||
}
|
||||
@ -33,70 +35,74 @@ class EntityCollection {
|
||||
});
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_allEntities.clear();
|
||||
}
|
||||
|
||||
Entity _createEntityInstance(rawEntityData) {
|
||||
switch (rawEntityData["entity_id"].split(".")[0]) {
|
||||
case 'sun': {
|
||||
return SunEntity(rawEntityData);
|
||||
return SunEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "media_player": {
|
||||
return MediaPlayerEntity(rawEntityData);
|
||||
return MediaPlayerEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case 'sensor': {
|
||||
return SensorEntity(rawEntityData);
|
||||
return SensorEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case 'lock': {
|
||||
return LockEntity(rawEntityData);
|
||||
return LockEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "automation": {
|
||||
return AutomationEntity(rawEntityData);
|
||||
return AutomationEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
|
||||
case "input_boolean":
|
||||
case "switch": {
|
||||
return SwitchEntity(rawEntityData);
|
||||
return SwitchEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "light": {
|
||||
return LightEntity(rawEntityData);
|
||||
return LightEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "group": {
|
||||
return GroupEntity(rawEntityData);
|
||||
return GroupEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "script":
|
||||
case "scene": {
|
||||
return ButtonEntity(rawEntityData);
|
||||
return ButtonEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_datetime": {
|
||||
return DateTimeEntity(rawEntityData);
|
||||
return DateTimeEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_select": {
|
||||
return SelectEntity(rawEntityData);
|
||||
return SelectEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_number": {
|
||||
return SliderEntity(rawEntityData);
|
||||
return SliderEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_text": {
|
||||
return TextEntity(rawEntityData);
|
||||
return TextEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "climate": {
|
||||
return ClimateEntity(rawEntityData);
|
||||
return ClimateEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "cover": {
|
||||
return CoverEntity(rawEntityData);
|
||||
return CoverEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "fan": {
|
||||
return FanEntity(rawEntityData);
|
||||
return FanEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "camera": {
|
||||
return CameraEntity(rawEntityData);
|
||||
return CameraEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "alarm_control_panel": {
|
||||
return AlarmControlPanelEntity(rawEntityData);
|
||||
return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "timer": {
|
||||
return TimerEntity(rawEntityData);
|
||||
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
default: {
|
||||
return Entity(rawEntityData);
|
||||
return Entity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -121,7 +127,7 @@ class EntityCollection {
|
||||
}
|
||||
|
||||
void updateFromRaw(Map rawEntityData) {
|
||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
|
||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
|
||||
Entity get(String entityId) {
|
||||
|
@ -16,6 +16,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
}
|
||||
|
||||
CameraEntity _entity;
|
||||
String _webHost;
|
||||
|
||||
http.Client client;
|
||||
http.StreamedResponse response;
|
||||
@ -28,7 +29,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
void _connect() async {
|
||||
started = true;
|
||||
timeToStop = false;
|
||||
String streamUrl = '$homeAssistantWebHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}';
|
||||
String streamUrl = '$_webHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}';
|
||||
client = new http.Client(); // create a client to make api calls
|
||||
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
|
||||
Logger.d("[Sending] ==> $streamUrl");
|
||||
@ -130,6 +131,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
.of(context)
|
||||
.entityWrapper
|
||||
.entity;
|
||||
_webHost = HomeAssistantModel.of(context).homeAssistant.connection.httpWebHost;
|
||||
_connect();
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildImage(MediaPlayerEntity entity) {
|
||||
String state = entity.state;
|
||||
if (homeAssistantWebHost != null && entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Row(
|
||||
|
@ -47,7 +47,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
}
|
||||
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
||||
_historyLastUpdated = now;
|
||||
ha.getHistory(entityId).then((history){
|
||||
ha.connection.getHistory(entityId).then((history){
|
||||
if (!_disposed) {
|
||||
setState(() {
|
||||
_history = history.isNotEmpty ? history[0] : [];
|
||||
|
@ -1,37 +1,27 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class HomeAssistant {
|
||||
String _webSocketAPIEndpoint;
|
||||
String _password;
|
||||
|
||||
final Connection connection = Connection();
|
||||
|
||||
bool _useLovelace = false;
|
||||
//bool isSettingsLoaded = false;
|
||||
|
||||
|
||||
|
||||
IOWebSocketChannel _hassioChannel;
|
||||
SendMessageQueue _messageQueue;
|
||||
|
||||
int _currentMessageId = 0;
|
||||
int _subscriptionMessageId = 0;
|
||||
Map<int, Completer> _messageResolver = {};
|
||||
EntityCollection entities;
|
||||
HomeAssistantUI ui;
|
||||
Map _instanceConfig = {};
|
||||
String _userName;
|
||||
String hostname;
|
||||
HSVColor savedColor;
|
||||
|
||||
Map _rawLovelaceData;
|
||||
|
||||
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 connectTimeout = Duration(seconds: 15);
|
||||
|
||||
String get locationName {
|
||||
if (_useLovelace) {
|
||||
@ -42,117 +32,39 @@ class HomeAssistant {
|
||||
}
|
||||
String get userName => _userName ?? locationName;
|
||||
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||
bool get isNoEntities => entities == null || entities.isEmpty;
|
||||
bool get isNoViews => ui == null || ui.isEmpty;
|
||||
//int get viewsCount => entities.views.length ?? 0;
|
||||
|
||||
HomeAssistant() {
|
||||
entities = EntityCollection();
|
||||
_messageQueue = SendMessageQueue(messageExpirationTime);
|
||||
HomeAssistant();
|
||||
|
||||
Completer _connectCompleter;
|
||||
|
||||
Future init() {
|
||||
if (_connectCompleter != null && !_connectCompleter.isCompleted) {
|
||||
Logger.w("Previous connection pending...");
|
||||
return _connectCompleter.future;
|
||||
}
|
||||
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;
|
||||
_connectCompleter.complete();
|
||||
}).catchError((e) => _connectCompleter.completeError(e));
|
||||
}).catchError((e) => _connectCompleter.completeError(e));
|
||||
return _connectCompleter.future;
|
||||
}
|
||||
|
||||
void updateSettings(String url, String password, bool useLovelace) {
|
||||
_webSocketAPIEndpoint = url;
|
||||
_password = password;
|
||||
_useLovelace = useLovelace;
|
||||
Logger.d( "Use lovelace is $_useLovelace");
|
||||
}
|
||||
Completer _fetchCompleter;
|
||||
|
||||
Future fetch() {
|
||||
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
|
||||
Logger.w("Previous fetch is not complited");
|
||||
} else {
|
||||
_fetchCompleter = new Completer();
|
||||
_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);
|
||||
});
|
||||
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||
Logger.w("Previous data fetch is not completed yet");
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
|
||||
disconnect() async {
|
||||
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
|
||||
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
|
||||
onTimeout: () => Logger.d( "Socket sink closed")
|
||||
);
|
||||
await _socketSubscription.cancel();
|
||||
_hassioChannel = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Future _connection() {
|
||||
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
|
||||
Logger.d("Previous connection is not complited");
|
||||
} else {
|
||||
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
|
||||
_connectionCompleter = new Completer();
|
||||
autoReconnect = false;
|
||||
disconnect().then((_){
|
||||
Logger.d( "Socket connecting...");
|
||||
_connectionTimer = Timer(connectTimeout, () {
|
||||
Logger.e( "Socket connection timeout");
|
||||
_handleSocketError(null);
|
||||
});
|
||||
if (_socketSubscription != 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 {
|
||||
_fetchCompleter = Completer();
|
||||
List<Future> futures = [];
|
||||
futures.add(_getStates());
|
||||
if (_useLovelace) {
|
||||
@ -162,194 +74,76 @@ class HomeAssistant {
|
||||
futures.add(_getServices());
|
||||
futures.add(_getUserInfo());
|
||||
futures.add(_getPanels());
|
||||
try {
|
||||
await Future.wait(futures);
|
||||
Future.wait(futures).then((_) {
|
||||
_createUI();
|
||||
_completeFetching(null);
|
||||
} catch (error) {
|
||||
_completeFetching(error);
|
||||
}
|
||||
_fetchCompleter.complete();
|
||||
}).catchError((e) {
|
||||
_fetchCompleter.completeError(e);
|
||||
});
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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") {
|
||||
_sendAuthMessage('{"type": "auth","access_token": "$_password"}');
|
||||
} else if (data["type"] == "auth_ok") {
|
||||
_completeConnecting(null);
|
||||
_sendSubscribe();
|
||||
} else if (data["type"] == "auth_invalid") {
|
||||
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
|
||||
} else if (data["type"] == "result") {
|
||||
Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
|
||||
_messageResolver[data["id"]]?.complete(data);
|
||||
_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.w("Unknown message type: $message");
|
||||
}
|
||||
}
|
||||
|
||||
void _sendSubscribe() {
|
||||
_incrementMessageId();
|
||||
_subscriptionMessageId = _currentMessageId;
|
||||
_send('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
|
||||
Future logout() async {
|
||||
Logger.d("Logging out...");
|
||||
await connection.logout().then((_) {
|
||||
ui?.clear();
|
||||
entities?.clear();
|
||||
});
|
||||
}
|
||||
|
||||
Future _getConfig() async {
|
||||
await _sendInitialMessage("get_config").then((data) => _instanceConfig = Map.from(data["result"]));
|
||||
await connection.sendSocketMessage(type: "get_config").then((data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
}).catchError((e) {
|
||||
throw {"errorCode": 1, "errorMessage": "Error getting config: $e"};
|
||||
});
|
||||
}
|
||||
|
||||
Future _getStates() async {
|
||||
await _sendInitialMessage("get_states").then((data) => entities.parse(data["result"]));
|
||||
await connection.sendSocketMessage(type: "get_states").then(
|
||||
(data) => entities.parse(data)
|
||||
).catchError((e) {
|
||||
throw {"errorCode": 1, "errorMessage": "Error getting states: $e"};
|
||||
});
|
||||
}
|
||||
|
||||
Future _getLovelace() async {
|
||||
await _sendInitialMessage("lovelace/config").then((data) => _rawLovelaceData = data["result"]);
|
||||
await connection.sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
||||
throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"};
|
||||
});
|
||||
}
|
||||
|
||||
Future _getUserInfo() async {
|
||||
_userName = null;
|
||||
await _sendInitialMessage("auth/current_user").then((data) => _userName = data["result"]["name"]);
|
||||
await connection.sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
||||
Logger.w("Can't get user info: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getServices() async {
|
||||
await _sendInitialMessage("get_services").then((data) => Logger.d("We actually don`t need the list of servcies for now"));
|
||||
await connection.sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
||||
Logger.w("Can't get services: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getPanels() async {
|
||||
panels.clear();
|
||||
await _sendInitialMessage("get_panels").then((data) {
|
||||
if (data["success"]) {
|
||||
data["result"].forEach((k,v) {
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
panels.add(Panel(
|
||||
id: k,
|
||||
type: v["component_name"],
|
||||
title: title,
|
||||
urlPath: v["url_path"],
|
||||
config: v["config"],
|
||||
icon: v["icon"]
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_incrementMessageId() {
|
||||
_currentMessageId += 1;
|
||||
}
|
||||
|
||||
void _sendAuthMessage(String message) {
|
||||
Logger.d( "[Sending] ==> auth request");
|
||||
_hassioChannel.sink.add(message);
|
||||
}
|
||||
|
||||
Future _sendInitialMessage(String type) {
|
||||
Completer _completer = Completer();
|
||||
_incrementMessageId();
|
||||
_messageResolver[_currentMessageId] = _completer;
|
||||
_send('{"id": $_currentMessageId, "type": "$type"}', false);
|
||||
return _completer.future;
|
||||
}
|
||||
|
||||
_send(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);
|
||||
await connection.sendSocketMessage(type: "get_panels").then((data) {
|
||||
data.forEach((k,v) {
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
panels.add(Panel(
|
||||
id: k,
|
||||
type: v["component_name"],
|
||||
title: title,
|
||||
urlPath: v["url_path"],
|
||||
config: v["config"],
|
||||
icon: v["icon"]
|
||||
)
|
||||
);
|
||||
});
|
||||
if (!queued) {
|
||||
Logger.d( "[Sending] ==> $message");
|
||||
_hassioChannel.sink.add(message);
|
||||
}
|
||||
sendCompleter.complete();
|
||||
}).catchError((e){
|
||||
sendCompleter.completeError(e);
|
||||
}).catchError((e) {
|
||||
throw {"errorCode": 1, "errorMessage": "Error getting panels list: $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 _send(message, true);
|
||||
}
|
||||
|
||||
void _handleEntityStateChange(Map eventData) {
|
||||
@ -555,31 +349,12 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildViews(BuildContext context, bool lovelace, TabController tabController) {
|
||||
Widget buildViews(BuildContext context, TabController 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 = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||
Logger.d("[Sending] ==> $url");
|
||||
http.Response historyResponse;
|
||||
historyResponse = await http.get(url, headers: {
|
||||
"authorization": "Bearer $_password",
|
||||
"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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class SendMessageQueue {
|
||||
int _messageTimeout;
|
||||
List<HAMessage> _queue = [];
|
||||
@ -618,4 +393,4 @@ class HAMessage {
|
||||
bool isExpired() {
|
||||
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
347
lib/main.dart
@ -17,6 +17,9 @@ import 'package:progress_indicators/progress_indicators.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
part 'entity_class/const.dart';
|
||||
part 'entity_class/entity.class.dart';
|
||||
@ -87,6 +90,8 @@ part 'entity.page.dart';
|
||||
part 'utils.class.dart';
|
||||
part 'mdi.class.dart';
|
||||
part 'entity_collection.class.dart';
|
||||
part 'auth_manager.class.dart';
|
||||
part 'connection.class.dart';
|
||||
part 'ui_class/ui.dart';
|
||||
part 'ui_class/view.class.dart';
|
||||
part 'ui_class/card.class.dart';
|
||||
@ -100,9 +105,7 @@ part 'ui_widgets/config_panel_widget.dart';
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
const String appName = "HA Client";
|
||||
const appVersion = "0.5.0";
|
||||
|
||||
String homeAssistantWebHost;
|
||||
const appVersion = "0.6.0-alpha1";
|
||||
|
||||
void main() {
|
||||
FlutterError.onError = (errorDetails) {
|
||||
@ -124,6 +127,8 @@ void main() {
|
||||
}
|
||||
|
||||
class HAClientApp extends StatelessWidget {
|
||||
|
||||
final HomeAssistant homeAssistant = HomeAssistant();
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -134,7 +139,7 @@ class HAClientApp extends StatelessWidget {
|
||||
),
|
||||
initialRoute: "/",
|
||||
routes: {
|
||||
"/": (context) => MainPage(title: 'HA Client'),
|
||||
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
|
||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||
"/configuration": (context) => PanelPage(title: "Configuration"),
|
||||
"/log-view": (context) => LogViewPage(title: "Log")
|
||||
@ -144,82 +149,84 @@ class HAClientApp extends StatelessWidget {
|
||||
}
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
MainPage({Key key, this.title}) : super(key: key);
|
||||
MainPage({Key key, this.title, this.homeAssistant}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final HomeAssistant homeAssistant;
|
||||
|
||||
@override
|
||||
_MainPageState createState() => new _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||
HomeAssistant _homeAssistant;
|
||||
//Map _instanceConfig;
|
||||
String _webSocketApiEndpoint;
|
||||
String _password;
|
||||
//int _uiViewsCount = 0;
|
||||
String _instanceHost;
|
||||
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _settingsSubscription;
|
||||
StreamSubscription _serviceCallSubscription;
|
||||
StreamSubscription _showEntityPageSubscription;
|
||||
StreamSubscription _showErrorSubscription;
|
||||
bool _settingsLoaded = false;
|
||||
bool _accountMenuExpanded = false;
|
||||
bool _useLovelaceUI;
|
||||
StreamSubscription _startAuthSubscription;
|
||||
StreamSubscription _reloadUISubscription;
|
||||
int _previousViewCount;
|
||||
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_settingsLoaded = false;
|
||||
//widget.homeAssistant = HomeAssistant();
|
||||
//_settingsLoaded = false;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
Logger.d("<!!!> Creating new HomeAssistant instance");
|
||||
_homeAssistant = HomeAssistant();
|
||||
|
||||
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
||||
Logger.d("Settings change event: reconnect=${event.reconnect}");
|
||||
if (event.reconnect) {
|
||||
_homeAssistant.disconnect().then((_){
|
||||
_initialLoad();
|
||||
});
|
||||
_reLoad();
|
||||
}
|
||||
});
|
||||
|
||||
_initialLoad();
|
||||
}
|
||||
|
||||
void _initialLoad() {
|
||||
_loadConnectionSettings().then((_){
|
||||
_subscribe();
|
||||
_refreshData();
|
||||
}, onError: (_) {
|
||||
_showErrorBottomBar(message: _, errorCode: 5);
|
||||
_showInfoBottomBar(progress: true,);
|
||||
_subscribe();
|
||||
widget.homeAssistant.init().then((_){
|
||||
_fetchData();
|
||||
}, 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
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
Logger.d("$state");
|
||||
if (state == AppLifecycleState.resumed && _settingsLoaded) {
|
||||
_refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
_loadConnectionSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String domain = prefs.getString('hassio-domain');
|
||||
String port = prefs.getString('hassio-port');
|
||||
_instanceHost = "$domain:$port";
|
||||
_webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
|
||||
homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
|
||||
_password = prefs.getString('hassio-password');
|
||||
_useLovelaceUI = prefs.getBool('use-lovelace') ?? true;
|
||||
if ((domain == null) || (port == null) || (_password == null) ||
|
||||
(domain.length == 0) || (port.length == 0) || (_password.length == 0)) {
|
||||
throw("Check connection settings");
|
||||
} else {
|
||||
_settingsLoaded = true;
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_reLoad();
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,12 +235,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.needToRebuildUI) {
|
||||
Logger.d("New entity. Need to rebuild UI");
|
||||
_refreshData();
|
||||
_reLoad();
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (_reloadUISubscription == null) {
|
||||
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
||||
_reLoad();
|
||||
});
|
||||
}
|
||||
if (_serviceCallSubscription == null) {
|
||||
_serviceCallSubscription =
|
||||
eventBus.on<ServiceCallEvent>().listen((event) {
|
||||
@ -254,24 +266,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
|
||||
});
|
||||
}
|
||||
|
||||
if (_startAuthSubscription == null) {
|
||||
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
|
||||
_showOAuth();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*_firebaseMessaging.getToken().then((String token) {
|
||||
//Logger.d("FCM token: $token");
|
||||
widget.homeAssistant.sendHTTPPost(
|
||||
endPoint: '/api/notify.fcm-android',
|
||||
jsonData: '{"token": "$token"}'
|
||||
);
|
||||
});
|
||||
_firebaseMessaging.configure(
|
||||
onLaunch: (data) {
|
||||
Logger.d("Notification [onLaunch]: $data");
|
||||
},
|
||||
onMessage: (data) {
|
||||
Logger.d("Notification [onMessage]: $data");
|
||||
},
|
||||
onResume: (data) {
|
||||
Logger.d("Notification [onResume]: $data");
|
||||
}
|
||||
);*/
|
||||
}
|
||||
|
||||
_refreshData() async {
|
||||
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
|
||||
_hideBottomBar();
|
||||
_showInfoBottomBar(progress: true,);
|
||||
await _homeAssistant.fetch().then((result) {
|
||||
_hideBottomBar();
|
||||
int currentViewCount = _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());
|
||||
void _showOAuth() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebviewScaffold(
|
||||
url: "${widget.homeAssistant.connection.oauthUrl}",
|
||||
appBar: new AppBar(
|
||||
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"),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_setErrorState(e) {
|
||||
@ -279,7 +318,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
Logger.e(e.toString());
|
||||
Logger.e("${e.stackTrace}");
|
||||
_showErrorBottomBar(
|
||||
message: "There was some error",
|
||||
message: "Unknown error",
|
||||
errorCode: 13
|
||||
);
|
||||
} else {
|
||||
@ -290,19 +329,19 @@ 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(
|
||||
message: "Calling $domain.$service",
|
||||
duration: Duration(seconds: 3)
|
||||
);
|
||||
_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) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant),
|
||||
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant),
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -310,8 +349,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
List<Tab> buildUIViewTabs() {
|
||||
List<Tab> result = [];
|
||||
|
||||
if (_homeAssistant.ui.views.isNotEmpty) {
|
||||
_homeAssistant.ui.views.forEach((HAView view) {
|
||||
if (widget.homeAssistant.ui.views.isNotEmpty) {
|
||||
widget.homeAssistant.ui.views.forEach((HAView view) {
|
||||
result.add(view.buildTab());
|
||||
});
|
||||
}
|
||||
@ -323,16 +362,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
List<Widget> menuItems = [];
|
||||
menuItems.add(
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: Text(_homeAssistant.userName),
|
||||
accountEmail: Text(_instanceHost ?? "Not configured"),
|
||||
onDetailsPressed: () {
|
||||
accountName: Text(widget.homeAssistant.userName),
|
||||
accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"),
|
||||
/*onDetailsPressed: () {
|
||||
setState(() {
|
||||
_accountMenuExpanded = !_accountMenuExpanded;
|
||||
});
|
||||
},
|
||||
},*/
|
||||
currentAccountPicture: CircleAvatar(
|
||||
child: Text(
|
||||
_homeAssistant.userAvatarText,
|
||||
widget.homeAssistant.userAvatarText,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0
|
||||
),
|
||||
@ -340,21 +379,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
),
|
||||
)
|
||||
);
|
||||
if (_accountMenuExpanded) {
|
||||
menuItems.addAll([
|
||||
ListTile(
|
||||
leading: Icon(Icons.settings),
|
||||
title: Text("Settings"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/connection-settings');
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
]);
|
||||
} else {
|
||||
if (_homeAssistant != null && _homeAssistant.panels.isNotEmpty) {
|
||||
_homeAssistant.panels.forEach((Panel panel) {
|
||||
if (widget.homeAssistant.panels.isNotEmpty) {
|
||||
widget.homeAssistant.panels.forEach((Panel panel) {
|
||||
if (!panel.isHidden) {
|
||||
menuItems.add(
|
||||
new ListTile(
|
||||
@ -365,16 +391,28 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
);
|
||||
}
|
||||
});
|
||||
menuItems.addAll([
|
||||
}
|
||||
//TODO check for loaded
|
||||
menuItems.add(
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
|
||||
title: Text("Open Web UI"),
|
||||
onTap: () => HAUtils.launchURL(homeAssistantWebHost),
|
||||
),
|
||||
Divider()
|
||||
]);
|
||||
}
|
||||
onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost),
|
||||
)
|
||||
);
|
||||
menuItems.addAll([
|
||||
Divider(),
|
||||
ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
|
||||
title: Text("Connection settings"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
|
||||
},
|
||||
)
|
||||
]);
|
||||
menuItems.addAll([
|
||||
Divider(),
|
||||
new ListTile(
|
||||
leading: Icon(Icons.insert_drive_file),
|
||||
title: Text("Log"),
|
||||
@ -392,6 +430,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
||||
title: Text("Join Discord server"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
||||
},
|
||||
),
|
||||
new AboutListTile(
|
||||
aboutBoxChildren: <Widget>[
|
||||
GestureDetector(
|
||||
@ -412,7 +458,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
applicationVersion: appVersion
|
||||
)
|
||||
]);
|
||||
}
|
||||
return new Drawer(
|
||||
child: ListView(
|
||||
children: menuItems,
|
||||
@ -465,7 +510,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
child: Text("Retry", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
_reLoad();
|
||||
},
|
||||
);
|
||||
break;
|
||||
@ -483,12 +528,32 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
break;
|
||||
}
|
||||
|
||||
case 6: {
|
||||
case 60: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Settings", style: textStyle),
|
||||
child: Text("Login", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
Navigator.pushNamed(context, '/connection-settings');
|
||||
_reLoad();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 63:
|
||||
case 61: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Try again", style: textStyle),
|
||||
onPressed: () {
|
||||
_reLoad();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 62: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Login again", style: textStyle),
|
||||
onPressed: () {
|
||||
_reLoad();
|
||||
},
|
||||
);
|
||||
break;
|
||||
@ -499,52 +564,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
child: Text("Refresh", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
_reLoad();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 82:
|
||||
case 81:
|
||||
case 8: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Reconnect", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
_reLoad();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Reload", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
},
|
||||
);
|
||||
_bottomBarAction = Container(width: 0.0, height: 0.0,);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_bottomBarProgress = false;
|
||||
_bottomBarText = "$message (code: $errorCode)";
|
||||
_bottomBarText = "$message";
|
||||
_showBottomBar = true;
|
||||
});
|
||||
/*_scaffoldKey.currentState.hideCurrentSnackBar();
|
||||
_scaffoldKey.currentState.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("$message (code: $errorCode)"),
|
||||
action: action,
|
||||
duration: Duration(hours: 1),
|
||||
)
|
||||
);*/
|
||||
}
|
||||
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
|
||||
Widget _buildScaffoldBody(bool empty) {
|
||||
List<PopupMenuItem<String>> popupMenuItems = [];
|
||||
popupMenuItems.add(PopupMenuItem<String>(
|
||||
child: new Text("Reload"),
|
||||
value: "reload",
|
||||
));
|
||||
if (widget.homeAssistant.connection.isAuthenticated) {
|
||||
popupMenuItems.add(
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Logout"),
|
||||
value: "logout",
|
||||
));
|
||||
}
|
||||
return NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
@ -552,7 +616,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
floating: true,
|
||||
pinned: true,
|
||||
primary: true,
|
||||
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
|
||||
title: Text(widget.homeAssistant.locationName ?? ""),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
@ -561,13 +625,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
||||
context: context,
|
||||
items: [PopupMenuItem<String>(
|
||||
child: new Text("Reload"),
|
||||
value: "reload",
|
||||
)]
|
||||
items: popupMenuItems
|
||||
).then((String val) {
|
||||
if (val == "reload") {
|
||||
_refreshData();
|
||||
_reLoad();
|
||||
} else if (val == "logout") {
|
||||
widget.homeAssistant.logout().then((_) {
|
||||
_reLoad();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -577,9 +642,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
icon: Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
setState(() {
|
||||
_accountMenuExpanded = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
bottom: empty ? null : TabBar(
|
||||
@ -597,15 +659,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant"),
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"),
|
||||
size: 100.0,
|
||||
color: Colors.blue,
|
||||
color: Colors.black26,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
:
|
||||
_homeAssistant.buildViews(context, _useLovelaceUI, _viewsTabController),
|
||||
widget.homeAssistant.buildViews(context, _viewsTabController),
|
||||
);
|
||||
}
|
||||
|
||||
@ -662,7 +724,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
}
|
||||
}
|
||||
// This method is rerun every time setState is called.
|
||||
if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) {
|
||||
if (widget.homeAssistant.isNoViews) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
primary: false,
|
||||
@ -678,7 +740,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: HomeAssistantModel(
|
||||
child: _buildScaffoldBody(false),
|
||||
homeAssistant: _homeAssistant
|
||||
homeAssistant: widget.homeAssistant
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -686,14 +748,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
flutterWebviewPlugin.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_viewsTabController.dispose();
|
||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||
if (_settingsSubscription != null) _settingsSubscription.cancel();
|
||||
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
|
||||
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
|
||||
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
|
||||
_homeAssistant.disconnect();
|
||||
_viewsTabController?.dispose();
|
||||
_stateSubscription?.cancel();
|
||||
_settingsSubscription?.cancel();
|
||||
_serviceCallSubscription?.cancel();
|
||||
_showEntityPageSubscription?.cancel();
|
||||
_showErrorSubscription?.cancel();
|
||||
_startAuthSubscription?.cancel();
|
||||
_reloadUISubscription?.cancel();
|
||||
//TODO disconnect
|
||||
//widget.homeAssistant?.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -14,17 +14,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
String _newHassioDomain = "";
|
||||
String _hassioPort = "";
|
||||
String _newHassioPort = "";
|
||||
String _hassioPassword = "";
|
||||
String _newHassioPassword = "";
|
||||
String _socketProtocol = "wss";
|
||||
String _newSocketProtocol = "wss";
|
||||
bool _useLovelace = true;
|
||||
bool _newUseLovelace = true;
|
||||
|
||||
String oauthUrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
|
||||
}
|
||||
|
||||
_loadSettings() async {
|
||||
@ -33,7 +34,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
setState(() {
|
||||
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
||||
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
||||
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
|
||||
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
||||
try {
|
||||
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
|
||||
@ -44,7 +44,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
}
|
||||
|
||||
bool _checkConfigChanged() {
|
||||
return ((_newHassioPassword != _hassioPassword) ||
|
||||
return (
|
||||
(_newHassioPort != _hassioPort) ||
|
||||
(_newHassioDomain != _hassioDomain) ||
|
||||
(_newSocketProtocol != _socketProtocol) ||
|
||||
@ -59,7 +59,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString("hassio-domain", _newHassioDomain);
|
||||
prefs.setString("hassio-port", _newHassioPort);
|
||||
prefs.setString("hassio-password", _newHassioPassword);
|
||||
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||
prefs.setBool("use-lovelace", _newUseLovelace);
|
||||
@ -149,21 +148,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
"Try ports 80 and 443 if default is not working and you don't know why.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
new TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Access token"
|
||||
),
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioPassword,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioPassword.length)
|
||||
)
|
||||
),
|
||||
onChanged: (value) {
|
||||
_newHassioPassword = value;
|
||||
}
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20.0),
|
||||
child: Text(
|
||||
|
@ -36,7 +36,8 @@ class Panel {
|
||||
)
|
||||
);
|
||||
} else {
|
||||
String url = "$homeAssistantWebHost/$urlPath";
|
||||
HomeAssistantModel haModel = HomeAssistantModel.of(context);
|
||||
String url = "${haModel.homeAssistant.connection.httpWebHost}/$urlPath";
|
||||
Logger.d("Launching custom tab with $url");
|
||||
HAUtils.launchURLInCustomTab(context, url);
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ class HomeAssistantUI {
|
||||
List<HAView> views;
|
||||
String title;
|
||||
|
||||
bool get isEmpty => views == null || views.isEmpty;
|
||||
|
||||
HomeAssistantUI() {
|
||||
views = [];
|
||||
}
|
||||
@ -25,4 +27,8 @@ class HomeAssistantUI {
|
||||
return result;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
views.clear();
|
||||
}
|
||||
|
||||
}
|
@ -109,6 +109,16 @@ class RefreshDataFinishedEvent {
|
||||
RefreshDataFinishedEvent();
|
||||
}
|
||||
|
||||
class ReloadUIEvent {
|
||||
ReloadUIEvent();
|
||||
}
|
||||
|
||||
class StartAuthEvent {
|
||||
String oauthUrl;
|
||||
|
||||
StartAuthEvent(this.oauthUrl);
|
||||
}
|
||||
|
||||
class ServiceCallEvent {
|
||||
String domain;
|
||||
String service;
|
||||
|
30
pubspec.lock
@ -101,6 +101,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0+1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -134,6 +141,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -146,6 +160,13 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_webview_plugin:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webview_plugin
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -244,6 +265,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
progress_indicators:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -264,7 +292,7 @@ packages:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.1+1"
|
||||
version: "0.5.1+2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 0.5.0+97
|
||||
version: 0.6.0+101
|
||||
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||
@ -20,6 +20,9 @@ dependencies:
|
||||
flutter_markdown: any
|
||||
flutter_svg: ^0.10.3
|
||||
flutter_custom_tabs: ^0.6.0
|
||||
firebase_messaging: ^4.0.0+1
|
||||
flutter_webview_plugin: ^0.3.1
|
||||
flutter_secure_storage: ^3.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|