Compare commits
60 Commits
0.6.0-alph
...
0.6.1
Author | SHA1 | Date | |
---|---|---|---|
85ac746e9d | |||
8215175098 | |||
39ee8b1799 | |||
c76d3d68c8 | |||
cde257922b | |||
be0c9d3372 | |||
66cd7ea307 | |||
b704ce6984 | |||
247c856a41 | |||
9afaebfa12 | |||
929abea5d3 | |||
5c31ddd00f | |||
8f55be187d | |||
1fe82d8b0d | |||
cbc56a8105 | |||
b63cddfa46 | |||
91db82f730 | |||
0c4d1b78ff | |||
5af2fd0562 | |||
2375543ebf | |||
de187f3ed5 | |||
9266ffacf3 | |||
3c0ca5d16d | |||
caabf25260 | |||
0af2afbb80 | |||
12d226509d | |||
3417c38426 | |||
c7fc5afbb8 | |||
11f565a9dc | |||
53240faac3 | |||
95d4878785 | |||
ef15026203 | |||
ad6355503b | |||
491c2b0dc0 | |||
5b99ade088 | |||
e1d9d9f304 | |||
209ccd4f7f | |||
5a8a207f2e | |||
19c85d9c16 | |||
a916ddfa50 | |||
8c1ad9c7f9 | |||
93af1eca7e | |||
cabf836fa3 | |||
15b3d31a6f | |||
9b98689012 | |||
84ebd0c33c | |||
ccd7774931 | |||
b2773635f5 | |||
8b046b7313 | |||
885a516676 | |||
921b0e09b0 | |||
277c67fc6f | |||
2a01ff8a03 | |||
b246b7bc1d | |||
e1868b9a14 | |||
125f3ac16c | |||
be502b5668 | |||
6f33fdca9f | |||
a7cda2a35e | |||
102b10ade0 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,3 +11,4 @@ build/
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
|
pubspec.lock
|
@ -6,6 +6,7 @@
|
|||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
@ -17,6 +18,11 @@
|
|||||||
android:label="HA Client"
|
android:label="HA Client"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
|
android:value="ha_notify" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
@ -27,10 +33,10 @@
|
|||||||
<!-- This keeps the window background of the activity showing
|
<!-- This keeps the window background of the activity showing
|
||||||
until Flutter renders its first frame. It can be removed if
|
until Flutter renders its first frame. It can be removed if
|
||||||
there is no splash screen (such as the default splash screen
|
there is no splash screen (such as the default splash screen
|
||||||
defined in @style/LaunchTheme). -->
|
defined in @style/LaunchTheme).
|
||||||
<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>
|
<intent-filter>
|
||||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
BIN
android/app/src/main/res/drawable/mini_icon.png
Normal file
BIN
android/app/src/main/res/drawable/mini_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 612 B |
Binary file not shown.
Binary file not shown.
@ -10,15 +10,15 @@ class AuthManager {
|
|||||||
|
|
||||||
AuthManager._internal();
|
AuthManager._internal();
|
||||||
|
|
||||||
Future getTempToken({String httpWebHost, String oauthUrl}) {
|
Future getTempToken({String oauthUrl}) {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||||
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
||||||
|
Logger.d("Webview url changed to $url");
|
||||||
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
|
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
|
||||||
String authCode = url.split("=")[1];
|
String authCode = url.split("=")[1];
|
||||||
Logger.d("We have auth code. Getting temporary access token...");
|
Logger.d("We have auth code. Getting temporary access token...");
|
||||||
Connection().sendHTTPPost(
|
Connection().sendHTTPPost(
|
||||||
host: httpWebHost,
|
|
||||||
endPoint: "/auth/token",
|
endPoint: "/auth/token",
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
includeAuthHeader: false,
|
includeAuthHeader: false,
|
||||||
@ -27,17 +27,19 @@ class AuthManager {
|
|||||||
Logger.d("Gottemp token");
|
Logger.d("Gottemp token");
|
||||||
String tempToken = json.decode(response)['access_token'];
|
String tempToken = json.decode(response)['access_token'];
|
||||||
Logger.d("Closing webview...");
|
Logger.d("Closing webview...");
|
||||||
flutterWebviewPlugin.close();
|
//flutterWebviewPlugin.close();
|
||||||
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
completer.complete(tempToken);
|
completer.complete(tempToken);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
flutterWebviewPlugin.close();
|
//flutterWebviewPlugin.close();
|
||||||
completer.completeError({"errorCode": 61, "errorMessage": "Error getting temp token"});
|
|
||||||
Logger.e("Error getting temp token: ${e.toString()}");
|
Logger.e("Error getting temp token: ${e.toString()}");
|
||||||
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
|
completer.completeError(HAError("Error getting temp token"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Logger.d("Launching OAuth...");
|
Logger.d("Launching OAuth: $oauthUrl");
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl));
|
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,12 +10,17 @@ class Connection {
|
|||||||
|
|
||||||
Connection._internal();
|
Connection._internal();
|
||||||
|
|
||||||
|
String _domain;
|
||||||
|
String _port;
|
||||||
String displayHostname;
|
String displayHostname;
|
||||||
String _webSocketAPIEndpoint;
|
String _webSocketAPIEndpoint;
|
||||||
String httpWebHost;
|
String httpWebHost;
|
||||||
String _token;
|
String _token;
|
||||||
String _tempToken;
|
String _tempToken;
|
||||||
String oauthUrl;
|
String oauthUrl;
|
||||||
|
String webhookId;
|
||||||
|
bool useLovelace = true;
|
||||||
|
bool settingsLoaded = false;
|
||||||
bool get isAuthenticated => _token != null;
|
bool get isAuthenticated => _token != null;
|
||||||
StreamSubscription _socketSubscription;
|
StreamSubscription _socketSubscription;
|
||||||
Duration connectTimeout = Duration(seconds: 15);
|
Duration connectTimeout = Duration(seconds: 15);
|
||||||
@ -29,104 +34,158 @@ class Connection {
|
|||||||
int _currentMessageId = 0;
|
int _currentMessageId = 0;
|
||||||
Map<String, Completer> _messageResolver = {};
|
Map<String, Completer> _messageResolver = {};
|
||||||
|
|
||||||
Future init(onStateChange) async {
|
Future init({bool loadSettings, bool forceReconnect: false}) async {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
onStateChangeCallback = onStateChange;
|
bool stopInit = false;
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
if (loadSettings) {
|
||||||
String domain = prefs.getString('hassio-domain');
|
Logger.e("Loading settings...");
|
||||||
String port = prefs.getString('hassio-port');
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
displayHostname = "$domain:$port";
|
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
||||||
_webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
|
_domain = prefs.getString('hassio-domain');
|
||||||
httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
|
_port = prefs.getString('hassio-port');
|
||||||
//_token = prefs.getString('hassio-token');
|
webhookId = prefs.getString('app-webhook-id');
|
||||||
final storage = new FlutterSecureStorage();
|
displayHostname = "$_domain:$_port";
|
||||||
try {
|
_webSocketAPIEndpoint =
|
||||||
_token = await storage.read(key: "hacl_llt");
|
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||||
} catch (e) {
|
httpWebHost =
|
||||||
Logger.e("Cannt read secure storage. Need to relogin.");
|
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||||
_token = null;
|
if ((_domain == null) || (_port == null) ||
|
||||||
await storage.delete(key: "hacl_llt");
|
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||||
}
|
completer.completeError(HAError.checkConnectionSettings());
|
||||||
if ((domain == null) || (port == null) ||
|
stopInit = true;
|
||||||
(domain.length == 0) || (port.length == 0)) {
|
} else {
|
||||||
completer.completeError({"errorCode": 5, "errorMessage": "Check connection settings"});
|
//_token = prefs.getString('hassio-token');
|
||||||
|
final storage = new FlutterSecureStorage();
|
||||||
|
try {
|
||||||
|
_token = await storage.read(key: "hacl_llt");
|
||||||
|
Logger.e("Long-lived token read successful");
|
||||||
|
} catch (e) {
|
||||||
|
Logger.e("Cannt read secure storage. Need to relogin.");
|
||||||
|
_token = null;
|
||||||
|
await storage.delete(key: "hacl_llt");
|
||||||
|
}
|
||||||
|
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')}";
|
||||||
|
settingsLoaded = true;
|
||||||
|
}
|
||||||
} else {
|
} 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 ((_domain == null) || (_port == null) ||
|
||||||
|
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||||
|
completer.completeError(HAError.checkConnectionSettings());
|
||||||
|
stopInit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stopInit) {
|
||||||
if (_token == null) {
|
if (_token == null) {
|
||||||
await AuthManager().getTempToken(
|
AuthManager().getTempToken(
|
||||||
httpWebHost: httpWebHost,
|
|
||||||
oauthUrl: oauthUrl
|
oauthUrl: oauthUrl
|
||||||
).then((token) {
|
).then((token) {
|
||||||
Logger.d("Token from AuthManager recived");
|
Logger.d("Token from AuthManager recived");
|
||||||
_tempToken = token;
|
_tempToken = token;
|
||||||
|
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
||||||
|
}).catchError((e) {
|
||||||
|
completer.completeError(e);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _doConnect({Completer completer, bool forceReconnect}) {
|
||||||
|
if (forceReconnect || !isConnected) {
|
||||||
_connect().timeout(connectTimeout, onTimeout: () {
|
_connect().timeout(connectTimeout, onTimeout: () {
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
completer.completeError(
|
completer?.completeError(HAError("Connection timeout"));
|
||||||
{"errorCode": 1, "errorMessage": "Connection timeout"});
|
|
||||||
});
|
});
|
||||||
}).then((_) => completer.complete()).catchError((e) {
|
}).then((_) {
|
||||||
completer.completeError(e);
|
Logger.d("doConnect is finished 1");
|
||||||
|
completer?.complete();
|
||||||
|
}).catchError((e) {
|
||||||
|
completer?.completeError(e);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
Logger.d("doConnect is finished 2");
|
||||||
|
completer?.complete();
|
||||||
}
|
}
|
||||||
return completer.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Completer connecting;
|
Completer connecting;
|
||||||
|
|
||||||
Future _connect() async {
|
Future _connect() {
|
||||||
if (connecting != null && !connecting.isCompleted) {
|
if (connecting != null && !connecting.isCompleted) {
|
||||||
Logger.w("");
|
Logger.w("Previous connection attempt pending...");
|
||||||
|
return connecting.future;
|
||||||
|
} else {
|
||||||
|
connecting = Completer();
|
||||||
|
_disconnect().then((_) {
|
||||||
|
Logger.d("Socket connecting: $_webSocketAPIEndpoint...");
|
||||||
|
_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((_) {
|
||||||
|
Logger.d('Authentication complete');
|
||||||
|
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 (_token != null) {
|
||||||
|
if (!connecting.isCompleted) connecting.complete();
|
||||||
|
}
|
||||||
|
} else if (data["type"] == "auth_invalid") {
|
||||||
|
Logger.d("[Received] <== ${data.toString()}");
|
||||||
|
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||||
|
_messageResolver.remove("auth");
|
||||||
|
logout().then((_) {
|
||||||
|
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_handleMessage(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancelOnError: true,
|
||||||
|
onDone: () => _handleSocketClose(connecting),
|
||||||
|
onError: (e) => _handleSocketError(e, connecting)
|
||||||
|
);
|
||||||
|
});
|
||||||
return connecting.future;
|
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();
|
Future _disconnect() {
|
||||||
await _socket?.sink?.close()?.timeout(Duration(seconds: 4),
|
Completer completer = Completer();
|
||||||
onTimeout: () => Logger.d( "Socket sink close timeout")
|
if (!isConnected) {
|
||||||
);
|
completer.complete();
|
||||||
Logger.d( "..Disconnected");
|
} else {
|
||||||
|
isConnected = false;
|
||||||
|
List<Future> fl = [];
|
||||||
|
Logger.d("Socket disconnecting...");
|
||||||
|
if (_socketSubscription != null) {
|
||||||
|
fl.add(_socketSubscription.cancel());
|
||||||
|
}
|
||||||
|
if (_socket != null && _socket.sink != null &&
|
||||||
|
_socket.closeCode == null) {
|
||||||
|
fl.add(_socket.sink.close().timeout(Duration(seconds: 3)));
|
||||||
|
}
|
||||||
|
Future.wait(fl).whenComplete(() => completer.complete());
|
||||||
|
}
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMessage(data) {
|
_handleMessage(data) {
|
||||||
@ -136,7 +195,7 @@ class Connection {
|
|||||||
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
||||||
} else if (data["id"] != null) {
|
} else if (data["id"] != null) {
|
||||||
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
||||||
_messageResolver["${data["id"]}"]?.completeError({"errorMessage": "${data['error']["message"]}"});
|
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
|
||||||
}
|
}
|
||||||
_messageResolver.remove("${data["id"]}");
|
_messageResolver.remove("${data["id"]}");
|
||||||
} else if (data["type"] == "event") {
|
} else if (data["type"] == "event") {
|
||||||
@ -154,30 +213,36 @@ class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleSocketClose(Completer connectionCompleter) {
|
void _handleSocketClose(Completer connectionCompleter) {
|
||||||
isConnected = false;
|
|
||||||
Logger.d("Socket disconnected.");
|
Logger.d("Socket disconnected.");
|
||||||
if (!connectionCompleter.isCompleted) {
|
if (!connectionCompleter.isCompleted) {
|
||||||
connectionCompleter.completeError({"errorCode": 82, "errorMessage": "Disconnected"});
|
isConnected = false;
|
||||||
|
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||||
} else {
|
} else {
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
Timer(Duration(seconds: 5), () {
|
Timer(Duration(seconds: 5), () {
|
||||||
Logger.d("Trying to reconnect...");
|
Logger.d("Trying to reconnect...");
|
||||||
_connect();
|
_connect().catchError((e) {
|
||||||
|
isConnected = false;
|
||||||
|
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSocketError(e, Completer connectionCompleter) {
|
void _handleSocketError(e, Completer connectionCompleter) {
|
||||||
isConnected = false;
|
|
||||||
Logger.e("Socket stream Error: $e");
|
Logger.e("Socket stream Error: $e");
|
||||||
if (!connectionCompleter.isCompleted) {
|
if (!connectionCompleter.isCompleted) {
|
||||||
connectionCompleter.completeError({"errorCode": 81, "errorMessage": "Unable to connect to Home Assistant"});
|
isConnected = false;
|
||||||
|
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
|
||||||
} else {
|
} else {
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
Timer(Duration(seconds: 5), () {
|
Timer(Duration(seconds: 5), () {
|
||||||
Logger.d("Trying to reconnect...");
|
Logger.d("Trying to reconnect...");
|
||||||
_connect();
|
_connect().catchError((e) {
|
||||||
|
isConnected = false;
|
||||||
|
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -204,6 +269,7 @@ class Connection {
|
|||||||
).then((_) {
|
).then((_) {
|
||||||
Logger.d("Requesting long-lived token...");
|
Logger.d("Requesting long-lived token...");
|
||||||
_getLongLivedToken().then((_) {
|
_getLongLivedToken().then((_) {
|
||||||
|
Logger.d("getLongLivedToken finished");
|
||||||
completer.complete();
|
completer.complete();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
Logger.e("Can't get long-lived token: $e");
|
Logger.e("Can't get long-lived token: $e");
|
||||||
@ -211,16 +277,22 @@ class Connection {
|
|||||||
});
|
});
|
||||||
}).catchError((e) => completer.completeError(e));
|
}).catchError((e) => completer.completeError(e));
|
||||||
} else {
|
} else {
|
||||||
completer.completeError({"errorCode": 63, "errorMessage": "General login error"});
|
completer.completeError(HAError("General login error"));
|
||||||
}
|
}
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future logout() {
|
Future logout() {
|
||||||
_token = null;
|
Completer completer = Completer();
|
||||||
_tempToken = null;
|
_disconnect().whenComplete(() {
|
||||||
final storage = new FlutterSecureStorage();
|
_token = null;
|
||||||
return storage.delete(key: "hacl_llt");
|
_tempToken = null;
|
||||||
|
final storage = new FlutterSecureStorage();
|
||||||
|
storage.delete(key: "hacl_llt").whenComplete((){
|
||||||
|
completer.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getLongLivedToken() {
|
Future _getLongLivedToken() {
|
||||||
@ -237,7 +309,7 @@ class Connection {
|
|||||||
});
|
});
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
logout();
|
logout();
|
||||||
completer.completeError({"errorCode": 63, "errorMessage": "Authentication error: $e"});
|
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()]));
|
||||||
});
|
});
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
@ -258,16 +330,17 @@ class Connection {
|
|||||||
}
|
}
|
||||||
_messageResolver[callbackName] = _completer;
|
_messageResolver[callbackName] = _completer;
|
||||||
String rawMessage = json.encode(dataObject);
|
String rawMessage = json.encode(dataObject);
|
||||||
Logger.d("[Sending] ==> $rawMessage");
|
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
_connect().timeout(connectTimeout, onTimeout: (){
|
_connect().timeout(connectTimeout, onTimeout: (){
|
||||||
_completer.completeError({"errorCode": 8, "errorMessage": "No connection to Home Assistant"});
|
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||||
}).then((_) {
|
}).then((_) {
|
||||||
|
Logger.d("[Sending] ==> $rawMessage");
|
||||||
_socket.sink.add(rawMessage);
|
_socket.sink.add(rawMessage);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_completer.completeError(e);
|
_completer.completeError(e);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
Logger.d("[Sending] ==> $rawMessage");
|
||||||
_socket.sink.add(rawMessage);
|
_socket.sink.add(rawMessage);
|
||||||
}
|
}
|
||||||
return _completer.future;
|
return _completer.future;
|
||||||
@ -311,16 +384,16 @@ class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future sendHTTPPost({String host, String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true, String authToken}) async {
|
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
String url = "$host$endPoint";
|
String url = "$httpWebHost$endPoint";
|
||||||
Logger.d("[Sending] ==> $url");
|
Logger.d("[Sending] ==> $url");
|
||||||
Map<String, String> headers = {};
|
Map<String, String> headers = {};
|
||||||
if (contentType != null) {
|
if (contentType != null) {
|
||||||
headers["Content-Type"] = contentType;
|
headers["Content-Type"] = contentType;
|
||||||
}
|
}
|
||||||
if (includeAuthHeader) {
|
if (includeAuthHeader) {
|
||||||
headers["authorization"] = "Bearer $authToken";
|
headers["authorization"] = "Bearer $_token";
|
||||||
}
|
}
|
||||||
http.post(
|
http.post(
|
||||||
url,
|
url,
|
||||||
@ -328,7 +401,7 @@ class Connection {
|
|||||||
body: data
|
body: data
|
||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
|
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
||||||
completer.complete(response.body);
|
completer.complete(response.body);
|
||||||
} else {
|
} else {
|
||||||
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
|
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
|
||||||
@ -336,7 +409,6 @@ class Connection {
|
|||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
completer.completeError(e);
|
completer.completeError(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
29
lib/device.class.dart
Normal file
29
lib/device.class.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class Device {
|
||||||
|
|
||||||
|
static final Device _instance = Device._internal();
|
||||||
|
|
||||||
|
factory Device() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
String unicDeviceId;
|
||||||
|
String manufacturer;
|
||||||
|
String model;
|
||||||
|
String osName;
|
||||||
|
String osVersion;
|
||||||
|
|
||||||
|
Device._internal();
|
||||||
|
|
||||||
|
loadDeviceInfo() {
|
||||||
|
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||||
|
deviceInfo.androidInfo.then((androidInfo) {
|
||||||
|
unicDeviceId = "${androidInfo.model.toLowerCase().replaceAll(' ', '_')}_${androidInfo.androidId}";
|
||||||
|
manufacturer = "${androidInfo.manufacturer}";
|
||||||
|
model = "${androidInfo.model}";
|
||||||
|
osName = "Android";
|
||||||
|
osVersion = "${androidInfo.version.release}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -46,10 +46,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
|||||||
// the App.build method, and use it to set our appbar title.
|
// the App.build method, and use it to set our appbar title.
|
||||||
title: new Text(_title),
|
title: new Text(_title),
|
||||||
),
|
),
|
||||||
body: HomeAssistantModel(
|
body: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||||
homeAssistant: widget.homeAssistant,
|
|
||||||
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,71 +10,57 @@ class ClimateEntity extends Entity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
||||||
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
|
static const SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
|
||||||
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
|
static const SUPPORT_TARGET_HUMIDITY = 4;
|
||||||
static const SUPPORT_TARGET_HUMIDITY = 8;
|
static const SUPPORT_FAN_MODE = 8;
|
||||||
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
|
static const SUPPORT_PRESET_MODE = 16;
|
||||||
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
|
static const SUPPORT_SWING_MODE = 32;
|
||||||
static const SUPPORT_FAN_MODE = 64;
|
static const SUPPORT_AUX_HEAT = 64;
|
||||||
static const SUPPORT_OPERATION_MODE = 128;
|
|
||||||
static const SUPPORT_HOLD_MODE = 256;
|
|
||||||
static const SUPPORT_SWING_MODE = 512;
|
//static const SUPPORT_OPERATION_MODE = 16;
|
||||||
static const SUPPORT_AWAY_MODE = 1024;
|
//static const SUPPORT_HOLD_MODE = 256;
|
||||||
static const SUPPORT_AUX_HEAT = 2048;
|
//static const SUPPORT_AWAY_MODE = 1024;
|
||||||
static const SUPPORT_ON_OFF = 4096;
|
//static const SUPPORT_ON_OFF = 4096;
|
||||||
|
|
||||||
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
|
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);
|
||||||
bool get supportTargetTemperatureHigh => ((supportedFeatures &
|
bool get supportTargetTemperatureRange => ((supportedFeatures &
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) ==
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE);
|
||||||
bool get supportTargetTemperatureLow => ((supportedFeatures &
|
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
|
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
|
|
||||||
bool get supportTargetHumidity => ((supportedFeatures &
|
bool get supportTargetHumidity => ((supportedFeatures &
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
||||||
bool get supportTargetHumidityHigh => ((supportedFeatures &
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
|
|
||||||
bool get supportTargetHumidityLow => ((supportedFeatures &
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
|
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
|
|
||||||
bool get supportFanMode =>
|
bool get supportFanMode =>
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
|
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||||
ClimateEntity.SUPPORT_FAN_MODE);
|
ClimateEntity.SUPPORT_FAN_MODE);
|
||||||
bool get supportOperationMode => ((supportedFeatures &
|
|
||||||
ClimateEntity.SUPPORT_OPERATION_MODE) ==
|
|
||||||
ClimateEntity.SUPPORT_OPERATION_MODE);
|
|
||||||
bool get supportHoldMode =>
|
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) ==
|
|
||||||
ClimateEntity.SUPPORT_HOLD_MODE);
|
|
||||||
bool get supportSwingMode =>
|
bool get supportSwingMode =>
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
|
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||||
ClimateEntity.SUPPORT_SWING_MODE);
|
ClimateEntity.SUPPORT_SWING_MODE);
|
||||||
bool get supportAwayMode =>
|
bool get supportPresetMode =>
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) ==
|
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
|
||||||
ClimateEntity.SUPPORT_AWAY_MODE);
|
ClimateEntity.SUPPORT_PRESET_MODE);
|
||||||
bool get supportAuxHeat =>
|
bool get supportAuxHeat =>
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||||
ClimateEntity.SUPPORT_AUX_HEAT);
|
ClimateEntity.SUPPORT_AUX_HEAT);
|
||||||
bool get supportOnOff =>
|
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) ==
|
|
||||||
ClimateEntity.SUPPORT_ON_OFF);
|
|
||||||
|
|
||||||
List<String> get operationList => attributes["operation_list"] != null
|
List<String> get hvacModes => attributes["hvac_modes"] != null
|
||||||
? (attributes["operation_list"] as List).cast<String>()
|
? (attributes["hvac_modes"] as List).cast<String>()
|
||||||
: null;
|
: null;
|
||||||
List<String> get fanList => attributes["fan_list"] != null
|
List<String> get fanModes => attributes["fan_modes"] != null
|
||||||
? (attributes["fan_list"] as List).cast<String>()
|
? (attributes["fan_modes"] as List).cast<String>()
|
||||||
: null;
|
: null;
|
||||||
List<String> get swingList => attributes["swing_list"] != null
|
List<String> get presetModes => attributes["preset_modes"] != null
|
||||||
? (attributes["swing_list"] as List).cast<String>()
|
? (attributes["preset_modes"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
List<String> get swingModes => attributes["swing_modes"] != null
|
||||||
|
? (attributes["swing_modes"] as List).cast<String>()
|
||||||
: null;
|
: null;
|
||||||
double get temperature => _getDoubleAttributeValue('temperature');
|
double get temperature => _getDoubleAttributeValue('temperature');
|
||||||
|
double get currentTemperature => _getDoubleAttributeValue('current_temperature');
|
||||||
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
||||||
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
||||||
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
||||||
@ -83,11 +69,12 @@ class ClimateEntity extends Entity {
|
|||||||
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||||
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||||
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
|
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
|
||||||
String get operationMode => attributes['operation_mode'];
|
String get hvacAction => attributes['hvac_action'];
|
||||||
String get fanMode => attributes['fan_mode'];
|
String get fanMode => attributes['fan_mode'];
|
||||||
|
String get presetMode => attributes['preset_mode'];
|
||||||
String get swingMode => attributes['swing_mode'];
|
String get swingMode => attributes['swing_mode'];
|
||||||
bool get awayMode => attributes['away_mode'] == "on";
|
bool get awayMode => attributes['away_mode'] == "on";
|
||||||
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";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -96,10 +83,8 @@ class ClimateEntity extends Entity {
|
|||||||
if (supportTargetTemperature) {
|
if (supportTargetTemperature) {
|
||||||
historyConfig.numericAttributesToShow.add("temperature");
|
historyConfig.numericAttributesToShow.add("temperature");
|
||||||
}
|
}
|
||||||
if (supportTargetTemperatureHigh) {
|
if (supportTargetTemperatureRange) {
|
||||||
historyConfig.numericAttributesToShow.add("target_temp_high");
|
historyConfig.numericAttributesToShow.add("target_temp_high");
|
||||||
}
|
|
||||||
if (supportTargetTemperatureLow) {
|
|
||||||
historyConfig.numericAttributesToShow.add("target_temp_low");
|
historyConfig.numericAttributesToShow.add("target_temp_low");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class Entity {
|
|||||||
"cold.on": "Cold",
|
"cold.on": "Cold",
|
||||||
"cold.off": "Normal",
|
"cold.off": "Normal",
|
||||||
"connectivity.on": "Connected",
|
"connectivity.on": "Connected",
|
||||||
"connectivity.off": "Diconnected",
|
"connectivity.off": "Disconnected",
|
||||||
"door.on": "Open",
|
"door.on": "Open",
|
||||||
"door.off": "Closed",
|
"door.off": "Closed",
|
||||||
"garage_door.on": "Open",
|
"garage_door.on": "Open",
|
||||||
@ -154,7 +154,7 @@ class Entity {
|
|||||||
entityId = rawData["entity_id"];
|
entityId = rawData["entity_id"];
|
||||||
deviceClass = attributes["device_class"];
|
deviceClass = attributes["device_class"];
|
||||||
state = rawData["state"];
|
state = rawData["state"];
|
||||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
|
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
||||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||||
entityPicture = _getEntityPictureUrl(webHost);
|
entityPicture = _getEntityPictureUrl(webHost);
|
||||||
}
|
}
|
||||||
@ -216,7 +216,7 @@ class Entity {
|
|||||||
entityWrapper: EntityWrapper(entity: this),
|
entityWrapper: EntityWrapper(entity: this),
|
||||||
child: EntityPageContainer(children: <Widget>[
|
child: EntityPageContainer(children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
||||||
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
||||||
),
|
),
|
||||||
LastUpdatedWidget(),
|
LastUpdatedWidget(),
|
||||||
|
@ -35,25 +35,36 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "device_tracker":
|
case "device_tracker":
|
||||||
|
case "person":
|
||||||
{
|
{
|
||||||
badgeIcon = EntityIcon(
|
badgeIcon = EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
color: Colors.black
|
color: Colors.black
|
||||||
);
|
);
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.state;
|
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
|
double stateFontSize;
|
||||||
|
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
|
||||||
|
stateFontSize = 18.0;
|
||||||
|
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
|
||||||
|
stateFontSize = 15.0;
|
||||||
|
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
|
||||||
|
stateFontSize = 10.0;
|
||||||
|
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||||
|
stateFontSize = 8.0;
|
||||||
|
}
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
||||||
badgeIcon = Center(
|
badgeIcon = Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"${entityModel.entityWrapper.entity.state}",
|
"${entityModel.entityWrapper.entity.displayState}",
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 17.0),
|
style: TextStyle(fontSize: stateFontSize),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -16,112 +16,26 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CameraEntity _entity;
|
CameraEntity _entity;
|
||||||
String _webHost;
|
|
||||||
|
|
||||||
http.Client client;
|
|
||||||
http.StreamedResponse response;
|
|
||||||
List<int> binaryImage = [];
|
|
||||||
bool timeToStop = false;
|
|
||||||
Completer streamCompleter;
|
|
||||||
bool started = false;
|
bool started = false;
|
||||||
bool useSVG = false;
|
String streamUrl = "";
|
||||||
|
|
||||||
void _connect() async {
|
launchStream() {
|
||||||
started = true;
|
Navigator.push(
|
||||||
timeToStop = false;
|
context,
|
||||||
String streamUrl = '$_webHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}';
|
MaterialPageRoute(
|
||||||
client = new http.Client(); // create a client to make api calls
|
builder: (context) => WebviewScaffold(
|
||||||
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
|
url: "$streamUrl",
|
||||||
Logger.d("[Sending] ==> $streamUrl");
|
withZoom: true,
|
||||||
response = await client.send(request);
|
appBar: new AppBar(
|
||||||
Logger.d("[Received] <== ${response.headers}");
|
leading: IconButton(
|
||||||
String frameBoundary = response.headers['content-type'].split('boundary=')[1];
|
icon: Icon(Icons.close),
|
||||||
final int frameBoundarySize = frameBoundary.length;
|
onPressed: () => Navigator.pop(context)
|
||||||
List<int> primaryBuffer=[];
|
),
|
||||||
int imageSizeStart = 59;
|
title: new Text("${_entity.displayName}"),
|
||||||
int imageSizeEnd = 0;
|
),
|
||||||
int imageStart = 0;
|
),
|
||||||
int imageSize = 0;
|
|
||||||
String strBuffer = "";
|
|
||||||
String contentType = "";
|
|
||||||
streamCompleter = Completer();
|
|
||||||
response.stream.transform(
|
|
||||||
StreamTransformer.fromHandlers(
|
|
||||||
handleData: (data, sink) {
|
|
||||||
primaryBuffer.addAll(data);
|
|
||||||
imageStart = 0;
|
|
||||||
imageSizeEnd = 0;
|
|
||||||
if (primaryBuffer.length >= imageSizeStart + 10) {
|
|
||||||
contentType = utf8.decode(
|
|
||||||
primaryBuffer.sublist(frameBoundarySize+16, imageSizeStart + 10), allowMalformed: true).split("\r\n")[0];
|
|
||||||
useSVG = contentType == "image/svg+xml";
|
|
||||||
imageSizeStart = frameBoundarySize + 16 + contentType.length + 18;
|
|
||||||
for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) {
|
|
||||||
strBuffer = utf8.decode(
|
|
||||||
primaryBuffer.sublist(i, i + 4), allowMalformed: true);
|
|
||||||
if (strBuffer == "\r\n\r\n") {
|
|
||||||
imageSizeEnd = i;
|
|
||||||
imageStart = i + 4;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (imageSizeEnd > 0) {
|
|
||||||
imageSize = int.tryParse(utf8.decode(
|
|
||||||
primaryBuffer.sublist(imageSizeStart, imageSizeEnd),
|
|
||||||
allowMalformed: true));
|
|
||||||
//Logger.d("content-length: $imageSize");
|
|
||||||
if (imageSize != null &&
|
|
||||||
primaryBuffer.length >= imageStart + imageSize + 2) {
|
|
||||||
sink.add(
|
|
||||||
primaryBuffer.sublist(
|
|
||||||
imageStart, imageStart + imageSize));
|
|
||||||
primaryBuffer.removeRange(0, imageStart + imageSize + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (timeToStop) {
|
|
||||||
sink?.close();
|
|
||||||
streamCompleter.complete();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleError: (error, stack, sink) {
|
|
||||||
Logger.e("Error parsing MJPEG stream: $error");
|
|
||||||
},
|
|
||||||
handleDone: (sink) {
|
|
||||||
Logger.d("Camera stream finished. Reconnecting...");
|
|
||||||
sink?.close();
|
|
||||||
streamCompleter?.complete();
|
|
||||||
_reconnect();
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
).listen((d) {
|
);
|
||||||
if (!timeToStop) {
|
|
||||||
setState(() {
|
|
||||||
binaryImage = d;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _reconnect() {
|
|
||||||
disconnect().then((_){
|
|
||||||
_connect();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future disconnect() {
|
|
||||||
Completer disconF = Completer();
|
|
||||||
timeToStop = true;
|
|
||||||
if (streamCompleter != null && !streamCompleter.isCompleted) {
|
|
||||||
streamCompleter.future.then((_) {
|
|
||||||
client?.close();
|
|
||||||
disconF.complete();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
client?.close();
|
|
||||||
disconF.complete();
|
|
||||||
}
|
|
||||||
return disconF.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -131,47 +45,26 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
.of(context)
|
.of(context)
|
||||||
.entityWrapper
|
.entityWrapper
|
||||||
.entity;
|
.entity;
|
||||||
_webHost = HomeAssistantModel.of(context).homeAssistant.connection.httpWebHost;
|
started = true;
|
||||||
_connect();
|
|
||||||
}
|
}
|
||||||
|
streamUrl = '${Connection().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||||
if (binaryImage.isEmpty) {
|
.entityId}?token=${_entity.attributes['access_token']}';
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: const CircularProgressIndicator()
|
child: IconButton(
|
||||||
)
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
||||||
],
|
iconSize: 50.0,
|
||||||
);
|
onPressed: () => launchStream(),
|
||||||
} else {
|
|
||||||
if (useSVG) {
|
|
||||||
return Column(
|
|
||||||
children: <Widget>[
|
|
||||||
SvgPicture.memory(
|
|
||||||
Uint8List.fromList(binaryImage),
|
|
||||||
placeholderBuilder: (BuildContext context) =>
|
|
||||||
new Container(
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
child: const CircularProgressIndicator()
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
)
|
||||||
);
|
],
|
||||||
} else {
|
);
|
||||||
return Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Image.memory(
|
|
||||||
Uint8List.fromList(binaryImage), gaplessPlayback: true),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
disconnect();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -19,22 +19,22 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
double _tmpTargetLow = 0.0;
|
double _tmpTargetLow = 0.0;
|
||||||
double _tmpTargetHigh = 0.0;
|
double _tmpTargetHigh = 0.0;
|
||||||
double _tmpTargetHumidity = 0.0;
|
double _tmpTargetHumidity = 0.0;
|
||||||
String _tmpOperationMode;
|
String _tmpHVACMode;
|
||||||
String _tmpFanMode;
|
String _tmpFanMode;
|
||||||
String _tmpSwingMode;
|
String _tmpSwingMode;
|
||||||
bool _tmpAwayMode = false;
|
String _tmpPresetMode;
|
||||||
bool _tmpIsOff = false;
|
//bool _tmpIsOff = false;
|
||||||
bool _tmpAuxHeat = false;
|
bool _tmpAuxHeat = false;
|
||||||
|
|
||||||
void _resetVars(ClimateEntity entity) {
|
void _resetVars(ClimateEntity entity) {
|
||||||
_tmpTemperature = entity.temperature;
|
_tmpTemperature = entity.temperature;
|
||||||
_tmpTargetHigh = entity.targetHigh;
|
_tmpTargetHigh = entity.targetHigh;
|
||||||
_tmpTargetLow = entity.targetLow;
|
_tmpTargetLow = entity.targetLow;
|
||||||
_tmpOperationMode = entity.operationMode;
|
_tmpHVACMode = entity.state;
|
||||||
_tmpFanMode = entity.fanMode;
|
_tmpFanMode = entity.fanMode;
|
||||||
_tmpSwingMode = entity.swingMode;
|
_tmpSwingMode = entity.swingMode;
|
||||||
_tmpAwayMode = entity.awayMode;
|
_tmpPresetMode = entity.presetMode;
|
||||||
_tmpIsOff = entity.isOff;
|
//_tmpIsOff = entity.isOff;
|
||||||
_tmpAuxHeat = entity.auxHeat;
|
_tmpAuxHeat = entity.auxHeat;
|
||||||
_tmpTargetHumidity = entity.targetHumidity;
|
_tmpTargetHumidity = entity.targetHumidity;
|
||||||
|
|
||||||
@ -116,11 +116,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setOperationMode(ClimateEntity entity, value) {
|
void _setHVACMode(ClimateEntity entity, value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tmpOperationMode = value;
|
_tmpHVACMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"}));
|
||||||
_resetStateTimer(entity);
|
_resetStateTimer(entity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -143,23 +143,23 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setAwayMode(ClimateEntity entity, value) {
|
void _setPresetMode(ClimateEntity entity, value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tmpAwayMode = value;
|
_tmpPresetMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"}));
|
||||||
_resetStateTimer(entity);
|
_resetStateTimer(entity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setOnOf(ClimateEntity entity, value) {
|
/*void _setOnOf(ClimateEntity entity, value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tmpIsOff = !value;
|
_tmpIsOff = !value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
|
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
|
||||||
_resetStateTimer(entity);
|
_resetStateTimer(entity);
|
||||||
});
|
});
|
||||||
}
|
}*/
|
||||||
|
|
||||||
void _setAuxHeat(ClimateEntity entity, value) {
|
void _setAuxHeat(ClimateEntity entity, value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -196,33 +196,34 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildOnOffControl(entity),
|
//_buildOnOffControl(entity),
|
||||||
_buildTemperatureControls(entity),
|
_buildTemperatureControls(entity),
|
||||||
_buildTargetTemperatureControls(entity),
|
_buildTargetTemperatureControls(entity),
|
||||||
_buildHumidityControls(entity),
|
_buildHumidityControls(entity),
|
||||||
_buildOperationControl(entity),
|
_buildOperationControl(entity),
|
||||||
_buildFanControl(entity),
|
_buildFanControl(entity),
|
||||||
_buildSwingControl(entity),
|
_buildSwingControl(entity),
|
||||||
_buildAwayModeControl(entity),
|
_buildPresetModeControl(entity),
|
||||||
_buildAuxHeatControl(entity)
|
_buildAuxHeatControl(entity)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAwayModeControl(ClimateEntity entity) {
|
Widget _buildPresetModeControl(ClimateEntity entity) {
|
||||||
if (entity.supportAwayMode) {
|
if (entity.supportPresetMode) {
|
||||||
return ModeSwitchWidget(
|
return ModeSelectorWidget(
|
||||||
caption: "Away mode",
|
options: entity.presetModes,
|
||||||
onChange: (value) => _setAwayMode(entity, value),
|
onChange: (mode) => _setPresetMode(entity, mode),
|
||||||
value: _tmpAwayMode,
|
caption: "Preset",
|
||||||
|
value: _tmpPresetMode,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOnOffControl(ClimateEntity entity) {
|
/*Widget _buildOnOffControl(ClimateEntity entity) {
|
||||||
if (entity.supportOnOff) {
|
if (entity.supportOnOff) {
|
||||||
return ModeSwitchWidget(
|
return ModeSwitchWidget(
|
||||||
onChange: (value) => _setOnOf(entity, value),
|
onChange: (value) => _setOnOf(entity, value),
|
||||||
@ -232,7 +233,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
} else {
|
} else {
|
||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
||||||
if (entity.supportAuxHeat ) {
|
if (entity.supportAuxHeat ) {
|
||||||
@ -247,12 +248,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOperationControl(ClimateEntity entity) {
|
Widget _buildOperationControl(ClimateEntity entity) {
|
||||||
if (entity.supportOperationMode) {
|
if (entity.hvacModes != null) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setOperationMode(entity, mode),
|
onChange: (mode) => _setHVACMode(entity, mode),
|
||||||
options: entity.operationList,
|
options: entity.hvacModes,
|
||||||
caption: "Operation",
|
caption: "Operation",
|
||||||
value: _tmpOperationMode,
|
value: _tmpHVACMode,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(height: 0.0, width: 0.0);
|
return Container(height: 0.0, width: 0.0);
|
||||||
@ -262,7 +263,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
Widget _buildFanControl(ClimateEntity entity) {
|
Widget _buildFanControl(ClimateEntity entity) {
|
||||||
if (entity.supportFanMode) {
|
if (entity.supportFanMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.fanList,
|
options: entity.fanModes,
|
||||||
onChange: (mode) => _setFanMode(entity, mode),
|
onChange: (mode) => _setFanMode(entity, mode),
|
||||||
caption: "Fan mode",
|
caption: "Fan mode",
|
||||||
value: _tmpFanMode,
|
value: _tmpFanMode,
|
||||||
@ -276,7 +277,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
if (entity.supportSwingMode) {
|
if (entity.supportSwingMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setSwingMode(entity, mode),
|
onChange: (mode) => _setSwingMode(entity, mode),
|
||||||
options: entity.swingList,
|
options: entity.swingModes,
|
||||||
value: _tmpSwingMode,
|
value: _tmpSwingMode,
|
||||||
caption: "Swing mode"
|
caption: "Swing mode"
|
||||||
);
|
);
|
||||||
@ -308,7 +309,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
|
|
||||||
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
||||||
List<Widget> controls = [];
|
List<Widget> controls = [];
|
||||||
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
|
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
||||||
controls.addAll(<Widget>[
|
controls.addAll(<Widget>[
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetLow,
|
value: _tmpTargetLow,
|
||||||
@ -321,7 +322,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) {
|
if ((entity.supportTargetTemperatureRange) && (entity.targetHigh != null)) {
|
||||||
controls.add(
|
controls.add(
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetHigh,
|
value: _tmpTargetHigh,
|
||||||
|
@ -17,7 +17,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
String _tmpEffect;
|
String _tmpEffect;
|
||||||
|
|
||||||
void _resetState(LightEntity entity) {
|
void _resetState(LightEntity entity) {
|
||||||
_tmpBrightness = entity.brightness ?? 0;
|
_tmpBrightness = entity.brightness ?? 1;
|
||||||
_tmpWhiteValue = entity.whiteValue ?? 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;
|
||||||
@ -28,15 +28,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpBrightness = value.round();
|
_tmpBrightness = value.round();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
if (_tmpBrightness > 0) {
|
eventBus.fire(new ServiceCallEvent(
|
||||||
eventBus.fire(new ServiceCallEvent(
|
entity.domain, "turn_on", entity.entityId,
|
||||||
entity.domain, "turn_on", entity.entityId,
|
{"brightness": _tmpBrightness}));
|
||||||
{"brightness": _tmpBrightness}));
|
|
||||||
} else {
|
|
||||||
eventBus.fire(new ServiceCallEvent(
|
|
||||||
entity.domain, "turn_off", entity.entityId,
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,10 +108,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpBrightness = value.round();
|
_tmpBrightness = value.round();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
min: 0.0,
|
min: 1.0,
|
||||||
max: 255.0,
|
max: 255.0,
|
||||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||||
value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(),
|
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(),
|
||||||
leading: Icon(Icons.brightness_5),
|
leading: Icon(Icons.brightness_5),
|
||||||
title: "Brightness",
|
title: "Brightness",
|
||||||
);
|
);
|
||||||
@ -171,7 +165,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
Widget _buildColorControl(LightEntity entity) {
|
Widget _buildColorControl(LightEntity entity) {
|
||||||
if (entity.supportColor) {
|
if (entity.supportColor) {
|
||||||
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
|
HSVColor savedColor = HomeAssistant().savedColor;
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -187,10 +181,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
child: Text('Copy color'),
|
child: Text('Copy color'),
|
||||||
onPressed: _tmpColor == null ? null : () {
|
onPressed: _tmpColor == null ? null : () {
|
||||||
setState(() {
|
setState(() {
|
||||||
HomeAssistantModel
|
HomeAssistant().savedColor = _tmpColor;
|
||||||
.of(context)
|
|
||||||
.homeAssistant
|
|
||||||
.savedColor = _tmpColor;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -15,14 +15,18 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
return MissedEntityWidget();
|
return MissedEntityWidget();
|
||||||
}
|
}
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
||||||
return Divider();
|
return Divider(
|
||||||
|
color: Colors.black45,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Divider(),
|
Divider(
|
||||||
|
color: Colors.black45,
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
"${entityModel.entityWrapper.entity.displayName}",
|
"${entityModel.entityWrapper.entity.displayName}",
|
||||||
style: TextStyle(color: Colors.blue),
|
style: TextStyle(color: Colors.blue),
|
||||||
|
@ -40,14 +40,14 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
_needToUpdateHistory = true;
|
_needToUpdateHistory = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadHistory(HomeAssistant ha, String entityId) {
|
void _loadHistory(String entityId) {
|
||||||
DateTime now = DateTime.now();
|
DateTime now = DateTime.now();
|
||||||
if (_historyLastUpdated != null) {
|
if (_historyLastUpdated != null) {
|
||||||
Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
|
Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
|
||||||
}
|
}
|
||||||
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
||||||
_historyLastUpdated = now;
|
_historyLastUpdated = now;
|
||||||
ha.connection.getHistory(entityId).then((history){
|
Connection().getHistory(entityId).then((history){
|
||||||
if (!_disposed) {
|
if (!_disposed) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_history = history.isNotEmpty ? history[0] : [];
|
_history = history.isNotEmpty ? history[0] : [];
|
||||||
@ -68,13 +68,12 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
|
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
final Entity entity = entityModel.entityWrapper.entity;
|
final Entity entity = entityModel.entityWrapper.entity;
|
||||||
if (!_needToUpdateHistory) {
|
if (!_needToUpdateHistory) {
|
||||||
_needToUpdateHistory = true;
|
_needToUpdateHistory = true;
|
||||||
} else {
|
} else {
|
||||||
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
|
_loadHistory(entity.entityId);
|
||||||
}
|
}
|
||||||
return _buildChart();
|
return _buildChart();
|
||||||
}
|
}
|
||||||
|
@ -20,23 +20,3 @@ class EntityModel extends InheritedWidget {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeAssistantModel extends InheritedWidget {
|
|
||||||
|
|
||||||
const HomeAssistantModel({
|
|
||||||
Key key,
|
|
||||||
@required this.homeAssistant,
|
|
||||||
@required Widget child,
|
|
||||||
}) : super(key: key, child: child);
|
|
||||||
|
|
||||||
final HomeAssistant homeAssistant;
|
|
||||||
|
|
||||||
static HomeAssistantModel of(BuildContext context) {
|
|
||||||
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,13 +8,19 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
String targetTemp = "-";
|
String targetTemp = "-";
|
||||||
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||||
targetTemp = "${entity.temperature}";
|
targetTemp = "${entity.temperature}";
|
||||||
} else if ((entity.supportTargetTemperatureLow) &&
|
} else if ((entity.supportTargetTemperatureRange) &&
|
||||||
(entity.targetLow != null)) {
|
(entity.targetLow != null) &&
|
||||||
targetTemp = "${entity.targetLow}";
|
(entity.targetHigh != null)) {
|
||||||
if ((entity.supportTargetTemperatureHigh) &&
|
targetTemp = "${entity.targetLow} - ${entity.targetHigh}";
|
||||||
(entity.targetHigh != null)) {
|
}
|
||||||
targetTemp += " - ${entity.targetHigh}";
|
String displayState = '';
|
||||||
}
|
if (entity.hvacAction != null) {
|
||||||
|
displayState = "${entity.hvacAction} (${entity.displayState})";
|
||||||
|
} else {
|
||||||
|
displayState = "${entity.displayState}";
|
||||||
|
}
|
||||||
|
if (entity.presetMode != null) {
|
||||||
|
displayState += " - ${entity.presetMode}";
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
@ -25,7 +31,7 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("${entity.state}",
|
Text("$displayState",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: new TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -38,8 +44,8 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
entity.attributes["current_temperature"] != null ?
|
entity.currentTemperature != null ?
|
||||||
Text("Currently: ${entity.attributes["current_temperature"]}",
|
Text("Currently: ${entity.currentTemperature}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: new TextStyle(
|
||||||
fontSize: Sizes.stateFontSize,
|
fontSize: Sizes.stateFontSize,
|
||||||
|
@ -2,21 +2,20 @@ part of 'main.dart';
|
|||||||
|
|
||||||
class HomeAssistant {
|
class HomeAssistant {
|
||||||
|
|
||||||
final Connection connection = Connection();
|
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||||
|
|
||||||
bool _useLovelace = false;
|
|
||||||
//bool isSettingsLoaded = false;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
factory HomeAssistant() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
EntityCollection entities;
|
EntityCollection entities;
|
||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
String _userName;
|
String _userName;
|
||||||
String hostname;
|
|
||||||
HSVColor savedColor;
|
HSVColor savedColor;
|
||||||
|
|
||||||
|
String fcmToken;
|
||||||
|
|
||||||
Map _rawLovelaceData;
|
Map _rawLovelaceData;
|
||||||
|
|
||||||
List<Panel> panels = [];
|
List<Panel> panels = [];
|
||||||
@ -24,7 +23,7 @@ class HomeAssistant {
|
|||||||
Duration fetchTimeout = Duration(seconds: 30);
|
Duration fetchTimeout = Duration(seconds: 30);
|
||||||
|
|
||||||
String get locationName {
|
String get locationName {
|
||||||
if (_useLovelace) {
|
if (Connection().useLovelace) {
|
||||||
return ui?.title ?? "";
|
return ui?.title ?? "";
|
||||||
} else {
|
} else {
|
||||||
return _instanceConfig["location_name"] ?? "";
|
return _instanceConfig["location_name"] ?? "";
|
||||||
@ -34,49 +33,43 @@ class HomeAssistant {
|
|||||||
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||||
bool get isNoEntities => entities == null || entities.isEmpty;
|
bool get isNoEntities => entities == null || entities.isEmpty;
|
||||||
bool get isNoViews => ui == null || ui.isEmpty;
|
bool get isNoViews => ui == null || ui.isEmpty;
|
||||||
//int get viewsCount => entities.views.length ?? 0;
|
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
||||||
|
|
||||||
HomeAssistant();
|
HomeAssistant._internal() {
|
||||||
|
Connection().onStateChangeCallback = _handleEntityStateChange;
|
||||||
Completer _connectCompleter;
|
Device().loadDeviceInfo();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Completer _fetchCompleter;
|
Completer _fetchCompleter;
|
||||||
|
|
||||||
Future fetch() {
|
Future fetchData() {
|
||||||
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||||
Logger.w("Previous data fetch is not completed yet");
|
Logger.w("Previous data fetch is not completed yet");
|
||||||
return _fetchCompleter.future;
|
return _fetchCompleter.future;
|
||||||
}
|
}
|
||||||
|
if (entities == null) entities = EntityCollection(Connection().httpWebHost);
|
||||||
_fetchCompleter = Completer();
|
_fetchCompleter = Completer();
|
||||||
List<Future> futures = [];
|
List<Future> futures = [];
|
||||||
futures.add(_getStates());
|
futures.add(_getStates());
|
||||||
if (_useLovelace) {
|
if (Connection().useLovelace) {
|
||||||
futures.add(_getLovelace());
|
futures.add(_getLovelace());
|
||||||
}
|
}
|
||||||
futures.add(_getConfig());
|
futures.add(_getConfig());
|
||||||
futures.add(_getServices());
|
futures.add(_getServices());
|
||||||
futures.add(_getUserInfo());
|
futures.add(_getUserInfo());
|
||||||
futures.add(_getPanels());
|
futures.add(_getPanels());
|
||||||
|
futures.add(Connection().sendSocketMessage(
|
||||||
|
type: "subscribe_events",
|
||||||
|
additionalData: {"event_type": "state_changed"},
|
||||||
|
));
|
||||||
Future.wait(futures).then((_) {
|
Future.wait(futures).then((_) {
|
||||||
_createUI();
|
if (isMobileAppEnabled) {
|
||||||
_fetchCompleter.complete();
|
_createUI();
|
||||||
|
_fetchCompleter.complete();
|
||||||
|
checkAppRegistration();
|
||||||
|
} else {
|
||||||
|
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app")]));
|
||||||
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_fetchCompleter.completeError(e);
|
_fetchCompleter.completeError(e);
|
||||||
});
|
});
|
||||||
@ -85,50 +78,160 @@ class HomeAssistant {
|
|||||||
|
|
||||||
Future logout() async {
|
Future logout() async {
|
||||||
Logger.d("Logging out...");
|
Logger.d("Logging out...");
|
||||||
await connection.logout().then((_) {
|
await Connection().logout().then((_) {
|
||||||
ui?.clear();
|
ui?.clear();
|
||||||
entities?.clear();
|
entities?.clear();
|
||||||
|
panels?.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map _getAppRegistrationData() {
|
||||||
|
return {
|
||||||
|
"app_version": "$appVersion",
|
||||||
|
"device_name": "$userName's ${Device().model}",
|
||||||
|
"manufacturer": Device().manufacturer,
|
||||||
|
"model": Device().model,
|
||||||
|
"os_version": Device().osVersion,
|
||||||
|
"app_data": {
|
||||||
|
"push_token": "$fcmToken",
|
||||||
|
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
|
||||||
|
Completer completer = Completer();
|
||||||
|
if (Connection().webhookId == null || forceRegister) {
|
||||||
|
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
||||||
|
var registrationData = _getAppRegistrationData();
|
||||||
|
registrationData.addAll({
|
||||||
|
"app_id": "ha_client",
|
||||||
|
"app_name": "$appName",
|
||||||
|
"supports_encryption": false,
|
||||||
|
});
|
||||||
|
Connection().sendHTTPPost(
|
||||||
|
endPoint: "/api/mobile_app/registrations",
|
||||||
|
includeAuthHeader: true,
|
||||||
|
data: json.encode(registrationData)
|
||||||
|
).then((response) {
|
||||||
|
Logger.d("Processing registration responce...");
|
||||||
|
var responseObject = json.decode(response);
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
||||||
|
Connection().webhookId = responseObject["webhook_id"];
|
||||||
|
completer.complete();
|
||||||
|
eventBus.fire(ShowDialogEvent(
|
||||||
|
title: "Mobile app Integration was created",
|
||||||
|
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
|
||||||
|
positiveText: "Restart now",
|
||||||
|
negativeText: "Later",
|
||||||
|
onPositive: () {
|
||||||
|
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
completer.complete();
|
||||||
|
Logger.e("Error registering the app: ${e.toString()}");
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
} else {
|
||||||
|
Logger.d("App was previously registered. Checking...");
|
||||||
|
var updateData = {
|
||||||
|
"type": "update_registration",
|
||||||
|
"data": _getAppRegistrationData()
|
||||||
|
};
|
||||||
|
Connection().sendHTTPPost(
|
||||||
|
endPoint: "/api/webhook/${Connection().webhookId}",
|
||||||
|
includeAuthHeader: false,
|
||||||
|
data: json.encode(updateData)
|
||||||
|
).then((response) {
|
||||||
|
Logger.d("App registration works fine");
|
||||||
|
if (showOkDialog) {
|
||||||
|
eventBus.fire(ShowDialogEvent(
|
||||||
|
title: "All good",
|
||||||
|
body: "HA Client integration with your Home Assistant server works fine",
|
||||||
|
positiveText: "Nice!",
|
||||||
|
negativeText: "Ok"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
completer.complete();
|
||||||
|
}).catchError((e) {
|
||||||
|
if (e['code'] != null && e['code'] == 410) {
|
||||||
|
Logger.e("MobileApp integration was removed");
|
||||||
|
eventBus.fire(ShowDialogEvent(
|
||||||
|
title: "App integration was removed",
|
||||||
|
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
|
||||||
|
positiveText: "Register now",
|
||||||
|
negativeText: "Cancel",
|
||||||
|
onPositive: () {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.remove("app-webhook-id");
|
||||||
|
Connection().webhookId = null;
|
||||||
|
HomeAssistant().checkAppRegistration();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
Logger.e("Error updating app registration: ${e.toString()}");
|
||||||
|
eventBus.fire(ShowDialogEvent(
|
||||||
|
title: "App integration is not working properly",
|
||||||
|
body: "Something wrong with HA Client integration on your Home Assistant server. Try to remove current app integration from Configuration -> Integrationds using web UI, restart your Home Assistant and go back to the app. NOTE that after clicking 'Ok' current integration data will be removed from the app and new integration wll be created on Home Assistant side on next app launch.",
|
||||||
|
positiveText: "Ok",
|
||||||
|
negativeText: "I'll handle it",
|
||||||
|
onPositive: () {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.remove("app-webhook-id");
|
||||||
|
Connection().webhookId = null;
|
||||||
|
HAUtils.launchURL(Connection().httpWebHost+"/config/integrations/dashboard");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
completer.complete();
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future _getConfig() async {
|
Future _getConfig() async {
|
||||||
await connection.sendSocketMessage(type: "get_config").then((data) {
|
await Connection().sendSocketMessage(type: "get_config").then((data) {
|
||||||
_instanceConfig = Map.from(data);
|
_instanceConfig = Map.from(data);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
throw {"errorCode": 1, "errorMessage": "Error getting config: $e"};
|
throw HAError("Error getting config: ${e}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getStates() async {
|
Future _getStates() async {
|
||||||
await connection.sendSocketMessage(type: "get_states").then(
|
await Connection().sendSocketMessage(type: "get_states").then(
|
||||||
(data) => entities.parse(data)
|
(data) => entities.parse(data)
|
||||||
).catchError((e) {
|
).catchError((e) {
|
||||||
throw {"errorCode": 1, "errorMessage": "Error getting states: $e"};
|
throw HAError("Error getting states: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getLovelace() async {
|
Future _getLovelace() async {
|
||||||
await connection.sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
await Connection().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
||||||
throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"};
|
throw HAError("Error getting lovelace config: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getUserInfo() async {
|
Future _getUserInfo() async {
|
||||||
_userName = null;
|
_userName = null;
|
||||||
await connection.sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
await Connection().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
||||||
Logger.w("Can't get user info: ${e}");
|
Logger.w("Can't get user info: ${e}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getServices() async {
|
Future _getServices() async {
|
||||||
await connection.sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
await Connection().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
||||||
Logger.w("Can't get services: ${e}");
|
Logger.w("Can't get services: ${e}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getPanels() async {
|
Future _getPanels() async {
|
||||||
panels.clear();
|
panels.clear();
|
||||||
await connection.sendSocketMessage(type: "get_panels").then((data) {
|
await Connection().sendSocketMessage(type: "get_panels").then((data) {
|
||||||
data.forEach((k,v) {
|
data.forEach((k,v) {
|
||||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||||
panels.add(Panel(
|
panels.add(Panel(
|
||||||
@ -142,17 +245,19 @@ class HomeAssistant {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"};
|
throw HAError("Error getting panels list: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleEntityStateChange(Map eventData) {
|
void _handleEntityStateChange(Map eventData) {
|
||||||
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||||
Map data = Map.from(eventData);
|
if (_fetchCompleter.isCompleted) {
|
||||||
eventBus.fire(new StateChangedEvent(
|
Map data = Map.from(eventData);
|
||||||
entityId: data["entity_id"],
|
eventBus.fire(new StateChangedEvent(
|
||||||
needToRebuildUI: entities.updateState(data)
|
entityId: data["entity_id"],
|
||||||
));
|
needToRebuildUI: entities.updateState(data)
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseLovelace() {
|
void _parseLovelace() {
|
||||||
@ -315,7 +420,7 @@ class HomeAssistant {
|
|||||||
|
|
||||||
void _createUI() {
|
void _createUI() {
|
||||||
ui = HomeAssistantUI();
|
ui = HomeAssistantUI();
|
||||||
if ((_useLovelace) && (_rawLovelaceData != null)) {
|
if ((Connection().useLovelace) && (_rawLovelaceData != null)) {
|
||||||
Logger.d("Creating Lovelace UI");
|
Logger.d("Creating Lovelace UI");
|
||||||
_parseLovelace();
|
_parseLovelace();
|
||||||
} else {
|
} else {
|
||||||
|
455
lib/main.dart
455
lib/main.dart
@ -15,11 +15,13 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:charts_flutter/flutter.dart' as charts;
|
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:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:device_info/device_info.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
part 'entity_class/const.dart';
|
part 'entity_class/const.dart';
|
||||||
part 'entity_class/entity.class.dart';
|
part 'entity_class/entity.class.dart';
|
||||||
@ -92,6 +94,7 @@ part 'mdi.class.dart';
|
|||||||
part 'entity_collection.class.dart';
|
part 'entity_collection.class.dart';
|
||||||
part 'auth_manager.class.dart';
|
part 'auth_manager.class.dart';
|
||||||
part 'connection.class.dart';
|
part 'connection.class.dart';
|
||||||
|
part 'device.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';
|
||||||
@ -104,8 +107,10 @@ part 'ui_widgets/config_panel_widget.dart';
|
|||||||
|
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||||
|
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersion = "0.6.0-alpha1";
|
const appVersion = "0.6.1";
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
FlutterError.onError = (errorDetails) {
|
FlutterError.onError = (errorDetails) {
|
||||||
@ -142,7 +147,17 @@ class HAClientApp extends StatelessWidget {
|
|||||||
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
|
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
|
||||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||||
"/configuration": (context) => PanelPage(title: "Configuration"),
|
"/configuration": (context) => PanelPage(title: "Configuration"),
|
||||||
"/log-view": (context) => LogViewPage(title: "Log")
|
"/log-view": (context) => LogViewPage(title: "Log"),
|
||||||
|
"/login": (_) => WebviewScaffold(
|
||||||
|
url: "${Connection().oauthUrl}",
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.help),
|
||||||
|
onPressed: () => HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
|
||||||
|
),
|
||||||
|
title: new Text("Login to your Home Assistant"),
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -166,41 +181,87 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
StreamSubscription _showEntityPageSubscription;
|
StreamSubscription _showEntityPageSubscription;
|
||||||
StreamSubscription _showErrorSubscription;
|
StreamSubscription _showErrorSubscription;
|
||||||
StreamSubscription _startAuthSubscription;
|
StreamSubscription _startAuthSubscription;
|
||||||
|
StreamSubscription _showDialogSubscription;
|
||||||
StreamSubscription _reloadUISubscription;
|
StreamSubscription _reloadUISubscription;
|
||||||
int _previousViewCount;
|
int _previousViewCount;
|
||||||
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
bool _showLoginButton = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
//widget.homeAssistant = HomeAssistant();
|
|
||||||
//_settingsLoaded = false;
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
_firebaseMessaging.configure(
|
||||||
|
onLaunch: (data) {
|
||||||
|
Logger.d("Notification [onLaunch]: $data");
|
||||||
|
},
|
||||||
|
onMessage: (data) {
|
||||||
|
Logger.d("Notification [onMessage]: $data");
|
||||||
|
_showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
|
||||||
|
},
|
||||||
|
onResume: (data) {
|
||||||
|
Logger.d("Notification [onResume]: $data");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
||||||
|
|
||||||
|
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
||||||
|
var initializationSettingsAndroid =
|
||||||
|
new AndroidInitializationSettings('mini_icon');
|
||||||
|
var initializationSettingsIOS = new IOSInitializationSettings(
|
||||||
|
onDidReceiveLocalNotification: null);
|
||||||
|
var initializationSettings = new InitializationSettings(
|
||||||
|
initializationSettingsAndroid, initializationSettingsIOS);
|
||||||
|
flutterLocalNotificationsPlugin.initialize(initializationSettings,
|
||||||
|
onSelectNotification: onSelectNotification);
|
||||||
|
|
||||||
_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) {
|
||||||
_reLoad();
|
_fullLoad();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_initialLoad();
|
_fullLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initialLoad() {
|
Future onSelectNotification(String payload) async {
|
||||||
|
if (payload != null) {
|
||||||
|
Logger.d('Notification clicked: ' + payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _showNotification({String title, String text}) async {
|
||||||
|
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
|
||||||
|
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
|
||||||
|
importance: Importance.Max, priority: Priority.High);
|
||||||
|
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
|
||||||
|
var platformChannelSpecifics = new NotificationDetails(
|
||||||
|
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
0,
|
||||||
|
title ?? appName,
|
||||||
|
text,
|
||||||
|
platformChannelSpecifics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fullLoad() async {
|
||||||
_showInfoBottomBar(progress: true,);
|
_showInfoBottomBar(progress: true,);
|
||||||
_subscribe();
|
_subscribe().then((_) {
|
||||||
widget.homeAssistant.init().then((_){
|
Connection().init(loadSettings: true, forceReconnect: true).then((__){
|
||||||
_fetchData();
|
_fetchData();
|
||||||
}, onError: (e) {
|
}, onError: (e) {
|
||||||
_setErrorState(e);
|
_setErrorState(e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _reLoad() {
|
void _quickLoad() {
|
||||||
_hideBottomBar();
|
_hideBottomBar();
|
||||||
_showInfoBottomBar(progress: true,);
|
_showInfoBottomBar(progress: true,);
|
||||||
widget.homeAssistant.init().then((_){
|
Connection().init(loadSettings: false, forceReconnect: false).then((_){
|
||||||
_fetchData();
|
_fetchData();
|
||||||
}, onError: (e) {
|
}, onError: (e) {
|
||||||
_setErrorState(e);
|
_setErrorState(e);
|
||||||
@ -208,7 +269,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fetchData() async {
|
_fetchData() async {
|
||||||
await widget.homeAssistant.fetch().then((_) {
|
await widget.homeAssistant.fetchData().then((_) {
|
||||||
_hideBottomBar();
|
_hideBottomBar();
|
||||||
int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0;
|
int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0;
|
||||||
if (_previousViewCount != currentViewCount) {
|
if (_previousViewCount != currentViewCount) {
|
||||||
@ -217,7 +278,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_previousViewCount = currentViewCount;
|
_previousViewCount = currentViewCount;
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_setErrorState(e);
|
if (e is HAError) {
|
||||||
|
_setErrorState(e);
|
||||||
|
} else {
|
||||||
|
_setErrorState(HAError(e.toString()));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
eventBus.fire(RefreshDataFinishedEvent());
|
eventBus.fire(RefreshDataFinishedEvent());
|
||||||
}
|
}
|
||||||
@ -225,17 +290,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
Logger.d("$state");
|
Logger.d("$state");
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed && Connection().settingsLoaded) {
|
||||||
_reLoad();
|
_quickLoad();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_subscribe() {
|
Future _subscribe() {
|
||||||
|
Completer completer = Completer();
|
||||||
if (_stateSubscription == null) {
|
if (_stateSubscription == null) {
|
||||||
_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");
|
||||||
_reLoad();
|
_quickLoad();
|
||||||
} else {
|
} else {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
@ -243,7 +309,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
if (_reloadUISubscription == null) {
|
if (_reloadUISubscription == null) {
|
||||||
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
||||||
_reLoad();
|
_quickLoad();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_showDialogSubscription == null) {
|
||||||
|
_showDialogSubscription = eventBus.on<ShowDialogEvent>().listen((event){
|
||||||
|
_showDialog(
|
||||||
|
title: event.title,
|
||||||
|
body: event.body,
|
||||||
|
onPositive: event.onPositive,
|
||||||
|
onNegative: event.onNegative,
|
||||||
|
positiveText: event.positiveText,
|
||||||
|
negativeText: event.negativeText
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (_serviceCallSubscription == null) {
|
if (_serviceCallSubscription == null) {
|
||||||
@ -263,78 +341,88 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
if (_showErrorSubscription == null) {
|
if (_showErrorSubscription == null) {
|
||||||
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
||||||
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
|
_showErrorBottomBar(event.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_startAuthSubscription == null) {
|
if (_startAuthSubscription == null) {
|
||||||
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
|
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
|
||||||
_showOAuth();
|
setState(() {
|
||||||
|
_showLoginButton = event.showButton;
|
||||||
|
});
|
||||||
|
if (event.showButton) {
|
||||||
|
_showOAuth();
|
||||||
|
} else {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_firebaseMessaging.getToken().then((String token) {
|
||||||
|
HomeAssistant().fcmToken = token;
|
||||||
/*_firebaseMessaging.getToken().then((String token) {
|
completer.complete();
|
||||||
//Logger.d("FCM token: $token");
|
|
||||||
widget.homeAssistant.sendHTTPPost(
|
|
||||||
endPoint: '/api/notify.fcm-android',
|
|
||||||
jsonData: '{"token": "$token"}'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
_firebaseMessaging.configure(
|
return completer.future;
|
||||||
onLaunch: (data) {
|
|
||||||
Logger.d("Notification [onLaunch]: $data");
|
|
||||||
},
|
|
||||||
onMessage: (data) {
|
|
||||||
Logger.d("Notification [onMessage]: $data");
|
|
||||||
},
|
|
||||||
onResume: (data) {
|
|
||||||
Logger.d("Notification [onResume]: $data");
|
|
||||||
}
|
|
||||||
);*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showOAuth() {
|
void _showOAuth() {
|
||||||
Navigator.push(
|
Logger.d("_showOAuth: ${Connection().oauthUrl}");
|
||||||
context,
|
Navigator.of(context).pushNamed('/login');
|
||||||
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(HAError e) {
|
||||||
if (e is Error) {
|
if (e == null) {
|
||||||
Logger.e(e.toString());
|
|
||||||
Logger.e("${e.stackTrace}");
|
|
||||||
_showErrorBottomBar(
|
_showErrorBottomBar(
|
||||||
message: "Unknown error",
|
HAError("Unknown error")
|
||||||
errorCode: 13
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_showErrorBottomBar(
|
_showErrorBottomBar(e);
|
||||||
message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error",
|
|
||||||
errorCode: e["errorCode"] != null ? e["errorCode"] : 99
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) {
|
||||||
|
List<Widget> buttons = [];
|
||||||
|
buttons.add(FlatButton(
|
||||||
|
child: new Text("$positiveText"),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
if (onPositive != null) {
|
||||||
|
onPositive();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
if (negativeText != null) {
|
||||||
|
buttons.add(FlatButton(
|
||||||
|
child: new Text("$negativeText"),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
if (onNegative != null) {
|
||||||
|
onNegative();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// flutter defined function
|
||||||
|
showDialog(
|
||||||
|
barrierDismissible: false,
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
// return object of type Dialog
|
||||||
|
return AlertDialog(
|
||||||
|
title: new Text("$title"),
|
||||||
|
content: new Text("$body"),
|
||||||
|
actions: buttons,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _callService(String domain, String service, String entityId, Map additionalParams) {
|
void _callService(String domain, String service, String entityId, Map additionalParams) {
|
||||||
_showInfoBottomBar(
|
_showInfoBottomBar(
|
||||||
message: "Calling $domain.$service",
|
message: "Calling $domain.$service",
|
||||||
duration: Duration(seconds: 3)
|
duration: Duration(seconds: 3)
|
||||||
);
|
);
|
||||||
widget.homeAssistant.connection.callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
|
Connection().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showEntityPage(String entityId) {
|
void _showEntityPage(String entityId) {
|
||||||
@ -363,7 +451,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
menuItems.add(
|
menuItems.add(
|
||||||
UserAccountsDrawerHeader(
|
UserAccountsDrawerHeader(
|
||||||
accountName: Text(widget.homeAssistant.userName),
|
accountName: Text(widget.homeAssistant.userName),
|
||||||
accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"),
|
accountEmail: Text(Connection().displayHostname ?? "Not configured"),
|
||||||
/*onDetailsPressed: () {
|
/*onDetailsPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_accountMenuExpanded = !_accountMenuExpanded;
|
_accountMenuExpanded = !_accountMenuExpanded;
|
||||||
@ -386,7 +474,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
||||||
title: Text("${panel.title}"),
|
title: Text("${panel.title}"),
|
||||||
onTap: () => panel.handleOpen(context)
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
panel.handleOpen(context);
|
||||||
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -397,7 +488,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
|
||||||
title: Text("Open Web UI"),
|
title: Text("Open Web UI"),
|
||||||
onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost),
|
onTap: () => HAUtils.launchURL(Connection().httpWebHost),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
menuItems.addAll([
|
menuItems.addAll([
|
||||||
@ -430,9 +521,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
Divider(),
|
Divider(),
|
||||||
|
new ListTile(
|
||||||
|
leading: Icon(Icons.help),
|
||||||
|
title: Text("Help"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
HAUtils.launchURL("http://ha-client.homemade.systems/docs");
|
||||||
|
},
|
||||||
|
),
|
||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
||||||
title: Text("Join Discord server"),
|
title: Text("Join Discord channel"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
||||||
@ -452,6 +551,38 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 10.0,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: "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: context, url: "http://ha-client.homemade.systems/privacy_policy");
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Privacy Policy",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
applicationName: appName,
|
applicationName: appName,
|
||||||
@ -495,103 +626,70 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showErrorBottomBar({Key key, @required String message, @required int errorCode}) {
|
void _showErrorBottomBar(HAError error) {
|
||||||
TextStyle textStyle = TextStyle(
|
TextStyle textStyle = TextStyle(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
fontSize: Sizes.nameFontSize
|
fontSize: Sizes.nameFontSize
|
||||||
);
|
);
|
||||||
_bottomBarColor = Colors.red.shade100;
|
_bottomBarColor = Colors.red.shade100;
|
||||||
switch (errorCode) {
|
List<Widget> actions = [];
|
||||||
case 9:
|
error.actions.forEach((HAErrorAction action) {
|
||||||
case 11:
|
switch (action.type) {
|
||||||
case 7:
|
case HAErrorActionType.FULL_RELOAD: {
|
||||||
case 1: {
|
actions.add(FlatButton(
|
||||||
_bottomBarAction = FlatButton(
|
child: Text("${action.title}", style: textStyle),
|
||||||
child: Text("Retry", style: textStyle),
|
onPressed: () {
|
||||||
onPressed: () {
|
_fullLoad();
|
||||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
},
|
||||||
_reLoad();
|
));
|
||||||
},
|
break;
|
||||||
);
|
}
|
||||||
break;
|
|
||||||
}
|
case HAErrorActionType.QUICK_RELOAD: {
|
||||||
|
actions.add(FlatButton(
|
||||||
case 5: {
|
child: Text("${action.title}", style: textStyle),
|
||||||
message = "Check connection settings";
|
onPressed: () {
|
||||||
_bottomBarAction = FlatButton(
|
_quickLoad();
|
||||||
child: Text("Open", style: textStyle),
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.URL: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: "${action.url}");
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
|
||||||
Navigator.pushNamed(context, '/connection-settings');
|
Navigator.pushNamed(context, '/connection-settings');
|
||||||
},
|
},
|
||||||
);
|
));
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 60: {
|
|
||||||
_bottomBarAction = FlatButton(
|
|
||||||
child: Text("Login", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_reLoad();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 63:
|
|
||||||
case 61: {
|
|
||||||
_bottomBarAction = FlatButton(
|
|
||||||
child: Text("Try again", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_reLoad();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 62: {
|
|
||||||
_bottomBarAction = FlatButton(
|
|
||||||
child: Text("Login again", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_reLoad();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 10: {
|
|
||||||
_bottomBarAction = FlatButton(
|
|
||||||
child: Text("Refresh", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
|
||||||
_reLoad();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 82:
|
|
||||||
case 81:
|
|
||||||
case 8: {
|
|
||||||
_bottomBarAction = FlatButton(
|
|
||||||
child: Text("Reconnect", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_reLoad();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
_bottomBarAction = Container(width: 0.0, height: 0.0,);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState(() {
|
});
|
||||||
_bottomBarProgress = false;
|
if (actions.isNotEmpty) {
|
||||||
_bottomBarText = "$message";
|
_bottomBarAction = Row(
|
||||||
_showBottomBar = true;
|
mainAxisSize: MainAxisSize.min,
|
||||||
});
|
children: actions,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_bottomBarProgress = false;
|
||||||
|
_bottomBarText = "${error.message}";
|
||||||
|
_showBottomBar = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
@ -602,13 +700,26 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
child: new Text("Reload"),
|
child: new Text("Reload"),
|
||||||
value: "reload",
|
value: "reload",
|
||||||
));
|
));
|
||||||
if (widget.homeAssistant.connection.isAuthenticated) {
|
List<Widget> emptyBody = [
|
||||||
|
Text("."),
|
||||||
|
];
|
||||||
|
if (Connection().isAuthenticated) {
|
||||||
|
_showLoginButton = false;
|
||||||
popupMenuItems.add(
|
popupMenuItems.add(
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
child: new Text("Logout"),
|
child: new Text("Logout"),
|
||||||
value: "logout",
|
value: "logout",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if (_showLoginButton) {
|
||||||
|
emptyBody = [
|
||||||
|
FlatButton(
|
||||||
|
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
||||||
|
color: Colors.blue,
|
||||||
|
onPressed: () => _fullLoad(),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
return NestedScrollView(
|
return NestedScrollView(
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
@ -628,10 +739,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
items: popupMenuItems
|
items: popupMenuItems
|
||||||
).then((String val) {
|
).then((String val) {
|
||||||
if (val == "reload") {
|
if (val == "reload") {
|
||||||
_reLoad();
|
_quickLoad();
|
||||||
} else if (val == "logout") {
|
} else if (val == "logout") {
|
||||||
widget.homeAssistant.logout().then((_) {
|
widget.homeAssistant.logout().then((_) {
|
||||||
_reLoad();
|
_quickLoad();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -657,13 +768,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
Center(
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: emptyBody
|
||||||
Icon(
|
|
||||||
MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"),
|
|
||||||
size: 100.0,
|
|
||||||
color: Colors.black26,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
:
|
:
|
||||||
@ -738,10 +843,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
drawer: _buildAppDrawer(),
|
drawer: _buildAppDrawer(),
|
||||||
primary: false,
|
primary: false,
|
||||||
bottomNavigationBar: bottomBar,
|
bottomNavigationBar: bottomBar,
|
||||||
body: HomeAssistantModel(
|
body: _buildScaffoldBody(false),
|
||||||
child: _buildScaffoldBody(false),
|
|
||||||
homeAssistant: widget.homeAssistant
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -755,6 +857,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_stateSubscription?.cancel();
|
_stateSubscription?.cancel();
|
||||||
_settingsSubscription?.cancel();
|
_settingsSubscription?.cancel();
|
||||||
_serviceCallSubscription?.cancel();
|
_serviceCallSubscription?.cancel();
|
||||||
|
_showDialogSubscription?.cancel();
|
||||||
_showEntityPageSubscription?.cancel();
|
_showEntityPageSubscription?.cancel();
|
||||||
_showErrorSubscription?.cancel();
|
_showErrorSubscription?.cancel();
|
||||||
_startAuthSubscription?.cancel();
|
_startAuthSubscription?.cancel();
|
||||||
|
@ -24,10 +24,12 @@ class MaterialDesignIcons {
|
|||||||
"cover.opening": "mdi:window-open",
|
"cover.opening": "mdi:window-open",
|
||||||
"camera": "mdi:cctv",
|
"camera": "mdi:cctv",
|
||||||
"calendar": "mdi:calendar",
|
"calendar": "mdi:calendar",
|
||||||
|
"device_tracker": "mdi:account",
|
||||||
"timer": "mdi:timer",
|
"timer": "mdi:timer",
|
||||||
"lock.locked": "mdi:lock",
|
"lock.locked": "mdi:lock",
|
||||||
"lock.unlocked": "mdi:lock-open",
|
"lock.unlocked": "mdi:lock-open",
|
||||||
"fan": "mdi:fan",
|
"fan": "mdi:fan",
|
||||||
|
"remote": "mdi:remote",
|
||||||
"alarm_control_panel.disarmed" : "mdi:bell-outline",
|
"alarm_control_panel.disarmed" : "mdi:bell-outline",
|
||||||
"alarm_control_panel.armed_home" : "mdi:bell-plus",
|
"alarm_control_panel.armed_home" : "mdi:bell-plus",
|
||||||
"alarm_control_panel.armed_away" : "mdi:bell",
|
"alarm_control_panel.armed_away" : "mdi:bell",
|
||||||
@ -64,18 +66,18 @@ class MaterialDesignIcons {
|
|||||||
"binary_sensor.occupancy.off": "mdi:home-outline",
|
"binary_sensor.occupancy.off": "mdi:home-outline",
|
||||||
"binary_sensor.opening.on": "mdi:square-outline",
|
"binary_sensor.opening.on": "mdi:square-outline",
|
||||||
"binary_sensor.opening.off": "mdi:square",
|
"binary_sensor.opening.off": "mdi:square",
|
||||||
//"binary_sensor.plug.on": "mdi:",
|
"binary_sensor.plug.on": "mdi:power-plug",
|
||||||
//"binary_sensor.plug.off": "mdi:",
|
"binary_sensor.plug.off": "mdi:power-plug-off",
|
||||||
"binary_sensor.power.on": "mdi:alert",
|
"binary_sensor.power.on": "mdi:alert",
|
||||||
"binary_sensor.power.off": "mdi:verified",
|
"binary_sensor.power.off": "mdi:shield-check",
|
||||||
//"binary_sensor.presence.on": "mdi:",
|
//"binary_sensor.presence.on": "mdi:",
|
||||||
//"binary_sensor.presence.off": "mdi:",
|
//"binary_sensor.presence.off": "mdi:",
|
||||||
"binary_sensor.problem.on": "mdi:alert-outline",
|
"binary_sensor.problem.on": "mdi:alert-outline",
|
||||||
"binary_sensor.problem.off": "mdi:check-outline",
|
"binary_sensor.problem.off": "mdi:check-outline",
|
||||||
"binary_sensor.safety.on": "mdi:alert",
|
"binary_sensor.safety.on": "mdi:alert",
|
||||||
"binary_sensor.safety.off": "mdi:verified",
|
"binary_sensor.safety.off": "mdi:shield-check",
|
||||||
"binary_sensor.smoke.on": "mdi:alert",
|
"binary_sensor.smoke.on": "mdi:alert",
|
||||||
"binary_sensor.smoke.off": "mdi:verified",
|
"binary_sensor.smoke.off": "mdi:shield-check",
|
||||||
"binary_sensor.sound.on": "mdi:music-note",
|
"binary_sensor.sound.on": "mdi:music-note",
|
||||||
"binary_sensor.sound.off": "mdi:music-note-off",
|
"binary_sensor.sound.off": "mdi:music-note-off",
|
||||||
"binary_sensor.vibration.on": "mdi:vibrate",
|
"binary_sensor.vibration.on": "mdi:vibrate",
|
||||||
@ -96,8 +98,8 @@ class MaterialDesignIcons {
|
|||||||
"cover.window.closing": "mdi:window-open",
|
"cover.window.closing": "mdi:window-open",
|
||||||
"cover.window.opening": "mdi:window-open",
|
"cover.window.opening": "mdi:window-open",
|
||||||
};
|
};
|
||||||
static final Map iconsDataMap = {
|
|
||||||
|
|
||||||
|
static final Map iconsDataMap = {
|
||||||
"mdi:access-point": 0xf002,
|
"mdi:access-point": 0xf002,
|
||||||
"mdi:access-point-network": 0xf003,
|
"mdi:access-point-network": 0xf003,
|
||||||
"mdi:access-point-network-off": 0xfbbd,
|
"mdi:access-point-network-off": 0xfbbd,
|
||||||
@ -111,6 +113,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:account-badge": 0xfd83,
|
"mdi:account-badge": 0xfd83,
|
||||||
"mdi:account-badge-alert": 0xfd84,
|
"mdi:account-badge-alert": 0xfd84,
|
||||||
"mdi:account-badge-alert-outline": 0xfd85,
|
"mdi:account-badge-alert-outline": 0xfd85,
|
||||||
|
"mdi:account-badge-horizontal": 0xfdf0,
|
||||||
|
"mdi:account-badge-horizontal-outline": 0xfdf1,
|
||||||
"mdi:account-badge-outline": 0xfd86,
|
"mdi:account-badge-outline": 0xfd86,
|
||||||
"mdi:account-box": 0xf006,
|
"mdi:account-box": 0xf006,
|
||||||
"mdi:account-box-multiple": 0xf933,
|
"mdi:account-box-multiple": 0xf933,
|
||||||
@ -492,6 +496,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:auto-upload": 0xf069,
|
"mdi:auto-upload": 0xf069,
|
||||||
"mdi:autorenew": 0xf06a,
|
"mdi:autorenew": 0xf06a,
|
||||||
"mdi:av-timer": 0xf06b,
|
"mdi:av-timer": 0xf06b,
|
||||||
|
"mdi:aws": 0xfdf2,
|
||||||
"mdi:axe": 0xf8c7,
|
"mdi:axe": 0xf8c7,
|
||||||
"mdi:axis": 0xfd24,
|
"mdi:axis": 0xfd24,
|
||||||
"mdi:axis-arrow": 0xfd25,
|
"mdi:axis-arrow": 0xfd25,
|
||||||
@ -519,6 +524,10 @@ class MaterialDesignIcons {
|
|||||||
"mdi:backspace-outline": 0xfb38,
|
"mdi:backspace-outline": 0xfb38,
|
||||||
"mdi:backup-restore": 0xf06f,
|
"mdi:backup-restore": 0xf06f,
|
||||||
"mdi:badminton": 0xf850,
|
"mdi:badminton": 0xf850,
|
||||||
|
"mdi:bag-personal": 0xfdf3,
|
||||||
|
"mdi:bag-personal-off": 0xfdf4,
|
||||||
|
"mdi:bag-personal-off-outline": 0xfdf5,
|
||||||
|
"mdi:bag-personal-outline": 0xfdf6,
|
||||||
"mdi:balloon": 0xfa25,
|
"mdi:balloon": 0xfa25,
|
||||||
"mdi:ballot": 0xf9c8,
|
"mdi:ballot": 0xf9c8,
|
||||||
"mdi:ballot-outline": 0xf9c9,
|
"mdi:ballot-outline": 0xf9c9,
|
||||||
@ -626,6 +635,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:bell-sleep-outline": 0xfa92,
|
"mdi:bell-sleep-outline": 0xfa92,
|
||||||
"mdi:beta": 0xf0a1,
|
"mdi:beta": 0xf0a1,
|
||||||
"mdi:betamax": 0xf9ca,
|
"mdi:betamax": 0xf9ca,
|
||||||
|
"mdi:biathlon": 0xfdf7,
|
||||||
"mdi:bible": 0xf0a2,
|
"mdi:bible": 0xf0a2,
|
||||||
"mdi:bike": 0xf0a3,
|
"mdi:bike": 0xf0a3,
|
||||||
"mdi:billiards": 0xfb3d,
|
"mdi:billiards": 0xfb3d,
|
||||||
@ -680,6 +690,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:bookmark-check": 0xf0c1,
|
"mdi:bookmark-check": 0xf0c1,
|
||||||
"mdi:bookmark-minus": 0xf9cb,
|
"mdi:bookmark-minus": 0xf9cb,
|
||||||
"mdi:bookmark-minus-outline": 0xf9cc,
|
"mdi:bookmark-minus-outline": 0xf9cc,
|
||||||
|
"mdi:bookmark-multiple": 0xfdf8,
|
||||||
|
"mdi:bookmark-multiple-outline": 0xfdf9,
|
||||||
"mdi:bookmark-music": 0xf0c2,
|
"mdi:bookmark-music": 0xf0c2,
|
||||||
"mdi:bookmark-off": 0xf9cd,
|
"mdi:bookmark-off": 0xf9cd,
|
||||||
"mdi:bookmark-off-outline": 0xf9ce,
|
"mdi:bookmark-off-outline": 0xf9ce,
|
||||||
@ -791,6 +803,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:calendar-heart": 0xf9d1,
|
"mdi:calendar-heart": 0xf9d1,
|
||||||
"mdi:calendar-import": 0xfb0a,
|
"mdi:calendar-import": 0xfb0a,
|
||||||
"mdi:calendar-minus": 0xfd38,
|
"mdi:calendar-minus": 0xfd38,
|
||||||
|
"mdi:calendar-month": 0xfdfa,
|
||||||
|
"mdi:calendar-month-outline": 0xfdfb,
|
||||||
"mdi:calendar-multiple": 0xf0f1,
|
"mdi:calendar-multiple": 0xf0f1,
|
||||||
"mdi:calendar-multiple-check": 0xf0f2,
|
"mdi:calendar-multiple-check": 0xf0f2,
|
||||||
"mdi:calendar-multiselect": 0xfa31,
|
"mdi:calendar-multiselect": 0xfa31,
|
||||||
@ -837,6 +851,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:camera-party-mode": 0xf105,
|
"mdi:camera-party-mode": 0xf105,
|
||||||
"mdi:camera-rear": 0xf106,
|
"mdi:camera-rear": 0xf106,
|
||||||
"mdi:camera-rear-variant": 0xf107,
|
"mdi:camera-rear-variant": 0xf107,
|
||||||
|
"mdi:camera-retake": 0xfdfc,
|
||||||
|
"mdi:camera-retake-outline": 0xfdfd,
|
||||||
"mdi:camera-switch": 0xf108,
|
"mdi:camera-switch": 0xf108,
|
||||||
"mdi:camera-timer": 0xf109,
|
"mdi:camera-timer": 0xf109,
|
||||||
"mdi:camera-wireless": 0xfd92,
|
"mdi:camera-wireless": 0xfd92,
|
||||||
@ -847,6 +863,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:cannabis": 0xf7a5,
|
"mdi:cannabis": 0xf7a5,
|
||||||
"mdi:caps-lock": 0xfa9a,
|
"mdi:caps-lock": 0xfa9a,
|
||||||
"mdi:car": 0xf10b,
|
"mdi:car": 0xf10b,
|
||||||
|
"mdi:car-back": 0xfdfe,
|
||||||
"mdi:car-battery": 0xf10c,
|
"mdi:car-battery": 0xf10c,
|
||||||
"mdi:car-brake-abs": 0xfc23,
|
"mdi:car-brake-abs": 0xfc23,
|
||||||
"mdi:car-brake-alert": 0xfc24,
|
"mdi:car-brake-alert": 0xfc24,
|
||||||
@ -868,6 +885,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:car-light-high": 0xfc28,
|
"mdi:car-light-high": 0xfc28,
|
||||||
"mdi:car-limousine": 0xf8cc,
|
"mdi:car-limousine": 0xf8cc,
|
||||||
"mdi:car-multiple": 0xfb4a,
|
"mdi:car-multiple": 0xfb4a,
|
||||||
|
"mdi:car-off": 0xfdff,
|
||||||
"mdi:car-parking-lights": 0xfd3f,
|
"mdi:car-parking-lights": 0xfd3f,
|
||||||
"mdi:car-pickup": 0xf7a9,
|
"mdi:car-pickup": 0xf7a9,
|
||||||
"mdi:car-side": 0xf7aa,
|
"mdi:car-side": 0xf7aa,
|
||||||
@ -916,6 +934,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:cassette": 0xf9d3,
|
"mdi:cassette": 0xf9d3,
|
||||||
"mdi:cast": 0xf118,
|
"mdi:cast": 0xf118,
|
||||||
"mdi:cast-connected": 0xf119,
|
"mdi:cast-connected": 0xf119,
|
||||||
|
"mdi:cast-education": 0xfe6d,
|
||||||
"mdi:cast-off": 0xf789,
|
"mdi:cast-off": 0xf789,
|
||||||
"mdi:castle": 0xf11a,
|
"mdi:castle": 0xf11a,
|
||||||
"mdi:cat": 0xf11b,
|
"mdi:cat": 0xf11b,
|
||||||
@ -966,6 +985,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:chat-processing": 0xfb57,
|
"mdi:chat-processing": 0xfb57,
|
||||||
"mdi:check": 0xf12c,
|
"mdi:check": 0xf12c,
|
||||||
"mdi:check-all": 0xf12d,
|
"mdi:check-all": 0xf12d,
|
||||||
|
"mdi:check-bold": 0xfe6e,
|
||||||
"mdi:check-box-multiple-outline": 0xfc2d,
|
"mdi:check-box-multiple-outline": 0xfc2d,
|
||||||
"mdi:check-box-outline": 0xfc2e,
|
"mdi:check-box-outline": 0xfc2e,
|
||||||
"mdi:check-circle": 0xf5e0,
|
"mdi:check-circle": 0xf5e0,
|
||||||
@ -974,6 +994,9 @@ class MaterialDesignIcons {
|
|||||||
"mdi:check-network": 0xfc2f,
|
"mdi:check-network": 0xfc2f,
|
||||||
"mdi:check-network-outline": 0xfc30,
|
"mdi:check-network-outline": 0xfc30,
|
||||||
"mdi:check-outline": 0xf854,
|
"mdi:check-outline": 0xf854,
|
||||||
|
"mdi:check-underline": 0xfe70,
|
||||||
|
"mdi:check-underline-circle": 0xfe71,
|
||||||
|
"mdi:check-underline-circle-outline": 0xfe72,
|
||||||
"mdi:checkbook": 0xfa9c,
|
"mdi:checkbook": 0xfa9c,
|
||||||
"mdi:checkbox-blank": 0xf12e,
|
"mdi:checkbox-blank": 0xf12e,
|
||||||
"mdi:checkbox-blank-circle": 0xf12f,
|
"mdi:checkbox-blank-circle": 0xf12f,
|
||||||
@ -1049,6 +1072,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:circle-slice-7": 0xfaa3,
|
"mdi:circle-slice-7": 0xfaa3,
|
||||||
"mdi:circle-slice-8": 0xfaa4,
|
"mdi:circle-slice-8": 0xfaa4,
|
||||||
"mdi:circle-small": 0xf9de,
|
"mdi:circle-small": 0xf9de,
|
||||||
|
"mdi:circular-saw": 0xfe73,
|
||||||
"mdi:cisco-webex": 0xf145,
|
"mdi:cisco-webex": 0xf145,
|
||||||
"mdi:city": 0xf146,
|
"mdi:city": 0xf146,
|
||||||
"mdi:city-variant": 0xfa35,
|
"mdi:city-variant": 0xfa35,
|
||||||
@ -1148,6 +1172,11 @@ class MaterialDesignIcons {
|
|||||||
"mdi:collapse-all": 0xfaa5,
|
"mdi:collapse-all": 0xfaa5,
|
||||||
"mdi:collapse-all-outline": 0xfaa6,
|
"mdi:collapse-all-outline": 0xfaa6,
|
||||||
"mdi:color-helper": 0xf179,
|
"mdi:color-helper": 0xf179,
|
||||||
|
"mdi:comma": 0xfe74,
|
||||||
|
"mdi:comma-box": 0xfe75,
|
||||||
|
"mdi:comma-box-outline": 0xfe76,
|
||||||
|
"mdi:comma-circle": 0xfe77,
|
||||||
|
"mdi:comma-circle-outline": 0xfe78,
|
||||||
"mdi:comment": 0xf17a,
|
"mdi:comment": 0xf17a,
|
||||||
"mdi:comment-account": 0xf17b,
|
"mdi:comment-account": 0xf17b,
|
||||||
"mdi:comment-account-outline": 0xf17c,
|
"mdi:comment-account-outline": 0xf17c,
|
||||||
@ -1201,6 +1230,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:content-save-all": 0xf194,
|
"mdi:content-save-all": 0xf194,
|
||||||
"mdi:content-save-edit": 0xfcd7,
|
"mdi:content-save-edit": 0xfcd7,
|
||||||
"mdi:content-save-edit-outline": 0xfcd8,
|
"mdi:content-save-edit-outline": 0xfcd8,
|
||||||
|
"mdi:content-save-move": 0xfe79,
|
||||||
|
"mdi:content-save-move-outline": 0xfe7a,
|
||||||
"mdi:content-save-outline": 0xf817,
|
"mdi:content-save-outline": 0xf817,
|
||||||
"mdi:content-save-settings": 0xf61b,
|
"mdi:content-save-settings": 0xf61b,
|
||||||
"mdi:content-save-settings-outline": 0xfb13,
|
"mdi:content-save-settings-outline": 0xfb13,
|
||||||
@ -1546,6 +1577,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:file-cancel-outline": 0xfda3,
|
"mdi:file-cancel-outline": 0xfda3,
|
||||||
"mdi:file-chart": 0xf215,
|
"mdi:file-chart": 0xf215,
|
||||||
"mdi:file-check": 0xf216,
|
"mdi:file-check": 0xf216,
|
||||||
|
"mdi:file-check-outline": 0xfe7b,
|
||||||
"mdi:file-cloud": 0xf217,
|
"mdi:file-cloud": 0xf217,
|
||||||
"mdi:file-compare": 0xf8a9,
|
"mdi:file-compare": 0xf8a9,
|
||||||
"mdi:file-delimited": 0xf218,
|
"mdi:file-delimited": 0xf218,
|
||||||
@ -1573,9 +1605,11 @@ class MaterialDesignIcons {
|
|||||||
"mdi:file-move": 0xfab8,
|
"mdi:file-move": 0xfab8,
|
||||||
"mdi:file-multiple": 0xf222,
|
"mdi:file-multiple": 0xf222,
|
||||||
"mdi:file-music": 0xf223,
|
"mdi:file-music": 0xf223,
|
||||||
|
"mdi:file-music-outline": 0xfe7c,
|
||||||
"mdi:file-outline": 0xf224,
|
"mdi:file-outline": 0xf224,
|
||||||
"mdi:file-pdf": 0xf225,
|
"mdi:file-pdf": 0xf225,
|
||||||
"mdi:file-pdf-box": 0xf226,
|
"mdi:file-pdf-box": 0xf226,
|
||||||
|
"mdi:file-pdf-outline": 0xfe7d,
|
||||||
"mdi:file-percent": 0xf81d,
|
"mdi:file-percent": 0xf81d,
|
||||||
"mdi:file-plus": 0xf751,
|
"mdi:file-plus": 0xf751,
|
||||||
"mdi:file-powerpoint": 0xf227,
|
"mdi:file-powerpoint": 0xf227,
|
||||||
@ -1596,6 +1630,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:file-upload": 0xfa4c,
|
"mdi:file-upload": 0xfa4c,
|
||||||
"mdi:file-upload-outline": 0xfa4d,
|
"mdi:file-upload-outline": 0xfa4d,
|
||||||
"mdi:file-video": 0xf22b,
|
"mdi:file-video": 0xf22b,
|
||||||
|
"mdi:file-video-outline": 0xfe10,
|
||||||
"mdi:file-word": 0xf22c,
|
"mdi:file-word": 0xf22c,
|
||||||
"mdi:file-word-box": 0xf22d,
|
"mdi:file-word-box": 0xf22d,
|
||||||
"mdi:file-xml": 0xf22e,
|
"mdi:file-xml": 0xf22e,
|
||||||
@ -1614,6 +1649,9 @@ class MaterialDesignIcons {
|
|||||||
"mdi:fire-truck": 0xf8aa,
|
"mdi:fire-truck": 0xf8aa,
|
||||||
"mdi:firebase": 0xf966,
|
"mdi:firebase": 0xf966,
|
||||||
"mdi:firefox": 0xf239,
|
"mdi:firefox": 0xf239,
|
||||||
|
"mdi:fireplace": 0xfe11,
|
||||||
|
"mdi:fireplace-off": 0xfe12,
|
||||||
|
"mdi:firework": 0xfe13,
|
||||||
"mdi:fish": 0xf23a,
|
"mdi:fish": 0xf23a,
|
||||||
"mdi:flag": 0xf23b,
|
"mdi:flag": 0xf23b,
|
||||||
"mdi:flag-checkered": 0xf23c,
|
"mdi:flag-checkered": 0xf23c,
|
||||||
@ -1715,6 +1753,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:format-bold": 0xf264,
|
"mdi:format-bold": 0xf264,
|
||||||
"mdi:format-clear": 0xf265,
|
"mdi:format-clear": 0xf265,
|
||||||
"mdi:format-color-fill": 0xf266,
|
"mdi:format-color-fill": 0xf266,
|
||||||
|
"mdi:format-color-highlight": 0xfe14,
|
||||||
"mdi:format-color-text": 0xf69d,
|
"mdi:format-color-text": 0xf69d,
|
||||||
"mdi:format-columns": 0xf8de,
|
"mdi:format-columns": 0xf8de,
|
||||||
"mdi:format-float-center": 0xf267,
|
"mdi:format-float-center": 0xf267,
|
||||||
@ -1769,6 +1808,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:format-text": 0xf284,
|
"mdi:format-text": 0xf284,
|
||||||
"mdi:format-text-rotation-down": 0xfd4f,
|
"mdi:format-text-rotation-down": 0xfd4f,
|
||||||
"mdi:format-text-rotation-none": 0xfd50,
|
"mdi:format-text-rotation-none": 0xfd50,
|
||||||
|
"mdi:format-text-variant": 0xfe15,
|
||||||
"mdi:format-text-wrapping-clip": 0xfcea,
|
"mdi:format-text-wrapping-clip": 0xfcea,
|
||||||
"mdi:format-text-wrapping-overflow": 0xfceb,
|
"mdi:format-text-wrapping-overflow": 0xfceb,
|
||||||
"mdi:format-text-wrapping-wrap": 0xfcec,
|
"mdi:format-text-wrapping-wrap": 0xfcec,
|
||||||
@ -1805,6 +1845,22 @@ class MaterialDesignIcons {
|
|||||||
"mdi:fuse": 0xfc61,
|
"mdi:fuse": 0xfc61,
|
||||||
"mdi:fuse-blade": 0xfc62,
|
"mdi:fuse-blade": 0xfc62,
|
||||||
"mdi:gamepad": 0xf296,
|
"mdi:gamepad": 0xf296,
|
||||||
|
"mdi:gamepad-circle": 0xfe16,
|
||||||
|
"mdi:gamepad-circle-down": 0xfe17,
|
||||||
|
"mdi:gamepad-circle-left": 0xfe18,
|
||||||
|
"mdi:gamepad-circle-outline": 0xfe19,
|
||||||
|
"mdi:gamepad-circle-right": 0xfe1a,
|
||||||
|
"mdi:gamepad-circle-up": 0xfe1b,
|
||||||
|
"mdi:gamepad-down": 0xfe1c,
|
||||||
|
"mdi:gamepad-left": 0xfe1d,
|
||||||
|
"mdi:gamepad-right": 0xfe1e,
|
||||||
|
"mdi:gamepad-round": 0xfe1f,
|
||||||
|
"mdi:gamepad-round-down": 0xfe7e,
|
||||||
|
"mdi:gamepad-round-left": 0xfe7f,
|
||||||
|
"mdi:gamepad-round-outline": 0xfe80,
|
||||||
|
"mdi:gamepad-round-right": 0xfe81,
|
||||||
|
"mdi:gamepad-round-up": 0xfe82,
|
||||||
|
"mdi:gamepad-up": 0xfe83,
|
||||||
"mdi:gamepad-variant": 0xf297,
|
"mdi:gamepad-variant": 0xf297,
|
||||||
"mdi:gantry-crane": 0xfdad,
|
"mdi:gantry-crane": 0xfdad,
|
||||||
"mdi:garage": 0xf6d8,
|
"mdi:garage": 0xf6d8,
|
||||||
@ -1820,6 +1876,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:gate-or": 0xf8e4,
|
"mdi:gate-or": 0xf8e4,
|
||||||
"mdi:gate-xnor": 0xf8e5,
|
"mdi:gate-xnor": 0xf8e5,
|
||||||
"mdi:gate-xor": 0xf8e6,
|
"mdi:gate-xor": 0xf8e6,
|
||||||
|
"mdi:gatsby": 0xfe84,
|
||||||
"mdi:gauge": 0xf29a,
|
"mdi:gauge": 0xf29a,
|
||||||
"mdi:gauge-empty": 0xf872,
|
"mdi:gauge-empty": 0xf872,
|
||||||
"mdi:gauge-full": 0xf873,
|
"mdi:gauge-full": 0xf873,
|
||||||
@ -1848,7 +1905,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:ghost": 0xf2a0,
|
"mdi:ghost": 0xf2a0,
|
||||||
"mdi:ghost-off": 0xf9f4,
|
"mdi:ghost-off": 0xf9f4,
|
||||||
"mdi:gif": 0xfd54,
|
"mdi:gif": 0xfd54,
|
||||||
"mdi:gift": 0xf2a1,
|
"mdi:gift": 0xfe85,
|
||||||
|
"mdi:gift-outline": 0xf2a1,
|
||||||
"mdi:git": 0xf2a2,
|
"mdi:git": 0xf2a2,
|
||||||
"mdi:github-box": 0xf2a3,
|
"mdi:github-box": 0xf2a3,
|
||||||
"mdi:github-circle": 0xf2a4,
|
"mdi:github-circle": 0xf2a4,
|
||||||
@ -1915,6 +1973,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:grid": 0xf2c1,
|
"mdi:grid": 0xf2c1,
|
||||||
"mdi:grid-large": 0xf757,
|
"mdi:grid-large": 0xf757,
|
||||||
"mdi:grid-off": 0xf2c2,
|
"mdi:grid-off": 0xf2c2,
|
||||||
|
"mdi:grill": 0xfe86,
|
||||||
"mdi:group": 0xf2c3,
|
"mdi:group": 0xf2c3,
|
||||||
"mdi:guitar-acoustic": 0xf770,
|
"mdi:guitar-acoustic": 0xf770,
|
||||||
"mdi:guitar-electric": 0xf2c4,
|
"mdi:guitar-electric": 0xf2c4,
|
||||||
@ -1927,6 +1986,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:hamburger": 0xf684,
|
"mdi:hamburger": 0xf684,
|
||||||
"mdi:hammer": 0xf8e9,
|
"mdi:hammer": 0xf8e9,
|
||||||
"mdi:hand": 0xfa4e,
|
"mdi:hand": 0xfa4e,
|
||||||
|
"mdi:hand-left": 0xfe87,
|
||||||
"mdi:hand-okay": 0xfa4f,
|
"mdi:hand-okay": 0xfa4f,
|
||||||
"mdi:hand-peace": 0xfa50,
|
"mdi:hand-peace": 0xfa50,
|
||||||
"mdi:hand-peace-variant": 0xfa51,
|
"mdi:hand-peace-variant": 0xfa51,
|
||||||
@ -1934,6 +1994,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:hand-pointing-left": 0xfa53,
|
"mdi:hand-pointing-left": 0xfa53,
|
||||||
"mdi:hand-pointing-right": 0xf2c7,
|
"mdi:hand-pointing-right": 0xf2c7,
|
||||||
"mdi:hand-pointing-up": 0xfa54,
|
"mdi:hand-pointing-up": 0xfa54,
|
||||||
|
"mdi:hand-right": 0xfe88,
|
||||||
|
"mdi:hand-saw": 0xfe89,
|
||||||
"mdi:hanger": 0xf2c8,
|
"mdi:hanger": 0xf2c8,
|
||||||
"mdi:hard-hat": 0xf96e,
|
"mdi:hard-hat": 0xf96e,
|
||||||
"mdi:harddisk": 0xf2ca,
|
"mdi:harddisk": 0xf2ca,
|
||||||
@ -2070,6 +2132,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:image-filter-none": 0xf2f6,
|
"mdi:image-filter-none": 0xf2f6,
|
||||||
"mdi:image-filter-tilt-shift": 0xf2f7,
|
"mdi:image-filter-tilt-shift": 0xf2f7,
|
||||||
"mdi:image-filter-vintage": 0xf2f8,
|
"mdi:image-filter-vintage": 0xf2f8,
|
||||||
|
"mdi:image-frame": 0xfe8a,
|
||||||
"mdi:image-move": 0xf9f7,
|
"mdi:image-move": 0xf9f7,
|
||||||
"mdi:image-multiple": 0xf2f9,
|
"mdi:image-multiple": 0xf2f9,
|
||||||
"mdi:image-off": 0xf82a,
|
"mdi:image-off": 0xf82a,
|
||||||
@ -2095,6 +2158,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:instapaper": 0xf2ff,
|
"mdi:instapaper": 0xf2ff,
|
||||||
"mdi:internet-explorer": 0xf300,
|
"mdi:internet-explorer": 0xf300,
|
||||||
"mdi:invert-colors": 0xf301,
|
"mdi:invert-colors": 0xf301,
|
||||||
|
"mdi:invert-colors-off": 0xfe8b,
|
||||||
"mdi:ip": 0xfa5e,
|
"mdi:ip": 0xfa5e,
|
||||||
"mdi:ip-network": 0xfa5f,
|
"mdi:ip-network": 0xfa5f,
|
||||||
"mdi:ip-network-outline": 0xfc6c,
|
"mdi:ip-network-outline": 0xfc6c,
|
||||||
@ -2124,6 +2188,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:keyboard-caps": 0xf30e,
|
"mdi:keyboard-caps": 0xf30e,
|
||||||
"mdi:keyboard-close": 0xf30f,
|
"mdi:keyboard-close": 0xf30f,
|
||||||
"mdi:keyboard-off": 0xf310,
|
"mdi:keyboard-off": 0xf310,
|
||||||
|
"mdi:keyboard-off-outline": 0xfe8c,
|
||||||
"mdi:keyboard-outline": 0xf97a,
|
"mdi:keyboard-outline": 0xf97a,
|
||||||
"mdi:keyboard-return": 0xf311,
|
"mdi:keyboard-return": 0xf311,
|
||||||
"mdi:keyboard-settings": 0xf9f8,
|
"mdi:keyboard-settings": 0xf9f8,
|
||||||
@ -2175,9 +2240,12 @@ class MaterialDesignIcons {
|
|||||||
"mdi:launch": 0xf327,
|
"mdi:launch": 0xf327,
|
||||||
"mdi:lava-lamp": 0xf7d4,
|
"mdi:lava-lamp": 0xf7d4,
|
||||||
"mdi:layers": 0xf328,
|
"mdi:layers": 0xf328,
|
||||||
|
"mdi:layers-minus": 0xfe8d,
|
||||||
"mdi:layers-off": 0xf329,
|
"mdi:layers-off": 0xf329,
|
||||||
"mdi:layers-off-outline": 0xf9fc,
|
"mdi:layers-off-outline": 0xf9fc,
|
||||||
"mdi:layers-outline": 0xf9fd,
|
"mdi:layers-outline": 0xf9fd,
|
||||||
|
"mdi:layers-plus": 0xfe30,
|
||||||
|
"mdi:layers-remove": 0xfe31,
|
||||||
"mdi:lead-pencil": 0xf64f,
|
"mdi:lead-pencil": 0xf64f,
|
||||||
"mdi:leaf": 0xf32a,
|
"mdi:leaf": 0xf32a,
|
||||||
"mdi:leaf-maple": 0xfc6f,
|
"mdi:leaf-maple": 0xfc6f,
|
||||||
@ -2202,6 +2270,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:lifebuoy": 0xf87d,
|
"mdi:lifebuoy": 0xf87d,
|
||||||
"mdi:light-switch": 0xf97d,
|
"mdi:light-switch": 0xf97d,
|
||||||
"mdi:lightbulb": 0xf335,
|
"mdi:lightbulb": 0xf335,
|
||||||
|
"mdi:lightbulb-off": 0xfe32,
|
||||||
|
"mdi:lightbulb-off-outline": 0xfe33,
|
||||||
"mdi:lightbulb-on": 0xf6e7,
|
"mdi:lightbulb-on": 0xf6e7,
|
||||||
"mdi:lightbulb-on-outline": 0xf6e8,
|
"mdi:lightbulb-on-outline": 0xf6e8,
|
||||||
"mdi:lightbulb-outline": 0xf336,
|
"mdi:lightbulb-outline": 0xf336,
|
||||||
@ -2374,6 +2444,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:monitor-lock": 0xfdb7,
|
"mdi:monitor-lock": 0xfdb7,
|
||||||
"mdi:monitor-multiple": 0xf37a,
|
"mdi:monitor-multiple": 0xf37a,
|
||||||
"mdi:monitor-off": 0xfd6c,
|
"mdi:monitor-off": 0xfd6c,
|
||||||
|
"mdi:monitor-screenshot": 0xfe34,
|
||||||
"mdi:monitor-star": 0xfdb8,
|
"mdi:monitor-star": 0xfdb8,
|
||||||
"mdi:more": 0xf37b,
|
"mdi:more": 0xf37b,
|
||||||
"mdi:mother-nurse": 0xfcfd,
|
"mdi:mother-nurse": 0xfcfd,
|
||||||
@ -2437,8 +2508,11 @@ class MaterialDesignIcons {
|
|||||||
"mdi:new-box": 0xf394,
|
"mdi:new-box": 0xf394,
|
||||||
"mdi:newspaper": 0xf395,
|
"mdi:newspaper": 0xf395,
|
||||||
"mdi:nfc": 0xf396,
|
"mdi:nfc": 0xf396,
|
||||||
|
"mdi:nfc-off": 0xfe35,
|
||||||
|
"mdi:nfc-search-variant": 0xfe36,
|
||||||
"mdi:nfc-tap": 0xf397,
|
"mdi:nfc-tap": 0xf397,
|
||||||
"mdi:nfc-variant": 0xf398,
|
"mdi:nfc-variant": 0xf398,
|
||||||
|
"mdi:nfc-variant-off": 0xfe37,
|
||||||
"mdi:ninja": 0xf773,
|
"mdi:ninja": 0xf773,
|
||||||
"mdi:nintendo-switch": 0xf7e0,
|
"mdi:nintendo-switch": 0xf7e0,
|
||||||
"mdi:nodejs": 0xf399,
|
"mdi:nodejs": 0xf399,
|
||||||
@ -2452,6 +2526,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:note-plus-outline": 0xf39d,
|
"mdi:note-plus-outline": 0xf39d,
|
||||||
"mdi:note-text": 0xf39e,
|
"mdi:note-text": 0xf39e,
|
||||||
"mdi:notebook": 0xf82d,
|
"mdi:notebook": 0xf82d,
|
||||||
|
"mdi:notebook-multiple": 0xfe38,
|
||||||
"mdi:notification-clear-all": 0xf39f,
|
"mdi:notification-clear-all": 0xf39f,
|
||||||
"mdi:npm": 0xf6f6,
|
"mdi:npm": 0xf6f6,
|
||||||
"mdi:npm-variant": 0xf98e,
|
"mdi:npm-variant": 0xf98e,
|
||||||
@ -2573,7 +2648,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:page-previous-outline": 0xfb8f,
|
"mdi:page-previous-outline": 0xfb8f,
|
||||||
"mdi:palette": 0xf3d8,
|
"mdi:palette": 0xf3d8,
|
||||||
"mdi:palette-advanced": 0xf3d9,
|
"mdi:palette-advanced": 0xf3d9,
|
||||||
"mdi:palette-outline": 0xfde8,
|
"mdi:palette-outline": 0xfe6c,
|
||||||
"mdi:palette-swatch": 0xf8b4,
|
"mdi:palette-swatch": 0xf8b4,
|
||||||
"mdi:pan": 0xfb90,
|
"mdi:pan": 0xfb90,
|
||||||
"mdi:pan-bottom-left": 0xfb91,
|
"mdi:pan-bottom-left": 0xfb91,
|
||||||
@ -2609,6 +2684,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:paw": 0xf3e9,
|
"mdi:paw": 0xf3e9,
|
||||||
"mdi:paw-off": 0xf657,
|
"mdi:paw-off": 0xf657,
|
||||||
"mdi:paypal": 0xf882,
|
"mdi:paypal": 0xf882,
|
||||||
|
"mdi:pdf-box": 0xfe39,
|
||||||
"mdi:peace": 0xf883,
|
"mdi:peace": 0xf883,
|
||||||
"mdi:pen": 0xf3ea,
|
"mdi:pen": 0xf3ea,
|
||||||
"mdi:pen-lock": 0xfdbe,
|
"mdi:pen-lock": 0xfdbe,
|
||||||
@ -2667,6 +2743,10 @@ class MaterialDesignIcons {
|
|||||||
"mdi:pi-hole": 0xfdcd,
|
"mdi:pi-hole": 0xfdcd,
|
||||||
"mdi:piano": 0xf67c,
|
"mdi:piano": 0xf67c,
|
||||||
"mdi:pickaxe": 0xf8b6,
|
"mdi:pickaxe": 0xf8b6,
|
||||||
|
"mdi:picture-in-picture-bottom-right": 0xfe3a,
|
||||||
|
"mdi:picture-in-picture-bottom-right-outline": 0xfe3b,
|
||||||
|
"mdi:picture-in-picture-top-right": 0xfe3c,
|
||||||
|
"mdi:picture-in-picture-top-right-outline": 0xfe3d,
|
||||||
"mdi:pier": 0xf886,
|
"mdi:pier": 0xf886,
|
||||||
"mdi:pier-crane": 0xf887,
|
"mdi:pier-crane": 0xf887,
|
||||||
"mdi:pig": 0xf401,
|
"mdi:pig": 0xf401,
|
||||||
@ -2762,7 +2842,10 @@ class MaterialDesignIcons {
|
|||||||
"mdi:presentation-play": 0xf429,
|
"mdi:presentation-play": 0xf429,
|
||||||
"mdi:printer": 0xf42a,
|
"mdi:printer": 0xf42a,
|
||||||
"mdi:printer-3d": 0xf42b,
|
"mdi:printer-3d": 0xf42b,
|
||||||
|
"mdi:printer-3d-nozzle": 0xfe3e,
|
||||||
|
"mdi:printer-3d-nozzle-outline": 0xfe3f,
|
||||||
"mdi:printer-alert": 0xf42c,
|
"mdi:printer-alert": 0xf42c,
|
||||||
|
"mdi:printer-off": 0xfe40,
|
||||||
"mdi:printer-settings": 0xf706,
|
"mdi:printer-settings": 0xf706,
|
||||||
"mdi:printer-wireless": 0xfa0a,
|
"mdi:printer-wireless": 0xfa0a,
|
||||||
"mdi:priority-high": 0xf603,
|
"mdi:priority-high": 0xf603,
|
||||||
@ -2822,6 +2905,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:record": 0xf44a,
|
"mdi:record": 0xf44a,
|
||||||
"mdi:record-player": 0xf999,
|
"mdi:record-player": 0xf999,
|
||||||
"mdi:record-rec": 0xf44b,
|
"mdi:record-rec": 0xf44b,
|
||||||
|
"mdi:rectangle": 0xfe41,
|
||||||
|
"mdi:rectangle-outline": 0xfe42,
|
||||||
"mdi:recycle": 0xf44c,
|
"mdi:recycle": 0xf44c,
|
||||||
"mdi:reddit": 0xf44d,
|
"mdi:reddit": 0xf44d,
|
||||||
"mdi:redo": 0xf44e,
|
"mdi:redo": 0xf44e,
|
||||||
@ -2866,6 +2951,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:ribbon": 0xf460,
|
"mdi:ribbon": 0xf460,
|
||||||
"mdi:rice": 0xf7e9,
|
"mdi:rice": 0xf7e9,
|
||||||
"mdi:ring": 0xf7ea,
|
"mdi:ring": 0xf7ea,
|
||||||
|
"mdi:rivet": 0xfe43,
|
||||||
"mdi:road": 0xf461,
|
"mdi:road": 0xf461,
|
||||||
"mdi:road-variant": 0xf462,
|
"mdi:road-variant": 0xf462,
|
||||||
"mdi:robot": 0xf6a8,
|
"mdi:robot": 0xf6a8,
|
||||||
@ -2908,6 +2994,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:satellite-uplink": 0xf908,
|
"mdi:satellite-uplink": 0xf908,
|
||||||
"mdi:satellite-variant": 0xf471,
|
"mdi:satellite-variant": 0xf471,
|
||||||
"mdi:sausage": 0xf8b9,
|
"mdi:sausage": 0xf8b9,
|
||||||
|
"mdi:saw-blade": 0xfe44,
|
||||||
"mdi:saxophone": 0xf609,
|
"mdi:saxophone": 0xf609,
|
||||||
"mdi:scale": 0xf472,
|
"mdi:scale": 0xf472,
|
||||||
"mdi:scale-balance": 0xf5d1,
|
"mdi:scale-balance": 0xf5d1,
|
||||||
@ -2919,10 +3006,10 @@ class MaterialDesignIcons {
|
|||||||
"mdi:screen-rotation": 0xf475,
|
"mdi:screen-rotation": 0xf475,
|
||||||
"mdi:screen-rotation-lock": 0xf476,
|
"mdi:screen-rotation-lock": 0xf476,
|
||||||
"mdi:screw-flat-top": 0xfdcf,
|
"mdi:screw-flat-top": 0xfdcf,
|
||||||
"mdi:screw-lag": 0xfdd0,
|
"mdi:screw-lag": 0xfe54,
|
||||||
"mdi:screw-machine-flat-top": 0xfdd1,
|
"mdi:screw-machine-flat-top": 0xfe55,
|
||||||
"mdi:screw-machine-round-top": 0xfdd2,
|
"mdi:screw-machine-round-top": 0xfe56,
|
||||||
"mdi:screw-round-top": 0xfdd3,
|
"mdi:screw-round-top": 0xfe57,
|
||||||
"mdi:screwdriver": 0xf477,
|
"mdi:screwdriver": 0xf477,
|
||||||
"mdi:script": 0xfb9d,
|
"mdi:script": 0xfb9d,
|
||||||
"mdi:script-outline": 0xf478,
|
"mdi:script-outline": 0xf478,
|
||||||
@ -2944,6 +3031,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:seatbelt": 0xfca1,
|
"mdi:seatbelt": 0xfca1,
|
||||||
"mdi:security": 0xf483,
|
"mdi:security": 0xf483,
|
||||||
"mdi:security-network": 0xf484,
|
"mdi:security-network": 0xf484,
|
||||||
|
"mdi:seed": 0xfe45,
|
||||||
|
"mdi:seed-outline": 0xfe46,
|
||||||
"mdi:select": 0xf485,
|
"mdi:select": 0xf485,
|
||||||
"mdi:select-all": 0xf486,
|
"mdi:select-all": 0xf486,
|
||||||
"mdi:select-color": 0xfd0d,
|
"mdi:select-color": 0xfd0d,
|
||||||
@ -2956,8 +3045,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:selection-ellipse": 0xfd0e,
|
"mdi:selection-ellipse": 0xfd0e,
|
||||||
"mdi:selection-off": 0xf776,
|
"mdi:selection-off": 0xf776,
|
||||||
"mdi:send": 0xf48a,
|
"mdi:send": 0xf48a,
|
||||||
"mdi:send-circle": 0xfdd4,
|
"mdi:send-circle": 0xfe58,
|
||||||
"mdi:send-circle-outline": 0xfdd5,
|
"mdi:send-circle-outline": 0xfe59,
|
||||||
"mdi:send-lock": 0xf7ec,
|
"mdi:send-lock": 0xf7ec,
|
||||||
"mdi:serial-port": 0xf65c,
|
"mdi:serial-port": 0xf65c,
|
||||||
"mdi:server": 0xf48b,
|
"mdi:server": 0xf48b,
|
||||||
@ -3021,7 +3110,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:ship-wheel": 0xf832,
|
"mdi:ship-wheel": 0xf832,
|
||||||
"mdi:shoe-formal": 0xfb22,
|
"mdi:shoe-formal": 0xfb22,
|
||||||
"mdi:shoe-heel": 0xfb23,
|
"mdi:shoe-heel": 0xfb23,
|
||||||
"mdi:shoe-print": 0xfdd6,
|
"mdi:shoe-print": 0xfe5a,
|
||||||
"mdi:shopify": 0xfadd,
|
"mdi:shopify": 0xfadd,
|
||||||
"mdi:shopping": 0xf49a,
|
"mdi:shopping": 0xf49a,
|
||||||
"mdi:shopping-music": 0xf49b,
|
"mdi:shopping-music": 0xf49b,
|
||||||
@ -3047,14 +3136,15 @@ class MaterialDesignIcons {
|
|||||||
"mdi:signal-cellular-2": 0xf8bc,
|
"mdi:signal-cellular-2": 0xf8bc,
|
||||||
"mdi:signal-cellular-3": 0xf8bd,
|
"mdi:signal-cellular-3": 0xf8bd,
|
||||||
"mdi:signal-cellular-outline": 0xf8be,
|
"mdi:signal-cellular-outline": 0xf8be,
|
||||||
|
"mdi:signal-distance-variant": 0xfe47,
|
||||||
"mdi:signal-hspa": 0xf714,
|
"mdi:signal-hspa": 0xf714,
|
||||||
"mdi:signal-hspa-plus": 0xf715,
|
"mdi:signal-hspa-plus": 0xf715,
|
||||||
"mdi:signal-off": 0xf782,
|
"mdi:signal-off": 0xf782,
|
||||||
"mdi:signal-variant": 0xf60a,
|
"mdi:signal-variant": 0xf60a,
|
||||||
"mdi:signature": 0xfdd7,
|
"mdi:signature": 0xfe5b,
|
||||||
"mdi:signature-freehand": 0xfdd8,
|
"mdi:signature-freehand": 0xfe5c,
|
||||||
"mdi:signature-image": 0xfdd9,
|
"mdi:signature-image": 0xfe5d,
|
||||||
"mdi:signature-text": 0xfdda,
|
"mdi:signature-text": 0xfe5e,
|
||||||
"mdi:silo": 0xfb24,
|
"mdi:silo": 0xfb24,
|
||||||
"mdi:silverware": 0xf4a3,
|
"mdi:silverware": 0xf4a3,
|
||||||
"mdi:silverware-fork": 0xf4a4,
|
"mdi:silverware-fork": 0xf4a4,
|
||||||
@ -3087,8 +3177,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:slackware": 0xf90a,
|
"mdi:slackware": 0xf90a,
|
||||||
"mdi:sleep": 0xf4b2,
|
"mdi:sleep": 0xf4b2,
|
||||||
"mdi:sleep-off": 0xf4b3,
|
"mdi:sleep-off": 0xf4b3,
|
||||||
"mdi:slope-downhill": 0xfddb,
|
"mdi:slope-downhill": 0xfe5f,
|
||||||
"mdi:slope-uphill": 0xfddc,
|
"mdi:slope-uphill": 0xfe60,
|
||||||
"mdi:smog": 0xfa70,
|
"mdi:smog": 0xfa70,
|
||||||
"mdi:smoke-detector": 0xf392,
|
"mdi:smoke-detector": 0xf392,
|
||||||
"mdi:smoking": 0xf4b4,
|
"mdi:smoking": 0xf4b4,
|
||||||
@ -3129,6 +3219,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:spa": 0xfcad,
|
"mdi:spa": 0xfcad,
|
||||||
"mdi:spa-outline": 0xfcae,
|
"mdi:spa-outline": 0xfcae,
|
||||||
"mdi:space-invaders": 0xfba5,
|
"mdi:space-invaders": 0xfba5,
|
||||||
|
"mdi:spade": 0xfe48,
|
||||||
"mdi:speaker": 0xf4c3,
|
"mdi:speaker": 0xf4c3,
|
||||||
"mdi:speaker-bluetooth": 0xf9a1,
|
"mdi:speaker-bluetooth": 0xf9a1,
|
||||||
"mdi:speaker-multiple": 0xfd14,
|
"mdi:speaker-multiple": 0xfd14,
|
||||||
@ -3142,6 +3233,8 @@ class MaterialDesignIcons {
|
|||||||
"mdi:spotlight-beam": 0xf4c9,
|
"mdi:spotlight-beam": 0xf4c9,
|
||||||
"mdi:spray": 0xf665,
|
"mdi:spray": 0xf665,
|
||||||
"mdi:spray-bottle": 0xfadf,
|
"mdi:spray-bottle": 0xfadf,
|
||||||
|
"mdi:sprout": 0xfe49,
|
||||||
|
"mdi:sprout-outline": 0xfe4a,
|
||||||
"mdi:square": 0xf763,
|
"mdi:square": 0xf763,
|
||||||
"mdi:square-edit-outline": 0xf90b,
|
"mdi:square-edit-outline": 0xf90b,
|
||||||
"mdi:square-inc": 0xf4ca,
|
"mdi:square-inc": 0xf4ca,
|
||||||
@ -3246,6 +3339,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:table-row-remove": 0xf4f5,
|
"mdi:table-row-remove": 0xf4f5,
|
||||||
"mdi:table-search": 0xf90e,
|
"mdi:table-search": 0xf90e,
|
||||||
"mdi:table-settings": 0xf837,
|
"mdi:table-settings": 0xf837,
|
||||||
|
"mdi:table-tennis": 0xfe4b,
|
||||||
"mdi:tablet": 0xf4f6,
|
"mdi:tablet": 0xf4f6,
|
||||||
"mdi:tablet-android": 0xf4f7,
|
"mdi:tablet-android": 0xf4f7,
|
||||||
"mdi:tablet-cellphone": 0xf9a6,
|
"mdi:tablet-cellphone": 0xf9a6,
|
||||||
@ -3301,12 +3395,12 @@ class MaterialDesignIcons {
|
|||||||
"mdi:theater": 0xf50d,
|
"mdi:theater": 0xf50d,
|
||||||
"mdi:theme-light-dark": 0xf50e,
|
"mdi:theme-light-dark": 0xf50e,
|
||||||
"mdi:thermometer": 0xf50f,
|
"mdi:thermometer": 0xf50f,
|
||||||
"mdi:thermometer-alert": 0xfddd,
|
"mdi:thermometer-alert": 0xfe61,
|
||||||
"mdi:thermometer-chevron-down": 0xfdde,
|
"mdi:thermometer-chevron-down": 0xfe62,
|
||||||
"mdi:thermometer-chevron-up": 0xfddf,
|
"mdi:thermometer-chevron-up": 0xfe63,
|
||||||
"mdi:thermometer-lines": 0xf510,
|
"mdi:thermometer-lines": 0xf510,
|
||||||
"mdi:thermometer-minus": 0xfde0,
|
"mdi:thermometer-minus": 0xfe64,
|
||||||
"mdi:thermometer-plus": 0xfde1,
|
"mdi:thermometer-plus": 0xfe65,
|
||||||
"mdi:thermostat": 0xf393,
|
"mdi:thermostat": 0xf393,
|
||||||
"mdi:thermostat-box": 0xf890,
|
"mdi:thermostat-box": 0xf890,
|
||||||
"mdi:thought-bubble": 0xf7f5,
|
"mdi:thought-bubble": 0xf7f5,
|
||||||
@ -3384,12 +3478,13 @@ class MaterialDesignIcons {
|
|||||||
"mdi:transition": 0xf914,
|
"mdi:transition": 0xf914,
|
||||||
"mdi:transition-masked": 0xf915,
|
"mdi:transition-masked": 0xf915,
|
||||||
"mdi:translate": 0xf5ca,
|
"mdi:translate": 0xf5ca,
|
||||||
"mdi:translate-off": 0xfde2,
|
"mdi:translate-off": 0xfe66,
|
||||||
"mdi:transmission-tower": 0xfd1a,
|
"mdi:transmission-tower": 0xfd1a,
|
||||||
"mdi:trash-can": 0xfa78,
|
"mdi:trash-can": 0xfa78,
|
||||||
"mdi:trash-can-outline": 0xfa79,
|
"mdi:trash-can-outline": 0xfa79,
|
||||||
"mdi:treasure-chest": 0xf725,
|
"mdi:treasure-chest": 0xf725,
|
||||||
"mdi:tree": 0xf531,
|
"mdi:tree": 0xf531,
|
||||||
|
"mdi:tree-outline": 0xfe4c,
|
||||||
"mdi:trello": 0xf532,
|
"mdi:trello": 0xf532,
|
||||||
"mdi:trending-down": 0xf533,
|
"mdi:trending-down": 0xf533,
|
||||||
"mdi:trending-neutral": 0xf534,
|
"mdi:trending-neutral": 0xf534,
|
||||||
@ -3450,7 +3545,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:upload-multiple": 0xf83c,
|
"mdi:upload-multiple": 0xf83c,
|
||||||
"mdi:upload-network": 0xf6f5,
|
"mdi:upload-network": 0xf6f5,
|
||||||
"mdi:upload-network-outline": 0xfcb4,
|
"mdi:upload-network-outline": 0xfcb4,
|
||||||
"mdi:upload-outline": 0xfde3,
|
"mdi:upload-outline": 0xfe67,
|
||||||
"mdi:usb": 0xf553,
|
"mdi:usb": 0xf553,
|
||||||
"mdi:van-passenger": 0xf7f9,
|
"mdi:van-passenger": 0xf7f9,
|
||||||
"mdi:van-utility": 0xf7fa,
|
"mdi:van-utility": 0xf7fa,
|
||||||
@ -3503,6 +3598,9 @@ class MaterialDesignIcons {
|
|||||||
"mdi:view-array": 0xf56b,
|
"mdi:view-array": 0xf56b,
|
||||||
"mdi:view-carousel": 0xf56c,
|
"mdi:view-carousel": 0xf56c,
|
||||||
"mdi:view-column": 0xf56d,
|
"mdi:view-column": 0xf56d,
|
||||||
|
"mdi:view-comfy": 0xfe4d,
|
||||||
|
"mdi:view-compact": 0xfe4e,
|
||||||
|
"mdi:view-compact-outline": 0xfe4f,
|
||||||
"mdi:view-dashboard": 0xf56e,
|
"mdi:view-dashboard": 0xf56e,
|
||||||
"mdi:view-dashboard-outline": 0xfa1c,
|
"mdi:view-dashboard-outline": 0xfa1c,
|
||||||
"mdi:view-dashboard-variant": 0xf842,
|
"mdi:view-dashboard-variant": 0xf842,
|
||||||
@ -3537,11 +3635,12 @@ class MaterialDesignIcons {
|
|||||||
"mdi:volume-mute": 0xf75e,
|
"mdi:volume-mute": 0xf75e,
|
||||||
"mdi:volume-off": 0xf581,
|
"mdi:volume-off": 0xf581,
|
||||||
"mdi:volume-plus": 0xf75c,
|
"mdi:volume-plus": 0xf75c,
|
||||||
"mdi:volume-variant-off": 0xfde4,
|
"mdi:volume-variant-off": 0xfe68,
|
||||||
"mdi:vote": 0xfa1e,
|
"mdi:vote": 0xfa1e,
|
||||||
"mdi:vote-outline": 0xfa1f,
|
"mdi:vote-outline": 0xfa1f,
|
||||||
"mdi:vpn": 0xf582,
|
"mdi:vpn": 0xf582,
|
||||||
"mdi:vuejs": 0xf843,
|
"mdi:vuejs": 0xf843,
|
||||||
|
"mdi:vuetify": 0xfe50,
|
||||||
"mdi:walk": 0xf583,
|
"mdi:walk": 0xf583,
|
||||||
"mdi:wall": 0xf7fd,
|
"mdi:wall": 0xf7fd,
|
||||||
"mdi:wall-sconce": 0xf91b,
|
"mdi:wall-sconce": 0xf91b,
|
||||||
@ -3552,7 +3651,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:wallet-membership": 0xf586,
|
"mdi:wallet-membership": 0xf586,
|
||||||
"mdi:wallet-outline": 0xfbb9,
|
"mdi:wallet-outline": 0xfbb9,
|
||||||
"mdi:wallet-travel": 0xf587,
|
"mdi:wallet-travel": 0xf587,
|
||||||
"mdi:wallpaper": 0xfde5,
|
"mdi:wallpaper": 0xfe69,
|
||||||
"mdi:wan": 0xf588,
|
"mdi:wan": 0xf588,
|
||||||
"mdi:washing-machine": 0xf729,
|
"mdi:washing-machine": 0xf729,
|
||||||
"mdi:watch": 0xf589,
|
"mdi:watch": 0xf589,
|
||||||
@ -3565,13 +3664,14 @@ class MaterialDesignIcons {
|
|||||||
"mdi:watch-vibrate-off": 0xfcb6,
|
"mdi:watch-vibrate-off": 0xfcb6,
|
||||||
"mdi:water": 0xf58c,
|
"mdi:water": 0xf58c,
|
||||||
"mdi:water-off": 0xf58d,
|
"mdi:water-off": 0xf58d,
|
||||||
"mdi:water-outline": 0xfde6,
|
"mdi:water-outline": 0xfe6a,
|
||||||
"mdi:water-percent": 0xf58e,
|
"mdi:water-percent": 0xf58e,
|
||||||
"mdi:water-pump": 0xf58f,
|
"mdi:water-pump": 0xf58f,
|
||||||
"mdi:watermark": 0xf612,
|
"mdi:watermark": 0xf612,
|
||||||
"mdi:waves": 0xf78c,
|
"mdi:waves": 0xf78c,
|
||||||
"mdi:waze": 0xfbba,
|
"mdi:waze": 0xfbba,
|
||||||
"mdi:weather-cloudy": 0xf590,
|
"mdi:weather-cloudy": 0xf590,
|
||||||
|
"mdi:weather-cloudy-arrow-right": 0xfe51,
|
||||||
"mdi:weather-fog": 0xf591,
|
"mdi:weather-fog": 0xf591,
|
||||||
"mdi:weather-hail": 0xf592,
|
"mdi:weather-hail": 0xf592,
|
||||||
"mdi:weather-hurricane": 0xf897,
|
"mdi:weather-hurricane": 0xf897,
|
||||||
@ -3608,7 +3708,7 @@ class MaterialDesignIcons {
|
|||||||
"mdi:widgets": 0xf72b,
|
"mdi:widgets": 0xf72b,
|
||||||
"mdi:wifi": 0xf5a9,
|
"mdi:wifi": 0xf5a9,
|
||||||
"mdi:wifi-off": 0xf5aa,
|
"mdi:wifi-off": 0xf5aa,
|
||||||
"mdi:wifi-star": 0xfde7,
|
"mdi:wifi-star": 0xfe6b,
|
||||||
"mdi:wifi-strength-1": 0xf91e,
|
"mdi:wifi-strength-1": 0xf91e,
|
||||||
"mdi:wifi-strength-1-alert": 0xf91f,
|
"mdi:wifi-strength-1-alert": 0xf91f,
|
||||||
"mdi:wifi-strength-1-lock": 0xf920,
|
"mdi:wifi-strength-1-lock": 0xf920,
|
||||||
@ -3659,7 +3759,9 @@ class MaterialDesignIcons {
|
|||||||
"mdi:xbox-controller-battery-low": 0xf74d,
|
"mdi:xbox-controller-battery-low": 0xf74d,
|
||||||
"mdi:xbox-controller-battery-medium": 0xf74e,
|
"mdi:xbox-controller-battery-medium": 0xf74e,
|
||||||
"mdi:xbox-controller-battery-unknown": 0xf74f,
|
"mdi:xbox-controller-battery-unknown": 0xf74f,
|
||||||
|
"mdi:xbox-controller-menu": 0xfe52,
|
||||||
"mdi:xbox-controller-off": 0xf5bb,
|
"mdi:xbox-controller-off": 0xf5bb,
|
||||||
|
"mdi:xbox-controller-view": 0xfe53,
|
||||||
"mdi:xda": 0xf5bc,
|
"mdi:xda": 0xf5bc,
|
||||||
"mdi:xing": 0xf5bd,
|
"mdi:xing": 0xf5bd,
|
||||||
"mdi:xing-box": 0xf5be,
|
"mdi:xing-box": 0xf5be,
|
||||||
|
@ -56,6 +56,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||||
}
|
}
|
||||||
|
_newHassioDomain = _newHassioDomain.split("/")[0];
|
||||||
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);
|
||||||
|
@ -28,7 +28,7 @@ class Panel {
|
|||||||
void handleOpen(BuildContext context) {
|
void handleOpen(BuildContext context) {
|
||||||
if (type == "iframe") {
|
if (type == "iframe") {
|
||||||
Logger.d("Launching custom tab with ${config["url"]}");
|
Logger.d("Launching custom tab with ${config["url"]}");
|
||||||
HAUtils.launchURLInCustomTab(context, config["url"]);
|
HAUtils.launchURLInCustomTab(context: context, url: config["url"]);
|
||||||
} else if (type == "config") {
|
} else if (type == "config") {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@ -36,10 +36,9 @@ class Panel {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
HomeAssistantModel haModel = HomeAssistantModel.of(context);
|
String url = "${Connection().httpWebHost}/$urlPath";
|
||||||
String url = "${haModel.homeAssistant.connection.httpWebHost}/$urlPath";
|
|
||||||
Logger.d("Launching custom tab with $url");
|
Logger.d("Launching custom tab with $url");
|
||||||
HAUtils.launchURLInCustomTab(context, url);
|
HAUtils.launchURLInCustomTab(context: context, url: url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class Sizes {
|
class Sizes {
|
||||||
static const rightWidgetPadding = 16.0;
|
static const rightWidgetPadding = 10.0;
|
||||||
static const leftWidgetPadding = 16.0;
|
static const leftWidgetPadding = 10.0;
|
||||||
static const buttonPadding = 4.0;
|
static const buttonPadding = 4.0;
|
||||||
static const extendedWidgetHeight = 50.0;
|
static const extendedWidgetHeight = 50.0;
|
||||||
static const iconSize = 28.0;
|
static const iconSize = 28.0;
|
||||||
|
@ -112,7 +112,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
if (!entity.entity.isHidden) {
|
if (!entity.entity.isHidden) {
|
||||||
body.add(
|
body.add(
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(10.0, 4.0, 0.0, 4.0),
|
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||||
child: EntityModel(
|
child: EntityModel(
|
||||||
entityWrapper: entity,
|
entityWrapper: entity,
|
||||||
handleTap: true,
|
handleTap: true,
|
||||||
@ -122,7 +122,10 @@ class CardWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return Card(
|
return Card(
|
||||||
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,60 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_items = <ConfigurationItem>[
|
_items = <ConfigurationItem>[
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Home Assistant Cloud',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/cloud/account");
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Integrations',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/integrations/dashboard");
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Users',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/users/picker");
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
ConfigurationItem(
|
ConfigurationItem(
|
||||||
header: 'General',
|
header: 'General',
|
||||||
body: Padding(
|
body: Padding(
|
||||||
@ -31,6 +85,13 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/core");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Text("Control your Home Assistant server from HA Client."),
|
Text("Control your Home Assistant server from HA Client."),
|
||||||
@ -38,27 +99,202 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FlatServiceButton(
|
FlatButton(
|
||||||
text: "Restart",
|
child: Text('Restart', style: TextStyle(color: Colors.blue)),
|
||||||
serviceName: "restart",
|
onPressed: () => restart(),
|
||||||
serviceDomain: "homeassistant",
|
|
||||||
entityId: null,
|
|
||||||
),
|
),
|
||||||
FlatServiceButton(
|
FlatButton(
|
||||||
text: "Stop",
|
child: Text("Stop", style: TextStyle(color: Colors.blue)),
|
||||||
serviceName: "stop",
|
onPressed: () => stop(),
|
||||||
serviceDomain: "homeassistant",
|
|
||||||
entityId: null,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Persons',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/person");
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Entity Registry',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/entity_registry");
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Area Registry',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/area_registry");
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Automation',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/automation");
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Script',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/script");
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Customization',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
||||||
|
onPressed: () {
|
||||||
|
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/customize");
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ConfigurationItem(
|
||||||
|
header: 'Mobile app',
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Registration", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Text("${HomeAssistant().userName}'s ${Device().model}, ${Device().osName} ${Device().osVersion}"),
|
||||||
|
Container(height: 6.0,),
|
||||||
|
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
|
||||||
|
Divider(),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () => updateRegistration(),
|
||||||
|
child: Text("Check registration", style: TextStyle(color: Colors.blue))
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () => resetRegistration(),
|
||||||
|
child: Text("Reset registration", style: TextStyle(color: Colors.red))
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
eventBus.fire(ShowDialogEvent(
|
||||||
|
title: "Are you sure you want to restart Home Assistant?",
|
||||||
|
body: "This will restart your Home Assistant server.",
|
||||||
|
positiveText: "Sure. Make it so",
|
||||||
|
negativeText: "What?? No!",
|
||||||
|
onPositive: () {
|
||||||
|
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
eventBus.fire(ShowDialogEvent(
|
||||||
|
title: "Are you sure you wanr to STOP Home Assistant?",
|
||||||
|
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
||||||
|
positiveText: "Sure. Make it so",
|
||||||
|
negativeText: "What?? No!",
|
||||||
|
onPositive: () {
|
||||||
|
Connection().callService(domain: "homeassistant", service: "stop", entityId: null);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRegistration() {
|
||||||
|
HomeAssistant().checkAppRegistration(showOkDialog: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetRegistration() {
|
||||||
|
eventBus.fire(ShowDialogEvent(
|
||||||
|
title: "Waaaait",
|
||||||
|
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
|
||||||
|
positiveText: "Done it already",
|
||||||
|
negativeText: "Ok, I will",
|
||||||
|
onPositive: () {
|
||||||
|
HomeAssistant().checkAppRegistration(showOkDialog: true, forceRegister: true);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
@ -45,6 +45,50 @@ class Logger {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HAError {
|
||||||
|
String message;
|
||||||
|
final List<HAErrorAction> actions;
|
||||||
|
|
||||||
|
HAError(this.message, {this.actions: const [HAErrorAction.tryAgain()]});
|
||||||
|
|
||||||
|
HAError.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) {
|
||||||
|
this.message = "Unable to connect to Home Assistant";
|
||||||
|
}
|
||||||
|
|
||||||
|
HAError.disconnected({this.actions = const [HAErrorAction.reconnect()]}) {
|
||||||
|
this.message = "Disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
|
HAError.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) {
|
||||||
|
this.message = "Check connection settings";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HAErrorAction {
|
||||||
|
final String title;
|
||||||
|
final int type;
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
const HAErrorAction({@required this.title, this.type: HAErrorActionType.FULL_RELOAD, this.url});
|
||||||
|
|
||||||
|
const HAErrorAction.tryAgain({this.title = "Try again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
||||||
|
|
||||||
|
const HAErrorAction.reconnect({this.title = "Reconnect", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
||||||
|
|
||||||
|
const HAErrorAction.reload({this.title = "Reload", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
||||||
|
|
||||||
|
const HAErrorAction.loginAgain({this.title = "Login again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class HAErrorActionType {
|
||||||
|
static const FULL_RELOAD = 0;
|
||||||
|
static const QUICK_RELOAD = 1;
|
||||||
|
static const LOGOUT = 2;
|
||||||
|
static const URL = 3;
|
||||||
|
static const OPEN_CONNECTION_SETTINGS = 4;
|
||||||
|
}
|
||||||
|
|
||||||
class HAUtils {
|
class HAUtils {
|
||||||
static void launchURL(String url) async {
|
static void launchURL(String url) async {
|
||||||
if (await urlLauncher.canLaunch(url)) {
|
if (await urlLauncher.canLaunch(url)) {
|
||||||
@ -54,15 +98,15 @@ class HAUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void launchURLInCustomTab(BuildContext context, String url) async {
|
static void launchURLInCustomTab({BuildContext context, String url, bool enableDefaultShare: true, bool showPageTitle: true}) async {
|
||||||
try {
|
try {
|
||||||
await launch(
|
await launch(
|
||||||
"$url",
|
"$url",
|
||||||
option: new CustomTabsOption(
|
option: new CustomTabsOption(
|
||||||
toolbarColor: Theme.of(context).primaryColor,
|
toolbarColor: Theme.of(context).primaryColor,
|
||||||
enableDefaultShare: true,
|
enableDefaultShare: enableDefaultShare,
|
||||||
enableUrlBarHiding: true,
|
enableUrlBarHiding: true,
|
||||||
showPageTitle: true,
|
showPageTitle: showPageTitle,
|
||||||
animation: new CustomTabsAnimation.slideIn()
|
animation: new CustomTabsAnimation.slideIn()
|
||||||
// or user defined animation.
|
// or user defined animation.
|
||||||
/*animation: new CustomTabsAnimation(
|
/*animation: new CustomTabsAnimation(
|
||||||
@ -115,8 +159,9 @@ class ReloadUIEvent {
|
|||||||
|
|
||||||
class StartAuthEvent {
|
class StartAuthEvent {
|
||||||
String oauthUrl;
|
String oauthUrl;
|
||||||
|
bool showButton;
|
||||||
|
|
||||||
StartAuthEvent(this.oauthUrl);
|
StartAuthEvent(this.oauthUrl, this.showButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServiceCallEvent {
|
class ServiceCallEvent {
|
||||||
@ -128,6 +173,17 @@ class ServiceCallEvent {
|
|||||||
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
|
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ShowDialogEvent {
|
||||||
|
final String title;
|
||||||
|
final String body;
|
||||||
|
final String positiveText;
|
||||||
|
final String negativeText;
|
||||||
|
final onPositive;
|
||||||
|
final onNegative;
|
||||||
|
|
||||||
|
ShowDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
|
||||||
|
}
|
||||||
|
|
||||||
class ShowEntityPageEvent {
|
class ShowEntityPageEvent {
|
||||||
Entity entity;
|
Entity entity;
|
||||||
|
|
||||||
@ -135,8 +191,7 @@ class ShowEntityPageEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ShowErrorEvent {
|
class ShowErrorEvent {
|
||||||
String text;
|
final HAError error;
|
||||||
int errorCode;
|
|
||||||
|
|
||||||
ShowErrorEvent(this.text, this.errorCode);
|
ShowErrorEvent(this.error);
|
||||||
}
|
}
|
114
pubspec.lock
114
pubspec.lock
@ -1,5 +1,5 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://www.dartlang.org/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@ -7,21 +7,21 @@ packages:
|
|||||||
name: archive
|
name: archive
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.0.10"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.1"
|
version: "1.5.2"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.2.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -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.7.0"
|
version: "1.1.1"
|
||||||
charcode:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -77,16 +77,7 @@ packages:
|
|||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.1.1+1"
|
||||||
dart_config:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "."
|
|
||||||
ref: HEAD
|
|
||||||
resolved-ref: a7ed88a4793e094a4d5d5c2d88a89e55510accde
|
|
||||||
url: "https://github.com/MarkOSullivan94/dart_config.git"
|
|
||||||
source: git
|
|
||||||
version: "0.5.0"
|
|
||||||
date_format:
|
date_format:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -94,20 +85,27 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.6"
|
version: "1.0.6"
|
||||||
|
device_info:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: device_info
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.0+2"
|
||||||
event_bus:
|
event_bus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: event_bus
|
name: event_bus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.1.0"
|
||||||
firebase_messaging:
|
firebase_messaging:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging
|
name: firebase_messaging
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0+1"
|
version: "4.0.0+4"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -119,7 +117,7 @@ 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.2"
|
version: "1.1.1"
|
||||||
flutter_custom_tabs:
|
flutter_custom_tabs:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -133,7 +131,14 @@ packages:
|
|||||||
name: flutter_launcher_icons
|
name: flutter_launcher_icons
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
version: "0.7.2+1"
|
||||||
|
flutter_local_notifications:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.1+3"
|
||||||
flutter_markdown:
|
flutter_markdown:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -147,14 +152,7 @@ packages:
|
|||||||
name: flutter_secure_storage
|
name: flutter_secure_storage
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.1+1"
|
||||||
flutter_svg:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_svg
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.10.4"
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -166,14 +164,14 @@ packages:
|
|||||||
name: flutter_webview_plugin
|
name: flutter_webview_plugin
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "0.3.7"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.0+1"
|
version: "0.12.0+2"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -187,14 +185,14 @@ packages:
|
|||||||
name: image
|
name: image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.1.4"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.7"
|
version: "0.15.8"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -208,14 +206,14 @@ packages:
|
|||||||
name: markdown
|
name: markdown
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.3"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.3+1"
|
version: "0.12.5"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -230,69 +228,55 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.2"
|
version: "1.6.2"
|
||||||
path_drawing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: path_drawing
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.0"
|
|
||||||
path_parsing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: path_parsing
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.3"
|
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0+1"
|
version: "1.2.0"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pedantic
|
name: pedantic
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.7.0"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.4.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.1"
|
||||||
progress_indicators:
|
progress_indicators:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: progress_indicators
|
name: progress_indicators
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.4"
|
||||||
quiver:
|
quiver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: quiver
|
name: quiver
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.3"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1+2"
|
version: "0.5.3+4"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -304,14 +288,14 @@ packages:
|
|||||||
name: source_span
|
name: source_span
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.4"
|
version: "1.5.5"
|
||||||
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.3"
|
version: "1.1.6+3"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -325,7 +309,7 @@ packages:
|
|||||||
name: stream_channel
|
name: stream_channel
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.8"
|
version: "2.0.0"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -339,7 +323,7 @@ packages:
|
|||||||
name: synchronized
|
name: synchronized
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0+1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -353,7 +337,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.5"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -367,14 +351,14 @@ packages:
|
|||||||
name: url_launcher
|
name: url_launcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.1.2"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.2"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -388,7 +372,7 @@ packages:
|
|||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.9"
|
version: "1.0.15"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -402,7 +386,7 @@ packages:
|
|||||||
name: yaml
|
name: yaml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.15"
|
version: "2.1.16"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.1.0 <3.0.0"
|
dart: ">=2.4.0 <3.0.0"
|
||||||
flutter: ">=1.2.1 <2.0.0"
|
flutter: ">=1.5.0 <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.6.0+101
|
version: 0.6.1+610
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||||
@ -18,11 +18,13 @@ dependencies:
|
|||||||
date_format: any
|
date_format: any
|
||||||
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
|
flutter_custom_tabs: ^0.6.0
|
||||||
firebase_messaging: ^4.0.0+1
|
firebase_messaging: ^4.0.0+1
|
||||||
flutter_webview_plugin: ^0.3.1
|
flutter_webview_plugin: ^0.3.1
|
||||||
flutter_secure_storage: ^3.2.0
|
flutter_secure_storage: ^3.2.0
|
||||||
|
device_info: ^0.4.0+1
|
||||||
|
flutter_local_notifications: ^0.7.1+3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -63,7 +65,7 @@ flutter:
|
|||||||
fonts:
|
fonts:
|
||||||
- family: "Material Design Icons"
|
- family: "Material Design Icons"
|
||||||
fonts:
|
fonts:
|
||||||
- asset: fonts/materialdesignicons-webfont-3-5-95.ttf
|
- asset: fonts/materialdesignicons-webfont-3-6-95.ttf
|
||||||
# fonts:
|
# fonts:
|
||||||
# - family: Schyler
|
# - family: Schyler
|
||||||
# fonts:
|
# fonts:
|
||||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user