Compare commits
65 Commits
0.4.4
...
0.6.0-alph
Author | SHA1 | Date | |
---|---|---|---|
885a516676 | |||
921b0e09b0 | |||
277c67fc6f | |||
2a01ff8a03 | |||
b246b7bc1d | |||
e1868b9a14 | |||
125f3ac16c | |||
be502b5668 | |||
6f33fdca9f | |||
4e96b9adbb | |||
b9581d3762 | |||
7c010359c3 | |||
4a75243994 | |||
d29d7e5b3b | |||
5ebd25e0d1 | |||
b7d5a53e86 | |||
20d3498bfd | |||
67d7bb45f5 | |||
6a03105d01 | |||
5ae580ecf1 | |||
0efef33e53 | |||
ccb88884a7 | |||
d70ba0a55a | |||
5140840d3a | |||
14759fd3c9 | |||
fed35be517 | |||
db77cc43aa | |||
b2269cc96d | |||
8b28bb2e9e | |||
fb456878bc | |||
8b961ebd69 | |||
9bd3a41cf5 | |||
491ae55a2a | |||
e1d2981782 | |||
74572168ae | |||
92d0b5c055 | |||
3504d3276c | |||
736b38b64c | |||
cb118b599a | |||
a08a056cff | |||
0ef2ebfe31 | |||
4f4ac3b574 | |||
7064cb0e30 | |||
91a99e17e0 | |||
2e9b7d20b9 | |||
b8aa808de4 | |||
2cfa92a42b | |||
146efef72d | |||
8c9804e16f | |||
a4736bfb5a | |||
15c54df629 | |||
32ffef21e9 | |||
848d3cb510 | |||
8a4caeebba | |||
aa923f0fba | |||
4d8f50ddd5 | |||
fe06b21a6c | |||
efed7fb1b5 | |||
df2cbb7d13 | |||
03edaa9ca2 | |||
1a7457abf9 | |||
00889b13e0 | |||
0615073ec4 | |||
eb7d17d147 | |||
24f80feeee |
@ -1,13 +1,12 @@
|
|||||||
|
[](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart)
|
||||||
# HA Client
|
# HA Client
|
||||||
## Native Android client for Home Assistant
|
## Native Android client for Home Assistant
|
||||||
### With Lovelace UI support
|
### With Lovelace UI support
|
||||||
|
|
||||||
Home Assistant Android client on Dart with Flutter.
|
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
||||||
|
|
||||||
Visit [www.vynn.co](https://www.vynn.co/ha-client) for more info.
|
|
||||||
|
|
||||||
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
|
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
|
||||||
|
|
||||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
|
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
|
||||||
|
|
||||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or in [Discord](https://discord.gg/NSaQEQ8)
|
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)
|
||||||
|
@ -29,7 +29,7 @@ def keystoreProperties = new Properties()
|
|||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 28
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -43,7 +43,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.keyboardcrumbs.haclient"
|
applicationId "com.keyboardcrumbs.haclient"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 27
|
targetSdkVersion 28
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
@ -70,7 +70,10 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'com.google.firebase:firebase-core:16.0.8'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.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
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name="io.flutter.app.FlutterApplication"
|
||||||
android:label="HA Client"
|
android:label="HA Client"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
@ -30,6 +31,10 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
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 {
|
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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1,5 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx2g
|
||||||
|
org.gradle.daemon=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
||||||
|
0
android/gradlew
vendored
Normal file → Executable file
BIN
fonts/materialdesignicons-webfont-3-5-95.ttf
Normal file
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';
|
part of '../main.dart';
|
||||||
|
|
||||||
class AlarmControlPanelEntity extends Entity {
|
class AlarmControlPanelEntity extends Entity {
|
||||||
AlarmControlPanelEntity(Map rawData) : super(rawData);
|
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class AutomationEntity extends Entity {
|
class AutomationEntity extends Entity {
|
||||||
AutomationEntity(Map rawData) : super(rawData);
|
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class ButtonEntity extends Entity {
|
class ButtonEntity extends Entity {
|
||||||
ButtonEntity(Map rawData) : super(rawData);
|
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
@ -9,7 +10,7 @@ class ButtonEntity extends Entity {
|
|||||||
entityId: entityId,
|
entityId: entityId,
|
||||||
serviceDomain: domain,
|
serviceDomain: domain,
|
||||||
serviceName: 'turn_on',
|
serviceName: 'turn_on',
|
||||||
text: "EXECUTE",
|
text: domain == "scene" ? "ACTIVATE" : "EXECUTE",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,7 @@ class CameraEntity extends Entity {
|
|||||||
|
|
||||||
static const SUPPORT_ON_OFF = 1;
|
static const SUPPORT_ON_OFF = 1;
|
||||||
|
|
||||||
CameraEntity(Map rawData) : super(rawData);
|
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get supportOnOff => ((supportedFeatures &
|
bool get supportOnOff => ((supportedFeatures &
|
||||||
CameraEntity.SUPPORT_ON_OFF) ==
|
CameraEntity.SUPPORT_ON_OFF) ==
|
||||||
|
@ -23,6 +23,8 @@ class ClimateEntity extends Entity {
|
|||||||
static const SUPPORT_AUX_HEAT = 2048;
|
static const SUPPORT_AUX_HEAT = 2048;
|
||||||
static const SUPPORT_ON_OFF = 4096;
|
static const SUPPORT_ON_OFF = 4096;
|
||||||
|
|
||||||
|
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get supportTargetTemperature => ((supportedFeatures &
|
bool get supportTargetTemperature => ((supportedFeatures &
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
||||||
@ -88,11 +90,9 @@ class ClimateEntity extends Entity {
|
|||||||
bool get isOff => state == EntityState.off;
|
bool get isOff => state == EntityState.off;
|
||||||
bool get auxHeat => attributes['aux_heat'] == "on";
|
bool get auxHeat => attributes['aux_heat'] == "on";
|
||||||
|
|
||||||
ClimateEntity(Map rawData) : super(rawData);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void update(Map rawData) {
|
void update(Map rawData, String webHost) {
|
||||||
super.update(rawData);
|
super.update(rawData, webHost);
|
||||||
if (supportTargetTemperature) {
|
if (supportTargetTemperature) {
|
||||||
historyConfig.numericAttributesToShow.add("temperature");
|
historyConfig.numericAttributesToShow.add("temperature");
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ class EntityState {
|
|||||||
static const unavailable = 'unavailable';
|
static const unavailable = 'unavailable';
|
||||||
static const ok = 'ok';
|
static const ok = 'ok';
|
||||||
static const problem = 'problem';
|
static const problem = 'problem';
|
||||||
|
static const active = 'active';
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntityUIAction {
|
class EntityUIAction {
|
||||||
|
@ -11,6 +11,8 @@ class CoverEntity extends Entity {
|
|||||||
static const SUPPORT_STOP_TILT = 64;
|
static const SUPPORT_STOP_TILT = 64;
|
||||||
static const SUPPORT_SET_TILT_POSITION = 128;
|
static const SUPPORT_SET_TILT_POSITION = 128;
|
||||||
|
|
||||||
|
CoverEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get supportOpen => ((supportedFeatures &
|
bool get supportOpen => ((supportedFeatures &
|
||||||
CoverEntity.SUPPORT_OPEN) ==
|
CoverEntity.SUPPORT_OPEN) ==
|
||||||
CoverEntity.SUPPORT_OPEN);
|
CoverEntity.SUPPORT_OPEN);
|
||||||
@ -45,8 +47,6 @@ class CoverEntity extends Entity {
|
|||||||
bool get canTiltBeOpened => currentTiltPosition < 100;
|
bool get canTiltBeOpened => currentTiltPosition < 100;
|
||||||
bool get canTiltBeClosed => currentTiltPosition > 0;
|
bool get canTiltBeClosed => currentTiltPosition > 0;
|
||||||
|
|
||||||
CoverEntity(Map rawData) : super(rawData);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
return CoverStateWidget();
|
return CoverStateWidget();
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class DateTimeEntity extends Entity {
|
class DateTimeEntity extends Entity {
|
||||||
|
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get hasDate => attributes["has_date"] ?? false;
|
bool get hasDate => attributes["has_date"] ?? false;
|
||||||
bool get hasTime => attributes["has_time"] ?? false;
|
bool get hasTime => attributes["has_time"] ?? false;
|
||||||
int get year => attributes["year"] ?? 1970;
|
int get year => attributes["year"] ?? 1970;
|
||||||
@ -12,8 +14,6 @@ class DateTimeEntity extends Entity {
|
|||||||
String get formattedState => _getFormattedState();
|
String get formattedState => _getFormattedState();
|
||||||
DateTime get dateTimeState => _getDateTimeState();
|
DateTime get dateTimeState => _getDateTimeState();
|
||||||
|
|
||||||
DateTimeEntity(Map rawData) : super(rawData);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
return DateTimeStateWidget();
|
return DateTimeStateWidget();
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class StatelessEntityType {
|
||||||
|
static const NONE = 0;
|
||||||
|
static const MISSED = 1;
|
||||||
|
static const DIVIDER = 2;
|
||||||
|
static const SECTION = 3;
|
||||||
|
static const CALL_SERVICE = 4;
|
||||||
|
static const WEBLINK = 5;
|
||||||
|
}
|
||||||
|
|
||||||
class Entity {
|
class Entity {
|
||||||
|
|
||||||
static List badgeDomains = [
|
static List badgeDomains = [
|
||||||
@ -64,12 +73,13 @@ class Entity {
|
|||||||
Map attributes;
|
Map attributes;
|
||||||
String domain;
|
String domain;
|
||||||
String entityId;
|
String entityId;
|
||||||
|
String entityPicture;
|
||||||
String state;
|
String state;
|
||||||
String displayState;
|
String displayState;
|
||||||
DateTime _lastUpdated;
|
DateTime _lastUpdated;
|
||||||
|
int statelessType = 0;
|
||||||
|
|
||||||
List<Entity> childEntities = [];
|
List<Entity> childEntities = [];
|
||||||
List<String> attributesToShow = ["all"];
|
|
||||||
String deviceClass;
|
String deviceClass;
|
||||||
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||||
chartType: EntityHistoryWidgetType.simple
|
chartType: EntityHistoryWidgetType.simple
|
||||||
@ -85,7 +95,6 @@ class Entity {
|
|||||||
bool get isBadge => Entity.badgeDomains.contains(domain);
|
bool get isBadge => Entity.badgeDomains.contains(domain);
|
||||||
String get icon => attributes["icon"] ?? "";
|
String get icon => attributes["icon"] ?? "";
|
||||||
bool get isOn => state == EntityState.on;
|
bool get isOn => state == EntityState.on;
|
||||||
String get entityPicture => attributes["entity_picture"];
|
|
||||||
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
||||||
List get childEntityIds => attributes["entity_id"] ?? [];
|
List get childEntityIds => attributes["entity_id"] ?? [];
|
||||||
String get lastUpdated => _getLastUpdatedFormatted();
|
String get lastUpdated => _getLastUpdatedFormatted();
|
||||||
@ -93,11 +102,53 @@ class Entity {
|
|||||||
double get doubleState => double.tryParse(state) ?? 0.0;
|
double get doubleState => double.tryParse(state) ?? 0.0;
|
||||||
int get supportedFeatures => attributes["supported_features"] ?? 0;
|
int get supportedFeatures => attributes["supported_features"] ?? 0;
|
||||||
|
|
||||||
Entity(Map rawData) {
|
String _getEntityPictureUrl(String webHost) {
|
||||||
update(rawData);
|
String result = attributes["entity_picture"];
|
||||||
|
if (result == null) return result;
|
||||||
|
if (!result.startsWith("http")) {
|
||||||
|
if (result.startsWith("/")) {
|
||||||
|
result = "$webHost$result";
|
||||||
|
} else {
|
||||||
|
result = "$webHost/$result";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void update(Map rawData) {
|
Entity(Map rawData, String webHost) {
|
||||||
|
update(rawData, webHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.missed(String entityId) {
|
||||||
|
statelessType = StatelessEntityType.MISSED;
|
||||||
|
attributes = {"hidden": false};
|
||||||
|
this.entityId = entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.divider() {
|
||||||
|
statelessType = StatelessEntityType.DIVIDER;
|
||||||
|
attributes = {"hidden": false};
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.section(String label) {
|
||||||
|
statelessType = StatelessEntityType.SECTION;
|
||||||
|
attributes = {"hidden": false, "friendly_name": "$label"};
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.callService({String icon, String name, String service, String actionName}) {
|
||||||
|
statelessType = StatelessEntityType.CALL_SERVICE;
|
||||||
|
entityId = service;
|
||||||
|
displayState = actionName?.toUpperCase() ?? "RUN";
|
||||||
|
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.weblink({String url, String name, String icon}) {
|
||||||
|
statelessType = StatelessEntityType.WEBLINK;
|
||||||
|
entityId = "custom.custom"; //TODO wtf??
|
||||||
|
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(Map rawData, String webHost) {
|
||||||
attributes = rawData["attributes"] ?? {};
|
attributes = rawData["attributes"] ?? {};
|
||||||
domain = rawData["entity_id"].split(".")[0];
|
domain = rawData["entity_id"].split(".")[0];
|
||||||
entityId = rawData["entity_id"];
|
entityId = rawData["entity_id"];
|
||||||
@ -105,6 +156,7 @@ class Entity {
|
|||||||
state = rawData["state"];
|
state = rawData["state"];
|
||||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
|
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
|
||||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||||
|
entityPicture = _getEntityPictureUrl(webHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
double _getDoubleAttributeValue(String attributeName) {
|
double _getDoubleAttributeValue(String attributeName) {
|
||||||
|
@ -4,6 +4,7 @@ class EntityWrapper {
|
|||||||
|
|
||||||
String displayName;
|
String displayName;
|
||||||
String icon;
|
String icon;
|
||||||
|
String entityPicture;
|
||||||
EntityUIAction uiAction;
|
EntityUIAction uiAction;
|
||||||
Entity entity;
|
Entity entity;
|
||||||
|
|
||||||
@ -14,10 +15,15 @@ class EntityWrapper {
|
|||||||
String displayName,
|
String displayName,
|
||||||
this.uiAction
|
this.uiAction
|
||||||
}) {
|
}) {
|
||||||
|
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||||
this.icon = icon ?? entity.icon;
|
this.icon = icon ?? entity.icon;
|
||||||
|
if (icon == null) {
|
||||||
|
entityPicture = entity.entityPicture;
|
||||||
|
}
|
||||||
this.displayName = displayName ?? entity.displayName;
|
this.displayName = displayName ?? entity.displayName;
|
||||||
if (this.uiAction == null) {
|
if (uiAction == null) {
|
||||||
this.uiAction = EntityUIAction();
|
uiAction = EntityUIAction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +55,16 @@ class EntityWrapper {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case EntityUIAction.navigate: {
|
||||||
|
if (uiAction.tapService.startsWith("/")) {
|
||||||
|
//TODO handle local urls
|
||||||
|
Logger.w("Local urls is not supported yet");
|
||||||
|
} else {
|
||||||
|
HAUtils.launchURL(uiAction.tapService);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -79,6 +95,16 @@ class EntityWrapper {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case EntityUIAction.navigate: {
|
||||||
|
if (uiAction.holdService.startsWith("/")) {
|
||||||
|
//TODO handle local urls
|
||||||
|
Logger.w("Local urls is not supported yet");
|
||||||
|
} else {
|
||||||
|
HAUtils.launchURL(uiAction.holdService);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ class FanEntity extends Entity {
|
|||||||
static const SUPPORT_OSCILLATE = 2;
|
static const SUPPORT_OSCILLATE = 2;
|
||||||
static const SUPPORT_DIRECTION = 4;
|
static const SUPPORT_DIRECTION = 4;
|
||||||
|
|
||||||
FanEntity(Map rawData) : super(rawData);
|
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get supportSetSpeed => ((supportedFeatures &
|
bool get supportSetSpeed => ((supportedFeatures &
|
||||||
FanEntity.SUPPORT_SET_SPEED) ==
|
FanEntity.SUPPORT_SET_SPEED) ==
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class GroupEntity extends Entity {
|
class GroupEntity extends Entity {
|
||||||
GroupEntity(Map rawData) : super(rawData);
|
|
||||||
|
|
||||||
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
|
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
|
||||||
String mutualDomain;
|
String mutualDomain;
|
||||||
bool switchable = false;
|
bool switchable = false;
|
||||||
|
|
||||||
|
GroupEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
if (switchable) {
|
if (switchable) {
|
||||||
@ -19,8 +20,8 @@ class GroupEntity extends Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void update(Map rawData) {
|
void update(Map rawData, String webHost) {
|
||||||
super.update(rawData);
|
super.update(rawData, webHost);
|
||||||
if (_isOneDomain()) {
|
if (_isOneDomain()) {
|
||||||
mutualDomain = attributes['entity_id'][0].split(".")[0];
|
mutualDomain = attributes['entity_id'][0].split(".")[0];
|
||||||
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
|
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
|
||||||
|
@ -33,6 +33,7 @@ class LightEntity extends Entity {
|
|||||||
LightEntity.SUPPORT_WHITE_VALUE);
|
LightEntity.SUPPORT_WHITE_VALUE);
|
||||||
|
|
||||||
int get brightness => _getIntAttributeValue("brightness");
|
int get brightness => _getIntAttributeValue("brightness");
|
||||||
|
int get whiteValue => _getIntAttributeValue("white_value");
|
||||||
String get effect => attributes["effect"];
|
String get effect => attributes["effect"];
|
||||||
int get colorTemp => _getIntAttributeValue("color_temp");
|
int get colorTemp => _getIntAttributeValue("color_temp");
|
||||||
double get maxMireds => _getDoubleAttributeValue("max_mireds");
|
double get maxMireds => _getDoubleAttributeValue("max_mireds");
|
||||||
@ -41,15 +42,18 @@ class LightEntity extends Entity {
|
|||||||
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
|
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
|
||||||
List<String> get effectList => getStringListAttributeValue("effect_list");
|
List<String> get effectList => getStringListAttributeValue("effect_list");
|
||||||
|
|
||||||
LightEntity(Map rawData) : super(rawData);
|
LightEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
HSVColor _getColor() {
|
HSVColor _getColor() {
|
||||||
List hs = attributes["hs_color"];
|
List hs = attributes["hs_color"];
|
||||||
|
List rgb = attributes["rgb_color"];
|
||||||
try {
|
try {
|
||||||
if ((hs != null) && (hs.length > 0)) {
|
if (hs != null && hs.isNotEmpty) {
|
||||||
double sat = hs[1]/100;
|
double sat = hs[1]/100;
|
||||||
String ssat = sat.toStringAsFixed(2);
|
String ssat = sat.toStringAsFixed(2);
|
||||||
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
|
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
|
||||||
|
} else if (rgb != null && rgb.isNotEmpty) {
|
||||||
|
return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2]));
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,21 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class LockEntity extends Entity {
|
class LockEntity extends Entity {
|
||||||
LockEntity(Map rawData) : super(rawData);
|
LockEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get isLocked => state == "locked";
|
bool get isLocked => state == "locked";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
return LockStateWidget();
|
return LockStateWidget(
|
||||||
|
assumedState: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePartForPage(BuildContext context) {
|
||||||
|
return LockStateWidget(
|
||||||
|
assumedState: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,7 +20,7 @@ class MediaPlayerEntity extends Entity {
|
|||||||
static const SUPPORT_SHUFFLE_SET = 32768;
|
static const SUPPORT_SHUFFLE_SET = 32768;
|
||||||
static const SUPPORT_SELECT_SOUND_MODE = 65536;
|
static const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||||
|
|
||||||
MediaPlayerEntity(Map rawData) : super(rawData);
|
MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get supportPause => ((supportedFeatures &
|
bool get supportPause => ((supportedFeatures &
|
||||||
MediaPlayerEntity.SUPPORT_PAUSE) ==
|
MediaPlayerEntity.SUPPORT_PAUSE) ==
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class SunEntity extends Entity {
|
class SunEntity extends Entity {
|
||||||
SunEntity(Map rawData) : super(rawData);
|
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SensorEntity extends Entity {
|
class SensorEntity extends Entity {
|
||||||
@ -12,6 +12,6 @@ class SensorEntity extends Entity {
|
|||||||
numericState: true
|
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>()
|
? (attributes["options"] as List).cast<String>()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
SelectEntity(Map rawData) : super(rawData);
|
SelectEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class SliderEntity extends Entity {
|
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 minValue => _getDoubleAttributeValue("min") ?? 0.0;
|
||||||
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
|
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class SwitchEntity extends Entity {
|
class SwitchEntity extends Entity {
|
||||||
SwitchEntity(Map rawData) : super(rawData);
|
SwitchEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class TextEntity extends Entity {
|
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 valueMinLength => attributes["min"] ?? -1;
|
||||||
int get valueMaxLength => attributes["max"] ?? -1;
|
int get valueMaxLength => attributes["max"] ?? -1;
|
||||||
|
45
lib/entity_class/timer_entity.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class TimerEntity extends Entity {
|
||||||
|
TimerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
Duration duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(Map rawData, String webHost) {
|
||||||
|
super.update(rawData, webHost);
|
||||||
|
String durationSource = "${attributes["duration"]}";
|
||||||
|
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 {
|
||||||
|
duration = Duration(seconds: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return TimerState();
|
||||||
|
}
|
||||||
|
}
|
@ -2,13 +2,15 @@ part of 'main.dart';
|
|||||||
|
|
||||||
class EntityCollection {
|
class EntityCollection {
|
||||||
|
|
||||||
|
final homeAssistantWebHost;
|
||||||
|
|
||||||
Map<String, Entity> _allEntities;
|
Map<String, Entity> _allEntities;
|
||||||
//Map<String, Entity> views;
|
//Map<String, Entity> views;
|
||||||
|
|
||||||
bool get isEmpty => _allEntities.isEmpty;
|
bool get isEmpty => _allEntities.isEmpty;
|
||||||
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
||||||
|
|
||||||
EntityCollection() {
|
EntityCollection(this.homeAssistantWebHost) {
|
||||||
_allEntities = {};
|
_allEntities = {};
|
||||||
//views = {};
|
//views = {};
|
||||||
}
|
}
|
||||||
@ -33,67 +35,74 @@ class EntityCollection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_allEntities.clear();
|
||||||
|
}
|
||||||
|
|
||||||
Entity _createEntityInstance(rawEntityData) {
|
Entity _createEntityInstance(rawEntityData) {
|
||||||
switch (rawEntityData["entity_id"].split(".")[0]) {
|
switch (rawEntityData["entity_id"].split(".")[0]) {
|
||||||
case 'sun': {
|
case 'sun': {
|
||||||
return SunEntity(rawEntityData);
|
return SunEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "media_player": {
|
case "media_player": {
|
||||||
return MediaPlayerEntity(rawEntityData);
|
return MediaPlayerEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case 'sensor': {
|
case 'sensor': {
|
||||||
return SensorEntity(rawEntityData);
|
return SensorEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case 'lock': {
|
case 'lock': {
|
||||||
return LockEntity(rawEntityData);
|
return LockEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "automation": {
|
case "automation": {
|
||||||
return AutomationEntity(rawEntityData);
|
return AutomationEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "input_boolean":
|
case "input_boolean":
|
||||||
case "switch": {
|
case "switch": {
|
||||||
return SwitchEntity(rawEntityData);
|
return SwitchEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "light": {
|
case "light": {
|
||||||
return LightEntity(rawEntityData);
|
return LightEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "group": {
|
case "group": {
|
||||||
return GroupEntity(rawEntityData);
|
return GroupEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "script":
|
case "script":
|
||||||
case "scene": {
|
case "scene": {
|
||||||
return ButtonEntity(rawEntityData);
|
return ButtonEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "input_datetime": {
|
case "input_datetime": {
|
||||||
return DateTimeEntity(rawEntityData);
|
return DateTimeEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "input_select": {
|
case "input_select": {
|
||||||
return SelectEntity(rawEntityData);
|
return SelectEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "input_number": {
|
case "input_number": {
|
||||||
return SliderEntity(rawEntityData);
|
return SliderEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "input_text": {
|
case "input_text": {
|
||||||
return TextEntity(rawEntityData);
|
return TextEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "climate": {
|
case "climate": {
|
||||||
return ClimateEntity(rawEntityData);
|
return ClimateEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "cover": {
|
case "cover": {
|
||||||
return CoverEntity(rawEntityData);
|
return CoverEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "fan": {
|
case "fan": {
|
||||||
return FanEntity(rawEntityData);
|
return FanEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "camera": {
|
case "camera": {
|
||||||
return CameraEntity(rawEntityData);
|
return CameraEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
case "alarm_control_panel": {
|
case "alarm_control_panel": {
|
||||||
return AlarmControlPanelEntity(rawEntityData);
|
return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "timer": {
|
||||||
|
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return Entity(rawEntityData);
|
return Entity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,7 +127,7 @@ class EntityCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateFromRaw(Map rawEntityData) {
|
void updateFromRaw(Map rawEntityData) {
|
||||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
|
get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity get(String entityId) {
|
Entity get(String entityId) {
|
||||||
|
@ -9,7 +9,12 @@ class ButtonEntityContainer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
}
|
||||||
|
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
@ -22,7 +27,7 @@ class ButtonEntityContainer extends StatelessWidget {
|
|||||||
fit: BoxFit.fitHeight,
|
fit: BoxFit.fitHeight,
|
||||||
child: EntityIcon(
|
child: EntityIcon(
|
||||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||||
iconSize: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -14,11 +14,11 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
{
|
{
|
||||||
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
||||||
? Icon(
|
? Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
|
MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
)
|
)
|
||||||
: Icon(
|
: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
|
MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -27,14 +27,20 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
case "media_player":
|
case "media_player":
|
||||||
case "binary_sensor":
|
case "binary_sensor":
|
||||||
{
|
{
|
||||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
badgeIcon = EntityIcon(
|
||||||
entityModel.entityWrapper, iconSize, Colors.black);
|
padding: EdgeInsets.all(0.0),
|
||||||
|
size: iconSize,
|
||||||
|
color: Colors.black
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "device_tracker":
|
case "device_tracker":
|
||||||
{
|
{
|
||||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
badgeIcon = EntityIcon(
|
||||||
entityModel.entityWrapper, iconSize, Colors.black);
|
padding: EdgeInsets.all(0.0),
|
||||||
|
size: iconSize,
|
||||||
|
color: Colors.black
|
||||||
|
);
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.state;
|
onBadgeTextValue = entityModel.entityWrapper.entity.state;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CameraEntity _entity;
|
CameraEntity _entity;
|
||||||
|
String _webHost;
|
||||||
|
|
||||||
http.Client client;
|
http.Client client;
|
||||||
http.StreamedResponse response;
|
http.StreamedResponse response;
|
||||||
@ -28,7 +29,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
void _connect() async {
|
void _connect() async {
|
||||||
started = true;
|
started = true;
|
||||||
timeToStop = false;
|
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
|
client = new http.Client(); // create a client to make api calls
|
||||||
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
|
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
|
||||||
Logger.d("[Sending] ==> $streamUrl");
|
Logger.d("[Sending] ==> $streamUrl");
|
||||||
@ -130,6 +131,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
.of(context)
|
.of(context)
|
||||||
.entityWrapper
|
.entityWrapper
|
||||||
.entity;
|
.entity;
|
||||||
|
_webHost = HomeAssistantModel.of(context).homeAssistant.connection.httpWebHost;
|
||||||
_connect();
|
_connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,20 +7,9 @@ class EntityAttributesList extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final entityModel = EntityModel.of(context);
|
final entityModel = EntityModel.of(context);
|
||||||
List<Widget> attrs = [];
|
List<Widget> attrs = [];
|
||||||
if ((entityModel.entityWrapper.entity.attributesToShow == null) ||
|
|
||||||
(entityModel.entityWrapper.entity.attributesToShow.contains("all"))) {
|
|
||||||
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
|
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
|
||||||
attrs.add(_buildSingleAttribute("$name", "$value"));
|
attrs.add(_buildSingleAttribute("$name", "${value ?? '-'}"));
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
entityModel.entityWrapper.entity.attributesToShow.forEach((String attr) {
|
|
||||||
String attrValue = entityModel.entityWrapper.entity.getAttribute("$attr");
|
|
||||||
if (attrValue != null) {
|
|
||||||
attrs.add(
|
|
||||||
_buildSingleAttribute("$attr", "$attrValue"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -49,7 +38,7 @@ class EntityAttributesList extends StatelessWidget {
|
|||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"$value",
|
"${value}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -440,13 +440,13 @@ class TemperatureControlWidget extends StatelessWidget {
|
|||||||
Column(
|
Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
'mdi:chevron-up')),
|
'mdi:chevron-up')),
|
||||||
iconSize: 30.0,
|
iconSize: 30.0,
|
||||||
onPressed: () => onInc(),
|
onPressed: () => onInc(),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
'mdi:chevron-down')),
|
'mdi:chevron-down')),
|
||||||
iconSize: 30.0,
|
iconSize: 30.0,
|
||||||
onPressed: () => onDec(),
|
onPressed: () => onDec(),
|
||||||
|
@ -157,7 +157,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
|
|||||||
if (entity.supportOpenTilt) {
|
if (entity.supportOpenTilt) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName(
|
MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:arrow-top-right"),
|
"mdi:arrow-top-right"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
@ -170,7 +170,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
|
|||||||
if (entity.supportStopTilt) {
|
if (entity.supportStopTilt) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
onPressed: () => _stop(entity)));
|
onPressed: () => _stop(entity)));
|
||||||
@ -182,7 +182,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
|
|||||||
if (entity.supportCloseTilt) {
|
if (entity.supportCloseTilt) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName(
|
MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:arrow-bottom-left"),
|
"mdi:arrow-bottom-left"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
|
@ -10,6 +10,7 @@ class LightControlsWidget extends StatefulWidget {
|
|||||||
class _LightControlsWidgetState extends State<LightControlsWidget> {
|
class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||||
|
|
||||||
int _tmpBrightness;
|
int _tmpBrightness;
|
||||||
|
int _tmpWhiteValue;
|
||||||
int _tmpColorTemp = 0;
|
int _tmpColorTemp = 0;
|
||||||
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
|
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
|
||||||
bool _changedHere = false;
|
bool _changedHere = false;
|
||||||
@ -17,6 +18,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
void _resetState(LightEntity entity) {
|
void _resetState(LightEntity entity) {
|
||||||
_tmpBrightness = entity.brightness ?? 0;
|
_tmpBrightness = entity.brightness ?? 0;
|
||||||
|
_tmpWhiteValue = entity.whiteValue ?? 0;
|
||||||
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
|
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
|
||||||
_tmpColor = entity.color ?? _tmpColor;
|
_tmpColor = entity.color ?? _tmpColor;
|
||||||
_tmpEffect = entity.effect;
|
_tmpEffect = entity.effect;
|
||||||
@ -38,6 +40,17 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setWhiteValue(LightEntity entity, double value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpWhiteValue = value.round();
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "turn_on", entity.entityId,
|
||||||
|
{"white_value": _tmpWhiteValue}));
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _setColorTemp(LightEntity entity, double value) {
|
void _setColorTemp(LightEntity entity, double value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tmpColorTemp = value.round();
|
_tmpColorTemp = value.round();
|
||||||
@ -84,6 +97,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildBrightnessControl(entity),
|
_buildBrightnessControl(entity),
|
||||||
|
_buildWhiteValueControl(entity),
|
||||||
_buildColorTempControl(entity),
|
_buildColorTempControl(entity),
|
||||||
_buildColorControl(entity),
|
_buildColorControl(entity),
|
||||||
_buildEffectControl(entity)
|
_buildEffectControl(entity)
|
||||||
@ -112,6 +126,27 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildWhiteValueControl(LightEntity entity) {
|
||||||
|
if ((entity.supportWhiteValue) && (_tmpWhiteValue != null)) {
|
||||||
|
return UniversalSlider(
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_changedHere = true;
|
||||||
|
_tmpWhiteValue = value.round();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
min: 0.0,
|
||||||
|
max: 255.0,
|
||||||
|
onChangeEnd: (value) => _setWhiteValue(entity, value),
|
||||||
|
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
|
||||||
|
title: "White",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildColorTempControl(LightEntity entity) {
|
Widget _buildColorTempControl(LightEntity entity) {
|
||||||
if (entity.supportColorTemp) {
|
if (entity.supportColorTemp) {
|
||||||
return UniversalSlider(
|
return UniversalSlider(
|
||||||
@ -136,30 +171,45 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
Widget _buildColorControl(LightEntity entity) {
|
Widget _buildColorControl(LightEntity entity) {
|
||||||
if (entity.supportColor) {
|
if (entity.supportColor) {
|
||||||
return LightColorPicker(
|
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
LightColorPicker(
|
||||||
color: _tmpColor,
|
color: _tmpColor,
|
||||||
onColorSelected: (color) => _setColor(entity, color),
|
onColorSelected: (color) => _setColor(entity, color),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
color: _tmpColor.toColor(),
|
||||||
|
child: Text('Copy color'),
|
||||||
|
onPressed: _tmpColor == null ? null : () {
|
||||||
|
setState(() {
|
||||||
|
HomeAssistantModel
|
||||||
|
.of(context)
|
||||||
|
.homeAssistant
|
||||||
|
.savedColor = _tmpColor;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
color: savedColor?.toColor() ?? Colors.transparent,
|
||||||
|
child: Text('Paste color'),
|
||||||
|
onPressed: savedColor == null ? null : () {
|
||||||
|
_setColor(entity, savedColor);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0.0, height: 0.0);
|
return Container(width: 0.0, height: 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showColorPicker(LightEntity entity) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
titlePadding: EdgeInsets.all(0.0),
|
|
||||||
contentPadding: EdgeInsets.all(0.0),
|
|
||||||
content: LightColorPicker(
|
|
||||||
color: _tmpColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEffectControl(LightEntity entity) {
|
Widget _buildEffectControl(LightEntity entity) {
|
||||||
if ((entity.supportEffect) && (entity.effectList != null)) {
|
if ((entity.supportEffect) && (entity.effectList != null)) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
|
@ -73,7 +73,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildImage(MediaPlayerEntity entity) {
|
Widget _buildImage(MediaPlayerEntity entity) {
|
||||||
String state = entity.state;
|
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(
|
return Container(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -81,7 +81,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Image(
|
child: Image(
|
||||||
image: CachedNetworkImageProvider("$homeAssistantWebHost${entity.entityPicture}"),
|
image: CachedNetworkImageProvider("${entity.entityPicture}"),
|
||||||
height: 240.0,
|
height: 240.0,
|
||||||
//width: 320.0,
|
//width: 320.0,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
@ -95,7 +95,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(
|
Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:movie"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
||||||
size: 150.0,
|
size: 150.0,
|
||||||
color: EntityColor.stateColor("$state"),
|
color: EntityColor.stateColor("$state"),
|
||||||
)
|
)
|
||||||
@ -227,7 +227,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
|||||||
if (showMenu) {
|
if (showMenu) {
|
||||||
result.add(
|
result.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:dots-vertical")),
|
"mdi:dots-vertical")),
|
||||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
|
||||||
)
|
)
|
||||||
@ -307,11 +307,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
||||||
Widget muteWidget;
|
Widget muteWidget;
|
||||||
Widget volumeStepWidget;
|
Widget volumeStepWidget;
|
||||||
if (entity.supportVolumeMute) {
|
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
|
||||||
bool isMuted = entity.attributes["is_volume_muted"] ?? false;
|
bool isMuted = entity.attributes["is_volume_muted"] ?? false;
|
||||||
muteWidget =
|
muteWidget =
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up),
|
icon: Icon(isMuted ? Icons.volume_up : Icons.volume_off),
|
||||||
onPressed: () => _setVolumeMute(!isMuted, entity.entityId)
|
onPressed: () => _setVolumeMute(!isMuted, entity.entityId)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -322,11 +322,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:plus")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||||
onPressed: () => _setVolumeUp(entity.entityId)
|
onPressed: () => _setVolumeUp(entity.entityId)
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:minus")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||||
onPressed: () => _setVolumeDown(entity.entityId)
|
onPressed: () => _setVolumeDown(entity.entityId)
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -11,6 +11,25 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
}
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
||||||
|
return Divider();
|
||||||
|
}
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Divider(),
|
||||||
|
Text(
|
||||||
|
"${entityModel.entityWrapper.entity.displayName}",
|
||||||
|
style: TextStyle(color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
if (entityModel.handleTap) {
|
if (entityModel.handleTap) {
|
||||||
@ -30,7 +49,9 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
fit: FlexFit.tight,
|
fit: FlexFit.tight,
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: EntityName(),
|
child: EntityName(
|
||||||
|
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
state
|
state
|
||||||
],
|
],
|
||||||
|
@ -2,6 +2,8 @@ part of '../main.dart';
|
|||||||
|
|
||||||
class EntityColor {
|
class EntityColor {
|
||||||
|
|
||||||
|
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
|
||||||
|
|
||||||
static const badgeColors = {
|
static const badgeColors = {
|
||||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||||
@ -10,15 +12,16 @@ class EntityColor {
|
|||||||
static const _stateColors = {
|
static const _stateColors = {
|
||||||
EntityState.on: Colors.amber,
|
EntityState.on: Colors.amber,
|
||||||
"auto": Colors.amber,
|
"auto": Colors.amber,
|
||||||
EntityState.idle: Colors.amber,
|
EntityState.active: Colors.amber,
|
||||||
EntityState.playing: Colors.amber,
|
EntityState.playing: Colors.amber,
|
||||||
"above_horizon": Colors.amber,
|
"above_horizon": Colors.amber,
|
||||||
EntityState.home: Colors.amber,
|
EntityState.home: Colors.amber,
|
||||||
EntityState.open: Colors.amber,
|
EntityState.open: Colors.amber,
|
||||||
EntityState.off: Color.fromRGBO(68, 115, 158, 1.0),
|
EntityState.off: defaultStateColor,
|
||||||
EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0),
|
EntityState.closed: defaultStateColor,
|
||||||
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0),
|
"below_horizon": defaultStateColor,
|
||||||
"default": Color.fromRGBO(68, 115, 158, 1.0),
|
"default": defaultStateColor,
|
||||||
|
EntityState.idle: defaultStateColor,
|
||||||
"heat": Colors.redAccent,
|
"heat": Colors.redAccent,
|
||||||
"cool": Colors.lightBlue,
|
"cool": Colors.lightBlue,
|
||||||
EntityState.unavailable: Colors.black26,
|
EntityState.unavailable: Colors.black26,
|
||||||
|
@ -3,20 +3,71 @@ part of '../main.dart';
|
|||||||
class EntityIcon extends StatelessWidget {
|
class EntityIcon extends StatelessWidget {
|
||||||
|
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final double iconSize;
|
final double size;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB(
|
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
|
||||||
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key);
|
|
||||||
|
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||||
|
String domain = entityId.split(".")[0];
|
||||||
|
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
|
||||||
|
String iconNameByDeviceClass;
|
||||||
|
if (deviceClass != null) {
|
||||||
|
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
|
||||||
|
}
|
||||||
|
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
|
||||||
|
if (iconName != null) {
|
||||||
|
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildIcon(EntityWrapper data, Color color) {
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (data.entityPicture != null) {
|
||||||
|
return Container(
|
||||||
|
height: size+12,
|
||||||
|
width: size+12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
image: DecorationImage(
|
||||||
|
fit:BoxFit.cover,
|
||||||
|
image: CachedNetworkImageProvider(
|
||||||
|
"${data.entityPicture}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
String iconName = data.icon;
|
||||||
|
int iconCode = 0;
|
||||||
|
if (iconName.length > 0) {
|
||||||
|
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||||
|
} else {
|
||||||
|
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||||
|
data.entity.deviceClass, data.entity.state); //
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
|
||||||
|
child: Icon(
|
||||||
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
|
size: size,
|
||||||
|
color: color,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
child: buildIcon(
|
||||||
entityWrapper,
|
entityWrapper,
|
||||||
iconSize,
|
color ?? EntityColor.stateColor(entityWrapper.entity.state)
|
||||||
EntityColor.stateColor(entityWrapper.entity.state)
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,10 @@ class EntityName extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
TextStyle textStyle = TextStyle(fontSize: fontSize);
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||||
|
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
|
||||||
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -21,7 +25,7 @@ class EntityName extends StatelessWidget {
|
|||||||
overflow: textOverflow,
|
overflow: textOverflow,
|
||||||
softWrap: wordsWrap,
|
softWrap: wordsWrap,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
style: TextStyle(fontSize: fontSize),
|
style: textStyle,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -22,6 +22,12 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
}
|
||||||
|
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (!nameInTheBottom) {
|
if (!nameInTheBottom) {
|
||||||
if (showName) {
|
if (showName) {
|
||||||
@ -35,7 +41,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
result.add(
|
result.add(
|
||||||
EntityIcon(
|
EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
iconSize: iconSize,
|
size: iconSize,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (!nameInTheBottom) {
|
if (!nameInTheBottom) {
|
||||||
|
@ -32,6 +32,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
List _history;
|
List _history;
|
||||||
bool _needToUpdateHistory;
|
bool _needToUpdateHistory;
|
||||||
DateTime _historyLastUpdated;
|
DateTime _historyLastUpdated;
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -46,17 +47,21 @@ 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) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_history = history.isNotEmpty ? history[0] : [];
|
_history = history.isNotEmpty ? history[0] : [];
|
||||||
_needToUpdateHistory = false;
|
_needToUpdateHistory = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
Logger.e("Error loading $entityId history: $e");
|
Logger.e("Error loading $entityId history: $e");
|
||||||
|
if (!_disposed) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_history = [];
|
_history = [];
|
||||||
_needToUpdateHistory = false;
|
_needToUpdateHistory = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,4 +136,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposed = true;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
19
lib/entity_widgets/missed_entity.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class MissedEntityWidget extends StatelessWidget {
|
||||||
|
MissedEntityWidget({
|
||||||
|
Key key
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
return Container(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(5.0),
|
||||||
|
child: Text("Entity not available: ${entityModel.entityWrapper.entity.entityId}"),
|
||||||
|
),
|
||||||
|
color: Colors.amber[100],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,7 @@ class CoverStateWidget extends StatelessWidget {
|
|||||||
if (entity.supportOpen) {
|
if (entity.supportOpen) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-up"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
onPressed: entity.canBeOpened ? () => _open(entity) : null));
|
onPressed: entity.canBeOpened ? () => _open(entity) : null));
|
||||||
@ -36,7 +36,7 @@ class CoverStateWidget extends StatelessWidget {
|
|||||||
if (entity.supportStop) {
|
if (entity.supportStop) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
onPressed: () => _stop(entity)));
|
onPressed: () => _stop(entity)));
|
||||||
@ -48,7 +48,7 @@ class CoverStateWidget extends StatelessWidget {
|
|||||||
if (entity.supportClose) {
|
if (entity.supportClose) {
|
||||||
buttons.add(IconButton(
|
buttons.add(IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-down"),
|
||||||
size: Sizes.iconSize,
|
size: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
onPressed: entity.canBeClosed ? () => _close(entity) : null));
|
onPressed: entity.canBeClosed ? () => _close(entity) : null));
|
||||||
|
@ -2,6 +2,10 @@ part of '../../main.dart';
|
|||||||
|
|
||||||
class LockStateWidget extends StatelessWidget {
|
class LockStateWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool assumedState;
|
||||||
|
|
||||||
|
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
|
||||||
|
|
||||||
void _lock(Entity entity) {
|
void _lock(Entity entity) {
|
||||||
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
|
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
|
||||||
}
|
}
|
||||||
@ -14,6 +18,35 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final entityModel = EntityModel.of(context);
|
final entityModel = EntityModel.of(context);
|
||||||
final LockEntity entity = entityModel.entityWrapper.entity;
|
final LockEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
if (assumedState) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
height: 34.0,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: () => _unlock(entity),
|
||||||
|
child: Text("UNLOCK",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 34.0,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: () => _lock(entity),
|
||||||
|
child: Text("LOCK",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 34.0,
|
height: 34.0,
|
||||||
child: FlatButton(
|
child: FlatButton(
|
||||||
@ -30,3 +63,4 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
@ -6,14 +6,26 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
final TextAlign textAlign;
|
final TextAlign textAlign;
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
|
final String customValue;
|
||||||
|
|
||||||
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final entityModel = EntityModel.of(context);
|
final entityModel = EntityModel.of(context);
|
||||||
String state = entityModel.entityWrapper.entity.displayState ?? "";
|
String state;
|
||||||
|
if (customValue == null) {
|
||||||
|
state = entityModel.entityWrapper.entity.displayState ?? "";
|
||||||
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
|
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
|
||||||
|
} else {
|
||||||
|
state = customValue;
|
||||||
|
}
|
||||||
|
TextStyle textStyle = TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
);
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||||
|
textStyle = textStyle.apply(color: Colors.blue);
|
||||||
|
}
|
||||||
while (state.contains(" ")){
|
while (state.contains(" ")){
|
||||||
state = state.replaceAll(" ", " ");
|
state = state.replaceAll(" ", " ");
|
||||||
}
|
}
|
||||||
@ -25,9 +37,7 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
style: new TextStyle(
|
style: textStyle
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
|
@ -71,13 +71,13 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _setNewState(false, entity),
|
onPressed: () => _setNewState(false, entity),
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash-off")),
|
||||||
color: newState == EntityState.on ? Colors.black : Colors.blue,
|
color: newState == EntityState.on ? Colors.black : Colors.blue,
|
||||||
iconSize: Sizes.iconSize,
|
iconSize: Sizes.iconSize,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _setNewState(true, entity),
|
onPressed: () => _setNewState(true, entity),
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash")),
|
||||||
color: newState == EntityState.on ? Colors.blue : Colors.black,
|
color: newState == EntityState.on ? Colors.blue : Colors.black,
|
||||||
iconSize: Sizes.iconSize
|
iconSize: Sizes.iconSize
|
||||||
)
|
)
|
||||||
|
65
lib/entity_widgets/state/timer_state.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class TimerState extends StatefulWidget {
|
||||||
|
//final bool expanded;
|
||||||
|
//final TextAlign textAlign;
|
||||||
|
//final EdgeInsetsGeometry padding;
|
||||||
|
//final int maxLines;
|
||||||
|
|
||||||
|
const TimerState({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TimerStateState createState() => _TimerStateState();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimerStateState extends State<TimerState> {
|
||||||
|
|
||||||
|
Timer timer;
|
||||||
|
Duration remaining = Duration(seconds: 0);
|
||||||
|
|
||||||
|
void checkState(TimerEntity entity) {
|
||||||
|
if (entity.state == EntityState.active) {
|
||||||
|
//Logger.d("Timer is active");
|
||||||
|
if (timer == null || !timer.isActive) {
|
||||||
|
timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||||
|
setState(() {
|
||||||
|
try {
|
||||||
|
int passed = DateTime
|
||||||
|
.now()
|
||||||
|
.difference(entity._lastUpdated)
|
||||||
|
.inSeconds;
|
||||||
|
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}");
|
||||||
|
remaining = Duration(seconds: 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timer?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
EntityModel model = EntityModel.of(context);
|
||||||
|
TimerEntity entity = model.entityWrapper.entity;
|
||||||
|
checkState(entity);
|
||||||
|
if (entity.state != EntityState.active) {
|
||||||
|
return SimpleEntityState();
|
||||||
|
} else {
|
||||||
|
return SimpleEntityState(
|
||||||
|
customValue: "${remaining.toString().split('.')[0]}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,43 +1,27 @@
|
|||||||
part of 'main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class HomeAssistant {
|
class HomeAssistant {
|
||||||
String _webSocketAPIEndpoint;
|
|
||||||
String _password;
|
final Connection connection = Connection();
|
||||||
|
|
||||||
bool _useLovelace = false;
|
bool _useLovelace = false;
|
||||||
|
//bool isSettingsLoaded = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
IOWebSocketChannel _hassioChannel;
|
|
||||||
SendMessageQueue _messageQueue;
|
|
||||||
|
|
||||||
int _currentMessageId = 0;
|
|
||||||
int _statesMessageId = 0;
|
|
||||||
int _servicesMessageId = 0;
|
|
||||||
int _subscriptionMessageId = 0;
|
|
||||||
int _configMessageId = 0;
|
|
||||||
int _userInfoMessageId = 0;
|
|
||||||
int _lovelaceMessageId = 0;
|
|
||||||
EntityCollection entities;
|
EntityCollection entities;
|
||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
String _userName;
|
String _userName;
|
||||||
|
String hostname;
|
||||||
|
HSVColor savedColor;
|
||||||
|
|
||||||
Map _rawLovelaceData;
|
Map _rawLovelaceData;
|
||||||
|
|
||||||
Completer _fetchCompleter;
|
List<Panel> panels = [];
|
||||||
Completer _statesCompleter;
|
|
||||||
Completer _servicesCompleter;
|
|
||||||
Completer _lovelaceCompleter;
|
|
||||||
Completer _configCompleter;
|
|
||||||
Completer _connectionCompleter;
|
|
||||||
Completer _userInfoCompleter;
|
|
||||||
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) {
|
||||||
@ -48,117 +32,39 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
String get userName => _userName ?? locationName;
|
String get userName => _userName ?? locationName;
|
||||||
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 isNoViews => ui == null || ui.isEmpty;
|
||||||
//int get viewsCount => entities.views.length ?? 0;
|
//int get viewsCount => entities.views.length ?? 0;
|
||||||
|
|
||||||
HomeAssistant() {
|
HomeAssistant();
|
||||||
entities = EntityCollection();
|
|
||||||
_messageQueue = SendMessageQueue(messageExpirationTime);
|
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) {
|
Completer _fetchCompleter;
|
||||||
_webSocketAPIEndpoint = url;
|
|
||||||
_password = password;
|
|
||||||
_useLovelace = useLovelace;
|
|
||||||
Logger.d( "Use lovelace is $_useLovelace");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future fetch() {
|
Future fetch() {
|
||||||
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
|
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||||
Logger.w("Previous fetch is not complited");
|
Logger.w("Previous data fetch is not completed yet");
|
||||||
} 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _fetchCompleter.future;
|
return _fetchCompleter.future;
|
||||||
}
|
}
|
||||||
|
_fetchCompleter = Completer();
|
||||||
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 {
|
|
||||||
List<Future> futures = [];
|
List<Future> futures = [];
|
||||||
futures.add(_getStates());
|
futures.add(_getStates());
|
||||||
if (_useLovelace) {
|
if (_useLovelace) {
|
||||||
@ -167,199 +73,77 @@ class HomeAssistant {
|
|||||||
futures.add(_getConfig());
|
futures.add(_getConfig());
|
||||||
futures.add(_getServices());
|
futures.add(_getServices());
|
||||||
futures.add(_getUserInfo());
|
futures.add(_getUserInfo());
|
||||||
try {
|
futures.add(_getPanels());
|
||||||
await Future.wait(futures);
|
Future.wait(futures).then((_) {
|
||||||
_createUI();
|
_createUI();
|
||||||
_completeFetching(null);
|
|
||||||
} catch (error) {
|
|
||||||
_completeFetching(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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") {
|
|
||||||
_sendAuthMessageRaw('{"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'}");
|
|
||||||
if (data["id"] == _configMessageId) {
|
|
||||||
_parseConfig(data);
|
|
||||||
} else if (data["id"] == _statesMessageId) {
|
|
||||||
_parseEntities(data);
|
|
||||||
} else if (data["id"] == _lovelaceMessageId) {
|
|
||||||
_handleLovelace(data);
|
|
||||||
} else if (data["id"] == _servicesMessageId) {
|
|
||||||
_parseServices(data);
|
|
||||||
} else if (data["id"] == _userInfoMessageId) {
|
|
||||||
_parseUserInfo(data);
|
|
||||||
}
|
|
||||||
} 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;
|
|
||||||
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getConfig() {
|
|
||||||
_configCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_configMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
|
|
||||||
|
|
||||||
return _configCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getStates() {
|
|
||||||
_statesCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_statesMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
|
|
||||||
|
|
||||||
return _statesCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getLovelace() {
|
|
||||||
_lovelaceCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_lovelaceMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false);
|
|
||||||
|
|
||||||
return _lovelaceCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getUserInfo() {
|
|
||||||
_userInfoCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_userInfoMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
|
|
||||||
|
|
||||||
return _userInfoCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getServices() {
|
|
||||||
_servicesCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_servicesMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
|
|
||||||
|
|
||||||
return _servicesCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
_incrementMessageId() {
|
|
||||||
_currentMessageId += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendAuthMessageRaw(String message) {
|
|
||||||
Logger.d( "[Sending] ==> auth request");
|
|
||||||
_hassioChannel.sink.add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendMessageRaw(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) {
|
}).catchError((e) {
|
||||||
sendCompleter.completeError(e);
|
_fetchCompleter.completeError(e);
|
||||||
});
|
});
|
||||||
return sendCompleter.future;
|
return _fetchCompleter.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
|
Future logout() async {
|
||||||
_incrementMessageId();
|
Logger.d("Logging out...");
|
||||||
String message = "";
|
await connection.logout().then((_) {
|
||||||
if (entityId != null) {
|
ui?.clear();
|
||||||
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
|
entities?.clear();
|
||||||
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 += '}';
|
Future _getConfig() async {
|
||||||
|
await connection.sendSocketMessage(type: "get_config").then((data) {
|
||||||
|
_instanceConfig = Map.from(data);
|
||||||
|
}).catchError((e) {
|
||||||
|
throw {"errorCode": 1, "errorMessage": "Error getting config: $e"};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
message += '}';
|
|
||||||
|
Future _getStates() async {
|
||||||
|
await connection.sendSocketMessage(type: "get_states").then(
|
||||||
|
(data) => entities.parse(data)
|
||||||
|
).catchError((e) {
|
||||||
|
throw {"errorCode": 1, "errorMessage": "Error getting states: $e"};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return _sendMessageRaw(message, true);
|
|
||||||
|
Future _getLovelace() async {
|
||||||
|
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 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 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 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"]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleEntityStateChange(Map eventData) {
|
void _handleEntityStateChange(Map eventData) {
|
||||||
@ -371,39 +155,6 @@ class HomeAssistant {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseConfig(Map data) {
|
|
||||||
if (data["success"] == true) {
|
|
||||||
_instanceConfig = Map.from(data["result"]);
|
|
||||||
_configCompleter.complete();
|
|
||||||
} else {
|
|
||||||
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseUserInfo(Map data) {
|
|
||||||
if (data["success"] == true) {
|
|
||||||
_userName = data["result"]["name"];
|
|
||||||
} else {
|
|
||||||
_userName = null;
|
|
||||||
Logger.w("There was an error getting current user: $data");
|
|
||||||
}
|
|
||||||
_userInfoCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseServices(response) {
|
|
||||||
_servicesCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleLovelace(response) {
|
|
||||||
if (response["success"] == true) {
|
|
||||||
_rawLovelaceData = response["result"];
|
|
||||||
} else {
|
|
||||||
Logger.e("There was an error getting Lovelace config: $response");
|
|
||||||
_rawLovelaceData = null;
|
|
||||||
}
|
|
||||||
_lovelaceCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseLovelace() {
|
void _parseLovelace() {
|
||||||
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
||||||
ui.title = _rawLovelaceData["title"];
|
ui.title = _rawLovelaceData["title"];
|
||||||
@ -467,9 +218,51 @@ class HomeAssistant {
|
|||||||
if (rawEntity is String) {
|
if (rawEntity is String) {
|
||||||
if (entities.isExist(rawEntity)) {
|
if (entities.isExist(rawEntity)) {
|
||||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||||
|
} else {
|
||||||
|
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (entities.isExist(rawEntity["entity"])) {
|
if (rawEntity["type"] == "divider") {
|
||||||
|
card.entities.add(EntityWrapper(entity: Entity.divider()));
|
||||||
|
} else if (rawEntity["type"] == "section") {
|
||||||
|
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
||||||
|
} else if (rawEntity["type"] == "call-service") {
|
||||||
|
Map uiActionData = {
|
||||||
|
"tap_action": {
|
||||||
|
"action": EntityUIAction.callService,
|
||||||
|
"service": rawEntity["service"],
|
||||||
|
"service_data": rawEntity["service_data"]
|
||||||
|
},
|
||||||
|
"hold_action": EntityUIAction.none
|
||||||
|
};
|
||||||
|
card.entities.add(EntityWrapper(
|
||||||
|
entity: Entity.callService(
|
||||||
|
icon: rawEntity["icon"],
|
||||||
|
name: rawEntity["name"],
|
||||||
|
service: rawEntity["service"],
|
||||||
|
actionName: rawEntity["action_name"]
|
||||||
|
),
|
||||||
|
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (rawEntity["type"] == "weblink") {
|
||||||
|
Map uiActionData = {
|
||||||
|
"tap_action": {
|
||||||
|
"action": EntityUIAction.navigate,
|
||||||
|
"service": rawEntity["url"]
|
||||||
|
},
|
||||||
|
"hold_action": EntityUIAction.none
|
||||||
|
};
|
||||||
|
card.entities.add(EntityWrapper(
|
||||||
|
entity: Entity.weblink(
|
||||||
|
icon: rawEntity["icon"],
|
||||||
|
name: rawEntity["name"],
|
||||||
|
url: rawEntity["url"]
|
||||||
|
),
|
||||||
|
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (entities.isExist(rawEntity["entity"])) {
|
||||||
Entity e = entities.get(rawEntity["entity"]);
|
Entity e = entities.get(rawEntity["entity"]);
|
||||||
card.entities.add(
|
card.entities.add(
|
||||||
EntityWrapper(
|
EntityWrapper(
|
||||||
@ -479,6 +272,8 @@ class HomeAssistant {
|
|||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -493,6 +288,8 @@ class HomeAssistant {
|
|||||||
displayName: rawCard["name"],
|
displayName: rawCard["name"],
|
||||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (entities.isExist(en["entity"])) {
|
if (entities.isExist(en["entity"])) {
|
||||||
@ -503,6 +300,8 @@ class HomeAssistant {
|
|||||||
displayName: en["name"],
|
displayName: en["name"],
|
||||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -514,15 +313,6 @@ class HomeAssistant {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseEntities(response) async {
|
|
||||||
if (response["success"] == false) {
|
|
||||||
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entities.parse(response["result"]);
|
|
||||||
_statesCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _createUI() {
|
void _createUI() {
|
||||||
ui = HomeAssistantUI();
|
ui = HomeAssistantUI();
|
||||||
if ((_useLovelace) && (_rawLovelaceData != null)) {
|
if ((_useLovelace) && (_rawLovelaceData != null)) {
|
||||||
@ -559,31 +349,12 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildViews(BuildContext context, bool lovelace) {
|
Widget buildViews(BuildContext context, TabController tabController) {
|
||||||
return ui.build(context);
|
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 {
|
class SendMessageQueue {
|
||||||
int _messageTimeout;
|
int _messageTimeout;
|
||||||
List<HAMessage> _queue = [];
|
List<HAMessage> _queue = [];
|
||||||
@ -622,4 +393,4 @@ class HAMessage {
|
|||||||
bool isExpired() {
|
bool isExpired() {
|
||||||
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
399
lib/main.dart
@ -8,7 +8,7 @@ import 'package:web_socket_channel/io.dart';
|
|||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:date_format/date_format.dart';
|
import 'package:date_format/date_format.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
@ -16,10 +16,15 @@ import 'package:charts_flutter/flutter.dart' as charts;
|
|||||||
import 'package:progress_indicators/progress_indicators.dart';
|
import 'package:progress_indicators/progress_indicators.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.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/const.dart';
|
||||||
part 'entity_class/entity.class.dart';
|
part 'entity_class/entity.class.dart';
|
||||||
part 'entity_class/entity_wrapper.class.dart';
|
part 'entity_class/entity_wrapper.class.dart';
|
||||||
|
part 'entity_class/timer_entity.dart';
|
||||||
part 'entity_class/switch_entity.class.dart';
|
part 'entity_class/switch_entity.class.dart';
|
||||||
part 'entity_class/button_entity.class.dart';
|
part 'entity_class/button_entity.class.dart';
|
||||||
part 'entity_class/text_entity.class.dart';
|
part 'entity_class/text_entity.class.dart';
|
||||||
@ -40,6 +45,7 @@ part 'entity_class/alarm_control_panel.class.dart';
|
|||||||
part 'entity_widgets/common/badge.dart';
|
part 'entity_widgets/common/badge.dart';
|
||||||
part 'entity_widgets/model_widgets.dart';
|
part 'entity_widgets/model_widgets.dart';
|
||||||
part 'entity_widgets/default_entity_container.dart';
|
part 'entity_widgets/default_entity_container.dart';
|
||||||
|
part 'entity_widgets/missed_entity.dart';
|
||||||
part 'entity_widgets/glance_entity_container.dart';
|
part 'entity_widgets/glance_entity_container.dart';
|
||||||
part 'entity_widgets/button_entity_container.dart';
|
part 'entity_widgets/button_entity_container.dart';
|
||||||
part 'entity_widgets/common/entity_attributes_list.dart';
|
part 'entity_widgets/common/entity_attributes_list.dart';
|
||||||
@ -65,6 +71,7 @@ part 'entity_widgets/controls/slider_controls.dart';
|
|||||||
part 'entity_widgets/state/text_input_state.dart';
|
part 'entity_widgets/state/text_input_state.dart';
|
||||||
part 'entity_widgets/state/select_state.dart';
|
part 'entity_widgets/state/select_state.dart';
|
||||||
part 'entity_widgets/state/simple_state.dart';
|
part 'entity_widgets/state/simple_state.dart';
|
||||||
|
part 'entity_widgets/state/timer_state.dart';
|
||||||
part 'entity_widgets/state/climate_state.dart';
|
part 'entity_widgets/state/climate_state.dart';
|
||||||
part 'entity_widgets/state/cover_state.dart';
|
part 'entity_widgets/state/cover_state.dart';
|
||||||
part 'entity_widgets/state/date_time_state.dart';
|
part 'entity_widgets/state/date_time_state.dart';
|
||||||
@ -76,27 +83,29 @@ part 'entity_widgets/controls/media_player_widgets.dart';
|
|||||||
part 'entity_widgets/controls/fan_controls.dart';
|
part 'entity_widgets/controls/fan_controls.dart';
|
||||||
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
|
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
|
||||||
part 'settings.page.dart';
|
part 'settings.page.dart';
|
||||||
part 'configuration.page.dart';
|
part 'panel.page.dart';
|
||||||
part 'home_assistant.class.dart';
|
part 'home_assistant.class.dart';
|
||||||
part 'log.page.dart';
|
part 'log.page.dart';
|
||||||
part 'entity.page.dart';
|
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';
|
||||||
part 'ui_class/sizes_class.dart';
|
part 'ui_class/sizes_class.dart';
|
||||||
|
part 'ui_class/panel_class.dart';
|
||||||
part 'ui_widgets/view.dart';
|
part 'ui_widgets/view.dart';
|
||||||
part 'ui_widgets/card_widget.dart';
|
part 'ui_widgets/card_widget.dart';
|
||||||
part 'ui_widgets/card_header_widget.dart';
|
part 'ui_widgets/card_header_widget.dart';
|
||||||
|
part 'ui_widgets/config_panel_widget.dart';
|
||||||
|
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersion = "0.4.4";
|
const appVersion = "0.6.0-alpha2";
|
||||||
|
|
||||||
String homeAssistantWebHost;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
FlutterError.onError = (errorDetails) {
|
FlutterError.onError = (errorDetails) {
|
||||||
@ -118,6 +127,8 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class HAClientApp extends StatelessWidget {
|
class HAClientApp extends StatelessWidget {
|
||||||
|
|
||||||
|
final HomeAssistant homeAssistant = HomeAssistant();
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -128,9 +139,9 @@ class HAClientApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
initialRoute: "/",
|
initialRoute: "/",
|
||||||
routes: {
|
routes: {
|
||||||
"/": (context) => MainPage(title: 'HA Client'),
|
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
|
||||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||||
"/configuration": (context) => ConfigurationPage(title: "Configuration"),
|
"/configuration": (context) => PanelPage(title: "Configuration"),
|
||||||
"/log-view": (context) => LogViewPage(title: "Log")
|
"/log-view": (context) => LogViewPage(title: "Log")
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -138,81 +149,84 @@ class HAClientApp extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MainPage extends StatefulWidget {
|
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 String title;
|
||||||
|
final HomeAssistant homeAssistant;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MainPageState createState() => new _MainPageState();
|
_MainPageState createState() => new _MainPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||||
HomeAssistant _homeAssistant;
|
|
||||||
//Map _instanceConfig;
|
|
||||||
String _webSocketApiEndpoint;
|
|
||||||
String _password;
|
|
||||||
//int _uiViewsCount = 0;
|
|
||||||
String _instanceHost;
|
|
||||||
StreamSubscription _stateSubscription;
|
StreamSubscription _stateSubscription;
|
||||||
StreamSubscription _settingsSubscription;
|
StreamSubscription _settingsSubscription;
|
||||||
StreamSubscription _serviceCallSubscription;
|
StreamSubscription _serviceCallSubscription;
|
||||||
StreamSubscription _showEntityPageSubscription;
|
StreamSubscription _showEntityPageSubscription;
|
||||||
StreamSubscription _showErrorSubscription;
|
StreamSubscription _showErrorSubscription;
|
||||||
bool _settingsLoaded = false;
|
StreamSubscription _startAuthSubscription;
|
||||||
bool _accountMenuExpanded = false;
|
StreamSubscription _reloadUISubscription;
|
||||||
bool _useLovelaceUI;
|
int _previousViewCount;
|
||||||
|
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_settingsLoaded = false;
|
//widget.homeAssistant = HomeAssistant();
|
||||||
|
//_settingsLoaded = false;
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
Logger.d("<!!!> Creating new HomeAssistant instance");
|
|
||||||
_homeAssistant = HomeAssistant();
|
|
||||||
|
|
||||||
_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) {
|
||||||
_homeAssistant.disconnect().then((_){
|
_reLoad();
|
||||||
_initialLoad();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_initialLoad();
|
_initialLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initialLoad() {
|
void _initialLoad() {
|
||||||
_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 && _settingsLoaded) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
_refreshData();
|
_reLoad();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,12 +235,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
_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(() {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (_reloadUISubscription == null) {
|
||||||
|
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
||||||
|
_reLoad();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (_serviceCallSubscription == null) {
|
if (_serviceCallSubscription == null) {
|
||||||
_serviceCallSubscription =
|
_serviceCallSubscription =
|
||||||
eventBus.on<ServiceCallEvent>().listen((event) {
|
eventBus.on<ServiceCallEvent>().listen((event) {
|
||||||
@ -247,18 +266,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
|
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_startAuthSubscription == null) {
|
||||||
|
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
|
||||||
|
_showOAuth();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshData() async {
|
|
||||||
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
|
|
||||||
_hideBottomBar();
|
/*_firebaseMessaging.getToken().then((String token) {
|
||||||
_showInfoBottomBar(progress: true,);
|
//Logger.d("FCM token: $token");
|
||||||
await _homeAssistant.fetch().then((result) {
|
widget.homeAssistant.sendHTTPPost(
|
||||||
_hideBottomBar();
|
endPoint: '/api/notify.fcm-android',
|
||||||
}).catchError((e) {
|
jsonData: '{"token": "$token"}'
|
||||||
_setErrorState(e);
|
);
|
||||||
});
|
});
|
||||||
eventBus.fire(RefreshDataFinishedEvent());
|
_firebaseMessaging.configure(
|
||||||
|
onLaunch: (data) {
|
||||||
|
Logger.d("Notification [onLaunch]: $data");
|
||||||
|
},
|
||||||
|
onMessage: (data) {
|
||||||
|
Logger.d("Notification [onMessage]: $data");
|
||||||
|
},
|
||||||
|
onResume: (data) {
|
||||||
|
Logger.d("Notification [onResume]: $data");
|
||||||
|
}
|
||||||
|
);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
_setErrorState(e) {
|
||||||
@ -266,7 +318,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
Logger.e(e.toString());
|
Logger.e(e.toString());
|
||||||
Logger.e("${e.stackTrace}");
|
Logger.e("${e.stackTrace}");
|
||||||
_showErrorBottomBar(
|
_showErrorBottomBar(
|
||||||
message: "There was some error",
|
message: "Unknown error",
|
||||||
errorCode: 13
|
errorCode: 13
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -277,19 +329,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
);
|
);
|
||||||
_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) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant),
|
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -297,8 +349,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
List<Tab> buildUIViewTabs() {
|
List<Tab> buildUIViewTabs() {
|
||||||
List<Tab> result = [];
|
List<Tab> result = [];
|
||||||
|
|
||||||
if (_homeAssistant.ui.views.isNotEmpty) {
|
if (widget.homeAssistant.ui.views.isNotEmpty) {
|
||||||
_homeAssistant.ui.views.forEach((HAView view) {
|
widget.homeAssistant.ui.views.forEach((HAView view) {
|
||||||
result.add(view.buildTab());
|
result.add(view.buildTab());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -310,16 +362,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
List<Widget> menuItems = [];
|
List<Widget> menuItems = [];
|
||||||
menuItems.add(
|
menuItems.add(
|
||||||
UserAccountsDrawerHeader(
|
UserAccountsDrawerHeader(
|
||||||
accountName: Text(_homeAssistant.userName),
|
accountName: Text(widget.homeAssistant.userName),
|
||||||
accountEmail: Text(_instanceHost ?? "Not configured"),
|
accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"),
|
||||||
onDetailsPressed: () {
|
/*onDetailsPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_accountMenuExpanded = !_accountMenuExpanded;
|
_accountMenuExpanded = !_accountMenuExpanded;
|
||||||
});
|
});
|
||||||
},
|
},*/
|
||||||
currentAccountPicture: CircleAvatar(
|
currentAccountPicture: CircleAvatar(
|
||||||
child: Text(
|
child: Text(
|
||||||
_homeAssistant.userAvatarText,
|
widget.homeAssistant.userAvatarText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 32.0
|
fontSize: 32.0
|
||||||
),
|
),
|
||||||
@ -327,28 +379,40 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (_accountMenuExpanded) {
|
if (widget.homeAssistant.panels.isNotEmpty) {
|
||||||
menuItems.addAll([
|
widget.homeAssistant.panels.forEach((Panel panel) {
|
||||||
ListTile(
|
if (!panel.isHidden) {
|
||||||
leading: Icon(Icons.settings),
|
menuItems.add(
|
||||||
title: Text("Settings"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.of(context).pushNamed('/connection-settings');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
menuItems.addAll([
|
|
||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(Icons.settings),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
||||||
title: Text("Configuration"),
|
title: Text("${panel.title}"),
|
||||||
|
onTap: () => panel.handleOpen(context)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//TODO check for loaded
|
||||||
|
menuItems.add(
|
||||||
|
new ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
|
||||||
|
title: Text("Open Web UI"),
|
||||||
|
onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
menuItems.addAll([
|
||||||
|
Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
|
||||||
|
title: Text("Connection settings"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pushNamed('/configuration');
|
Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
|
]);
|
||||||
|
menuItems.addAll([
|
||||||
|
Divider(),
|
||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(Icons.insert_drive_file),
|
leading: Icon(Icons.insert_drive_file),
|
||||||
title: Text("Log"),
|
title: Text("Log"),
|
||||||
@ -358,7 +422,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
|
||||||
title: Text("Report an issue"),
|
title: Text("Report an issue"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@ -366,6 +430,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
Divider(),
|
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(
|
new AboutListTile(
|
||||||
aboutBoxChildren: <Widget>[
|
aboutBoxChildren: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
@ -380,13 +452,44 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 10.0,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/terms_and_conditions");
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Terms and Conditions",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 10.0,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/privacy_policy");
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Privacy Policy",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
applicationName: appName,
|
applicationName: appName,
|
||||||
applicationVersion: appVersion
|
applicationVersion: appVersion
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
return new Drawer(
|
return new Drawer(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: menuItems,
|
children: menuItems,
|
||||||
@ -439,7 +542,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
child: Text("Retry", style: textStyle),
|
child: Text("Retry", style: textStyle),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||||
_refreshData();
|
_reLoad();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -457,12 +560,32 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 6: {
|
case 60: {
|
||||||
_bottomBarAction = FlatButton(
|
_bottomBarAction = FlatButton(
|
||||||
child: Text("Settings", style: textStyle),
|
child: Text("Login", style: textStyle),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
_reLoad();
|
||||||
Navigator.pushNamed(context, '/connection-settings');
|
},
|
||||||
|
);
|
||||||
|
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;
|
break;
|
||||||
@ -473,52 +596,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_bottomBarProgress = false;
|
_bottomBarProgress = false;
|
||||||
_bottomBarText = "$message (code: $errorCode)";
|
_bottomBarText = "$message";
|
||||||
_showBottomBar = true;
|
_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>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
Widget _buildScaffoldBody(bool empty) {
|
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(
|
return NestedScrollView(
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
@ -526,22 +648,23 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
floating: true,
|
floating: true,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
primary: true,
|
primary: true,
|
||||||
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
|
title: Text(widget.homeAssistant.locationName ?? ""),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:dots-vertical"), color: Colors.white,),
|
"mdi:dots-vertical"), color: Colors.white,),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showMenu(
|
showMenu(
|
||||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
||||||
context: context,
|
context: context,
|
||||||
items: [PopupMenuItem<String>(
|
items: popupMenuItems
|
||||||
child: new Text("Reload"),
|
|
||||||
value: "reload",
|
|
||||||
)]
|
|
||||||
).then((String val) {
|
).then((String val) {
|
||||||
if (val == "reload") {
|
if (val == "reload") {
|
||||||
_refreshData();
|
_reLoad();
|
||||||
|
} else if (val == "logout") {
|
||||||
|
widget.homeAssistant.logout().then((_) {
|
||||||
|
_reLoad();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -551,12 +674,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
icon: Icon(Icons.menu),
|
icon: Icon(Icons.menu),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_scaffoldKey.currentState.openDrawer();
|
_scaffoldKey.currentState.openDrawer();
|
||||||
setState(() {
|
|
||||||
_accountMenuExpanded = false;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
bottom: empty ? null : TabBar(
|
bottom: empty ? null : TabBar(
|
||||||
|
controller: _viewsTabController,
|
||||||
tabs: buildUIViewTabs(),
|
tabs: buildUIViewTabs(),
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
),
|
),
|
||||||
@ -570,18 +691,20 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"),
|
||||||
size: 100.0,
|
size: 100.0,
|
||||||
color: Colors.blue,
|
color: Colors.black26,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
:
|
:
|
||||||
_homeAssistant.buildViews(context, _useLovelaceUI),
|
widget.homeAssistant.buildViews(context, _viewsTabController),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TabController _viewsTabController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget bottomBar;
|
Widget bottomBar;
|
||||||
@ -633,7 +756,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This method is rerun every time setState is called.
|
// This method is rerun every time setState is called.
|
||||||
if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) {
|
if (widget.homeAssistant.isNoViews) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
primary: false,
|
primary: false,
|
||||||
@ -647,9 +770,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
drawer: _buildAppDrawer(),
|
drawer: _buildAppDrawer(),
|
||||||
primary: false,
|
primary: false,
|
||||||
bottomNavigationBar: bottomBar,
|
bottomNavigationBar: bottomBar,
|
||||||
body: DefaultTabController(
|
body: HomeAssistantModel(
|
||||||
length: _homeAssistant.ui?.views?.length ?? 0,
|
|
||||||
child: _buildScaffoldBody(false),
|
child: _buildScaffoldBody(false),
|
||||||
|
homeAssistant: widget.homeAssistant
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -657,13 +780,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||||
|
flutterWebviewPlugin.dispose();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
_viewsTabController?.dispose();
|
||||||
if (_settingsSubscription != null) _settingsSubscription.cancel();
|
_stateSubscription?.cancel();
|
||||||
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
|
_settingsSubscription?.cancel();
|
||||||
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
|
_serviceCallSubscription?.cancel();
|
||||||
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
|
_showEntityPageSubscription?.cancel();
|
||||||
_homeAssistant.disconnect();
|
_showErrorSubscription?.cancel();
|
||||||
|
_startAuthSubscription?.cancel();
|
||||||
|
_reloadUISubscription?.cancel();
|
||||||
|
//TODO disconnect
|
||||||
|
//widget.homeAssistant?.disconnect();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
lib/panel.page.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class PanelPage extends StatefulWidget {
|
||||||
|
PanelPage({Key key, this.title, this.panel}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final Panel panel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PanelPageState createState() => new _PanelPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PanelPageState extends State<PanelPage> {
|
||||||
|
|
||||||
|
List<ConfigurationItem> _items;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
title: new Text(widget.title),
|
||||||
|
),
|
||||||
|
body: widget.panel.getWidget(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -14,17 +14,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
String _newHassioDomain = "";
|
String _newHassioDomain = "";
|
||||||
String _hassioPort = "";
|
String _hassioPort = "";
|
||||||
String _newHassioPort = "";
|
String _newHassioPort = "";
|
||||||
String _hassioPassword = "";
|
|
||||||
String _newHassioPassword = "";
|
|
||||||
String _socketProtocol = "wss";
|
String _socketProtocol = "wss";
|
||||||
String _newSocketProtocol = "wss";
|
String _newSocketProtocol = "wss";
|
||||||
bool _useLovelace = true;
|
bool _useLovelace = true;
|
||||||
bool _newUseLovelace = true;
|
bool _newUseLovelace = true;
|
||||||
|
|
||||||
|
String oauthUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadSettings();
|
_loadSettings();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadSettings() async {
|
_loadSettings() async {
|
||||||
@ -33,7 +34,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
||||||
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
||||||
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
|
|
||||||
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
||||||
try {
|
try {
|
||||||
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
|
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
|
||||||
@ -44,7 +44,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _checkConfigChanged() {
|
bool _checkConfigChanged() {
|
||||||
return ((_newHassioPassword != _hassioPassword) ||
|
return (
|
||||||
(_newHassioPort != _hassioPort) ||
|
(_newHassioPort != _hassioPort) ||
|
||||||
(_newHassioDomain != _hassioDomain) ||
|
(_newHassioDomain != _hassioDomain) ||
|
||||||
(_newSocketProtocol != _socketProtocol) ||
|
(_newSocketProtocol != _socketProtocol) ||
|
||||||
@ -59,7 +59,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
prefs.setString("hassio-domain", _newHassioDomain);
|
prefs.setString("hassio-domain", _newHassioDomain);
|
||||||
prefs.setString("hassio-port", _newHassioPort);
|
prefs.setString("hassio-port", _newHassioPort);
|
||||||
prefs.setString("hassio-password", _newHassioPassword);
|
|
||||||
prefs.setString("hassio-protocol", _newSocketProtocol);
|
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||||
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||||
prefs.setBool("use-lovelace", _newUseLovelace);
|
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.",
|
"Try ports 80 and 443 if default is not working and you don't know why.",
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
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(
|
||||||
padding: EdgeInsets.only(top: 20.0),
|
padding: EdgeInsets.only(top: 20.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
58
lib/ui_class/panel_class.dart
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class Panel {
|
||||||
|
|
||||||
|
static const iconsByComponent = {
|
||||||
|
"config": "mdi:settings",
|
||||||
|
"history": "mdi:poll-box",
|
||||||
|
"map": "mdi:tooltip-account",
|
||||||
|
"logbook": "mdi:format-list-bulleted-type",
|
||||||
|
"custom": "mdi:home-assistant"
|
||||||
|
};
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String type;
|
||||||
|
final String title;
|
||||||
|
final String urlPath;
|
||||||
|
final Map config;
|
||||||
|
String icon;
|
||||||
|
bool isHidden = true;
|
||||||
|
|
||||||
|
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
||||||
|
if (icon == null || !icon.startsWith("mdi:")) {
|
||||||
|
icon = Panel.iconsByComponent[type];
|
||||||
|
}
|
||||||
|
isHidden = (type != "iframe" && type != "config");
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleOpen(BuildContext context) {
|
||||||
|
if (type == "iframe") {
|
||||||
|
Logger.d("Launching custom tab with ${config["url"]}");
|
||||||
|
HAUtils.launchURLInCustomTab(context, config["url"]);
|
||||||
|
} else if (type == "config") {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PanelPage(title: "$title", panel: this),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
HomeAssistantModel haModel = HomeAssistantModel.of(context);
|
||||||
|
String url = "${haModel.homeAssistant.connection.httpWebHost}/$urlPath";
|
||||||
|
Logger.d("Launching custom tab with $url");
|
||||||
|
HAUtils.launchURLInCustomTab(context, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget getWidget() {
|
||||||
|
switch (type) {
|
||||||
|
case "config": {
|
||||||
|
return ConfigPanelWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return Text("Unsupported panel component: $type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,12 +4,15 @@ class HomeAssistantUI {
|
|||||||
List<HAView> views;
|
List<HAView> views;
|
||||||
String title;
|
String title;
|
||||||
|
|
||||||
|
bool get isEmpty => views == null || views.isEmpty;
|
||||||
|
|
||||||
HomeAssistantUI() {
|
HomeAssistantUI() {
|
||||||
views = [];
|
views = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, TabController tabController) {
|
||||||
return TabBarView(
|
return TabBarView(
|
||||||
|
controller: tabController,
|
||||||
children: _buildViews(context)
|
children: _buildViews(context)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -24,4 +27,8 @@ class HomeAssistantUI {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
views.clear();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -77,7 +77,7 @@ class HAView {
|
|||||||
Tab(
|
Tab(
|
||||||
icon:
|
icon:
|
||||||
Icon(
|
Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName(
|
MaterialDesignIcons.getIconDataFromIconName(
|
||||||
iconName ?? "mdi:home-assistant"),
|
iconName ?? "mdi:home-assistant"),
|
||||||
size: 24.0,
|
size: 24.0,
|
||||||
)
|
)
|
||||||
@ -92,7 +92,7 @@ class HAView {
|
|||||||
if (linkedEntity.icon != null && linkedEntity.icon.length > 0) {
|
if (linkedEntity.icon != null && linkedEntity.icon.length > 0) {
|
||||||
return Tab(
|
return Tab(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
MaterialDesignIcons.createIconDataFromIconName(
|
MaterialDesignIcons.getIconDataFromIconName(
|
||||||
linkedEntity.icon),
|
linkedEntity.icon),
|
||||||
size: 24.0,
|
size: 24.0,
|
||||||
)
|
)
|
||||||
|
@ -11,9 +11,18 @@ class CardWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if ((card.linkedEntityWrapper!= null) && (card.linkedEntityWrapper.entity.isHidden)) {
|
if (card.linkedEntityWrapper!= null) {
|
||||||
|
if (card.linkedEntityWrapper.entity.isHidden) {
|
||||||
return Container(width: 0.0, height: 0.0,);
|
return Container(width: 0.0, height: 0.0,);
|
||||||
}
|
}
|
||||||
|
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: MissedEntityWidget(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (card.type) {
|
switch (card.type) {
|
||||||
|
|
||||||
@ -103,7 +112,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
if (!entity.entity.isHidden) {
|
if (!entity.entity.isHidden) {
|
||||||
body.add(
|
body.add(
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
padding: EdgeInsets.fromLTRB(10.0, 4.0, 0.0, 4.0),
|
||||||
child: EntityModel(
|
child: EntityModel(
|
||||||
entityWrapper: entity,
|
entityWrapper: entity,
|
||||||
handleTap: true,
|
handleTap: true,
|
||||||
@ -133,9 +142,6 @@ class CardWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAlarmPanelCard(BuildContext context) {
|
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||||
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) {
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
} else {
|
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeaderWidget(
|
body.add(CardHeaderWidget(
|
||||||
name: card.name ?? "",
|
name: card.name ?? "",
|
||||||
@ -149,14 +155,14 @@ class CardWidget extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
EntityIcon(
|
EntityIcon(
|
||||||
iconSize: 50.0,
|
size: 50.0,
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
width: 26.0,
|
width: 26.0,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:dots-vertical")),
|
"mdi:dots-vertical")),
|
||||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
|
||||||
)
|
)
|
||||||
@ -182,7 +188,6 @@ class CardWidget extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildGlanceCard(BuildContext context) {
|
Widget _buildGlanceCard(BuildContext context) {
|
||||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||||
@ -227,9 +232,6 @@ class CardWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMediaControlsCard(BuildContext context) {
|
Widget _buildMediaControlsCard(BuildContext context) {
|
||||||
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) {
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
} else {
|
|
||||||
return Card(
|
return Card(
|
||||||
child: EntityModel(
|
child: EntityModel(
|
||||||
entityWrapper: card.linkedEntityWrapper,
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
@ -238,12 +240,8 @@ class CardWidget extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntityButtonCard(BuildContext context) {
|
Widget _buildEntityButtonCard(BuildContext context) {
|
||||||
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) {
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
} else {
|
|
||||||
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
||||||
card.linkedEntityWrapper.displayName.toUpperCase();
|
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||||
return Card(
|
return Card(
|
||||||
@ -254,7 +252,6 @@ class CardWidget extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildUnsupportedCard(BuildContext context) {
|
Widget _buildUnsupportedCard(BuildContext context) {
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
part of 'main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class ConfigurationPage extends StatefulWidget {
|
class ConfigPanelWidget extends StatefulWidget {
|
||||||
ConfigurationPage({Key key, this.title}) : super(key: key);
|
ConfigPanelWidget({Key key}) : super(key: key);
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ConfigurationPageState createState() => new _ConfigurationPageState();
|
_ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConfigurationItem {
|
class ConfigurationItem {
|
||||||
@ -17,7 +15,7 @@ class ConfigurationItem {
|
|||||||
final Widget body;
|
final Widget body;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ConfigurationPageState extends State<ConfigurationPage> {
|
class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
||||||
|
|
||||||
List<ConfigurationItem> _items;
|
List<ConfigurationItem> _items;
|
||||||
|
|
||||||
@ -64,14 +62,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
return new Scaffold(
|
return ListView(
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
|
||||||
Navigator.pop(context);
|
|
||||||
}),
|
|
||||||
title: new Text(widget.title),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
children: [
|
children: [
|
||||||
new ExpansionPanelList(
|
new ExpansionPanelList(
|
||||||
expansionCallback: (int index, bool isExpanded) {
|
expansionCallback: (int index, bool isExpanded) {
|
||||||
@ -94,7 +85,6 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
|
|||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -47,12 +47,44 @@ class Logger {
|
|||||||
|
|
||||||
class HAUtils {
|
class HAUtils {
|
||||||
static void launchURL(String url) async {
|
static void launchURL(String url) async {
|
||||||
if (await canLaunch(url)) {
|
if (await urlLauncher.canLaunch(url)) {
|
||||||
await launch(url);
|
await urlLauncher.launch(url);
|
||||||
} else {
|
} else {
|
||||||
Logger.e( "Could not launch $url");
|
Logger.e( "Could not launch $url");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void launchURLInCustomTab(BuildContext context, String url) async {
|
||||||
|
try {
|
||||||
|
await launch(
|
||||||
|
"$url",
|
||||||
|
option: new CustomTabsOption(
|
||||||
|
toolbarColor: Theme.of(context).primaryColor,
|
||||||
|
enableDefaultShare: true,
|
||||||
|
enableUrlBarHiding: true,
|
||||||
|
showPageTitle: true,
|
||||||
|
animation: new CustomTabsAnimation.slideIn()
|
||||||
|
// or user defined animation.
|
||||||
|
/*animation: new CustomTabsAnimation(
|
||||||
|
startEnter: 'slide_up',
|
||||||
|
startExit: 'android:anim/fade_out',
|
||||||
|
endEnter: 'android:anim/fade_in',
|
||||||
|
endExit: 'slide_down',
|
||||||
|
)*/,
|
||||||
|
extraCustomTabs: <String>[
|
||||||
|
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
|
||||||
|
'org.mozilla.firefox',
|
||||||
|
// ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx
|
||||||
|
'com.microsoft.emmx',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.w("Can't open custom tab: ${e.toString()}");
|
||||||
|
Logger.w("Launching in default browser");
|
||||||
|
HAUtils.launchURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StateChangedEvent {
|
class StateChangedEvent {
|
||||||
@ -77,6 +109,16 @@ class RefreshDataFinishedEvent {
|
|||||||
RefreshDataFinishedEvent();
|
RefreshDataFinishedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ReloadUIEvent {
|
||||||
|
ReloadUIEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class StartAuthEvent {
|
||||||
|
String oauthUrl;
|
||||||
|
|
||||||
|
StartAuthEvent(this.oauthUrl);
|
||||||
|
}
|
||||||
|
|
||||||
class ServiceCallEvent {
|
class ServiceCallEvent {
|
||||||
String domain;
|
String domain;
|
||||||
String service;
|
String service;
|
||||||
|
64
pubspec.lock
@ -35,7 +35,7 @@ packages:
|
|||||||
name: cached_network_image
|
name: cached_network_image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0+1"
|
version: "0.7.0"
|
||||||
charcode:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -101,6 +101,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -112,7 +119,14 @@ packages:
|
|||||||
name: flutter_cache_manager
|
name: flutter_cache_manager
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.3.2"
|
||||||
|
flutter_custom_tabs:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_custom_tabs
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -127,18 +141,32 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
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:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.3"
|
version: "0.10.4"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -223,6 +251,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0+1"
|
version: "0.5.0+1"
|
||||||
|
pedantic:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pedantic
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -230,6 +265,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
progress_indicators:
|
progress_indicators:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -250,7 +292,7 @@ packages:
|
|||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1+1"
|
version: "0.5.1+2"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -262,14 +304,14 @@ packages:
|
|||||||
name: source_span
|
name: source_span
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.5.4"
|
||||||
sqflite:
|
sqflite:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite
|
name: sqflite
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0+1"
|
version: "1.1.3"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -297,21 +339,21 @@ packages:
|
|||||||
name: synchronized
|
name: synchronized
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2+1"
|
version: "2.1.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: term_glyph
|
name: term_glyph
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.1.0"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1"
|
version: "0.2.2"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -325,7 +367,7 @@ packages:
|
|||||||
name: url_launcher
|
name: url_launcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.1"
|
version: "5.0.2"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -363,4 +405,4 @@ packages:
|
|||||||
version: "2.1.15"
|
version: "2.1.15"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.1.0 <3.0.0"
|
dart: ">=2.1.0 <3.0.0"
|
||||||
flutter: ">=0.7.3 <2.0.0"
|
flutter: ">=1.2.1 <2.0.0"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.4.4+95
|
version: 0.6.0+601
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||||
@ -19,6 +19,10 @@ dependencies:
|
|||||||
charts_flutter: any
|
charts_flutter: any
|
||||||
flutter_markdown: any
|
flutter_markdown: any
|
||||||
flutter_svg: ^0.10.3
|
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -59,7 +63,7 @@ flutter:
|
|||||||
fonts:
|
fonts:
|
||||||
- family: "Material Design Icons"
|
- family: "Material Design Icons"
|
||||||
fonts:
|
fonts:
|
||||||
- asset: fonts/materialdesignicons-webfont.ttf
|
- asset: fonts/materialdesignicons-webfont-3-5-95.ttf
|
||||||
# fonts:
|
# fonts:
|
||||||
# - family: Schyler
|
# - family: Schyler
|
||||||
# fonts:
|
# fonts:
|
||||||
|