Compare commits
178 Commits
0.3.11
...
0.6.0-alph
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
4e96b9adbb | |||
b9581d3762 | |||
7c010359c3 | |||
4a75243994 | |||
d29d7e5b3b | |||
5ebd25e0d1 | |||
b7d5a53e86 | |||
20d3498bfd | |||
67d7bb45f5 | |||
6a03105d01 | |||
5ae580ecf1 | |||
0efef33e53 | |||
ccb88884a7 | |||
d70ba0a55a | |||
5140840d3a | |||
14759fd3c9 | |||
fed35be517 | |||
db77cc43aa | |||
b2269cc96d | |||
8b28bb2e9e | |||
fb456878bc | |||
8b961ebd69 | |||
9bd3a41cf5 | |||
491ae55a2a | |||
e1d2981782 | |||
74572168ae | |||
92d0b5c055 | |||
3504d3276c | |||
736b38b64c | |||
cb118b599a | |||
a08a056cff | |||
0ef2ebfe31 | |||
4f4ac3b574 | |||
7064cb0e30 | |||
91a99e17e0 | |||
2e9b7d20b9 | |||
b8aa808de4 | |||
2cfa92a42b | |||
146efef72d | |||
8c9804e16f | |||
a4736bfb5a | |||
15c54df629 | |||
32ffef21e9 | |||
848d3cb510 | |||
8a4caeebba | |||
aa923f0fba | |||
4d8f50ddd5 | |||
fe06b21a6c | |||
efed7fb1b5 | |||
df2cbb7d13 | |||
03edaa9ca2 | |||
1a7457abf9 | |||
00889b13e0 | |||
0615073ec4 | |||
eb7d17d147 | |||
24f80feeee | |||
4b6dda5a9c | |||
4099fa0c83 | |||
76057e8797 | |||
538d3603dc | |||
bc0e72ca52 | |||
f25a47beb2 | |||
cc3c6b0087 | |||
6cf80c0bfd | |||
8ce9bdb7a5 | |||
31e50150b1 | |||
e359150d97 | |||
93680c981c | |||
e06b66c523 | |||
3dea844e1e | |||
62b1af30e0 | |||
e006c4e403 | |||
983573388e | |||
bdd1dc7e17 | |||
9c1970ee14 | |||
d0e0bf3571 | |||
b399357517 | |||
0290cd3a32 | |||
d8a1d03179 | |||
216fad3cb9 | |||
fead6ea348 | |||
8814687be6 | |||
71c0e2caa0 | |||
1531c41542 | |||
bc90d013e8 | |||
2adfaca0c4 | |||
6cc1a37d9d | |||
4bb616b327 | |||
38219618ba | |||
6774b53758 | |||
29a94c882f | |||
5897fa3a99 | |||
7af92c2dc9 | |||
1094177a42 | |||
5e814e8109 | |||
24c7675fa4 | |||
dc3ca38c78 | |||
96b528e055 | |||
3858036631 | |||
19d42ceeb3 | |||
a2836a3603 | |||
2a45758a6d | |||
dc1bf4d878 | |||
e82ba60c4e | |||
09199d30e8 | |||
724d32dbe2 | |||
949c8ee44e | |||
1a446d34c7 | |||
22a5847285 | |||
1c8f770f10 | |||
be5ea55f6b | |||
c65ade9827 | |||
d3c1422b9e | |||
b6ac9f985f | |||
a59de4b6dc | |||
f507d5df0c | |||
f77e46de37 | |||
cda17b1217 | |||
be560769ef | |||
3815800e32 | |||
a3226311a2 | |||
79669243c2 | |||
fdc81f6ea4 | |||
7fe44459e7 |
3
.gitignore
vendored
@ -10,4 +10,5 @@ build/
|
||||
|
||||
.idea/
|
||||
|
||||
key.properties
|
||||
key.properties
|
||||
pubspec.lock
|
76
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at vyalov.egor@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
@ -1,13 +1,12 @@
|
||||
[](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart)
|
||||
# HA Client
|
||||
## Native Android client for Home Assistant
|
||||
### With Lovelace UI support
|
||||
|
||||
Home Assistant Android client on Dart with Flutter.
|
||||
|
||||
Visit [www.keyboardcrumbs.io](http://www.keyboardcrumbs.io/ha-client) for more info.
|
||||
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
||||
|
||||
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
|
||||
|
||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
|
||||
|
||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or in [Discord](https://discord.gg/NSaQEQ8)
|
||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)
|
||||
|
@ -29,7 +29,12 @@ def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
compileSdkVersion 28
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
@ -38,7 +43,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "com.keyboardcrumbs.haclient"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
targetSdkVersion 28
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
@ -65,7 +70,10 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-core:16.0.8'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
42
android/app/google-services.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "441874387819",
|
||||
"firebase_url": "https://ha-client-c73c4.firebaseio.com",
|
||||
"project_id": "ha-client-c73c4",
|
||||
"storage_bucket": "ha-client-c73c4.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45",
|
||||
"android_client_info": {
|
||||
"package_name": "com.keyboardcrumbs.haclient"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
@ -15,7 +16,13 @@
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="ha_notify" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
@ -26,14 +33,18 @@
|
||||
<!-- This keeps the window background of the activity showing
|
||||
until Flutter renders its first frame. It can be removed if
|
||||
there is no splash screen (such as the default splash screen
|
||||
defined in @style/LaunchTheme). -->
|
||||
defined in @style/LaunchTheme).
|
||||
<meta-data
|
||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||
android:value="true" />
|
||||
android:value="true" />-->
|
||||
<intent-filter>
|
||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
BIN
android/app/src/main/res/drawable/mini_icon.png
Normal file
After Width: | Height: | Size: 612 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 11 KiB |
@ -5,7 +5,8 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.1.2'
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx2g
|
||||
org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||
|
0
android/gradlew
vendored
Normal file → Executable file
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 24 KiB |
46
lib/auth_manager.class.dart
Normal file
@ -0,0 +1,46 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class AuthManager {
|
||||
|
||||
static final AuthManager _instance = AuthManager._internal();
|
||||
|
||||
factory AuthManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
AuthManager._internal();
|
||||
|
||||
Future getTempToken({String oauthUrl}) {
|
||||
Completer completer = Completer();
|
||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
||||
Logger.d("Webview url changed to $url");
|
||||
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
|
||||
String authCode = url.split("=")[1];
|
||||
Logger.d("We have auth code. Getting temporary access token...");
|
||||
Connection().sendHTTPPost(
|
||||
endPoint: "/auth/token",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
includeAuthHeader: false,
|
||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}"
|
||||
).then((response) {
|
||||
Logger.d("Gottemp token");
|
||||
String tempToken = json.decode(response)['access_token'];
|
||||
Logger.d("Closing webview...");
|
||||
//flutterWebviewPlugin.close();
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.complete(tempToken);
|
||||
}).catchError((e) {
|
||||
//flutterWebviewPlugin.close();
|
||||
Logger.e("Error getting temp token: ${e.toString()}");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.completeError(HAError("Error getting temp token"));
|
||||
});
|
||||
}
|
||||
});
|
||||
Logger.d("Launching OAuth: $oauthUrl");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
}
|
415
lib/connection.class.dart
Normal file
@ -0,0 +1,415 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class Connection {
|
||||
|
||||
static final Connection _instance = Connection._internal();
|
||||
|
||||
factory Connection() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
Connection._internal();
|
||||
|
||||
String _domain;
|
||||
String _port;
|
||||
String displayHostname;
|
||||
String _webSocketAPIEndpoint;
|
||||
String httpWebHost;
|
||||
String _token;
|
||||
String _tempToken;
|
||||
String oauthUrl;
|
||||
String webhookId;
|
||||
bool useLovelace = true;
|
||||
bool settingsLoaded = false;
|
||||
bool get isAuthenticated => _token != null;
|
||||
StreamSubscription _socketSubscription;
|
||||
Duration connectTimeout = Duration(seconds: 15);
|
||||
|
||||
bool isConnected = false;
|
||||
|
||||
var onStateChangeCallback;
|
||||
|
||||
IOWebSocketChannel _socket;
|
||||
|
||||
int _currentMessageId = 0;
|
||||
Map<String, Completer> _messageResolver = {};
|
||||
|
||||
Future init({bool loadSettings, bool forceReconnect: false}) async {
|
||||
Completer completer = Completer();
|
||||
bool stopInit = false;
|
||||
if (loadSettings) {
|
||||
Logger.e("Loading settings...");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
||||
_domain = prefs.getString('hassio-domain');
|
||||
_port = prefs.getString('hassio-port');
|
||||
webhookId = prefs.getString('app-webhook-id');
|
||||
displayHostname = "$_domain:$_port";
|
||||
_webSocketAPIEndpoint =
|
||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||
httpWebHost =
|
||||
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||
if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
completer.completeError(HAError.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
} else {
|
||||
//_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 {
|
||||
if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
completer.completeError(HAError.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stopInit) {
|
||||
if (_token == null) {
|
||||
AuthManager().getTempToken(
|
||||
oauthUrl: oauthUrl
|
||||
).then((token) {
|
||||
Logger.d("Token from AuthManager recived");
|
||||
_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: () {
|
||||
_disconnect().then((_) {
|
||||
completer?.completeError(HAError("Connection timeout"));
|
||||
});
|
||||
}).then((_) {
|
||||
Logger.d("doConnect is finished 1");
|
||||
completer?.complete();
|
||||
}).catchError((e) {
|
||||
completer?.completeError(e);
|
||||
});
|
||||
} else {
|
||||
Logger.d("doConnect is finished 2");
|
||||
completer?.complete();
|
||||
}
|
||||
}
|
||||
|
||||
Completer connecting;
|
||||
|
||||
Future _connect() {
|
||||
if (connecting != null && !connecting.isCompleted) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future _disconnect() {
|
||||
Completer completer = Completer();
|
||||
if (!isConnected) {
|
||||
completer.complete();
|
||||
} 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) {
|
||||
if (data["type"] == "result") {
|
||||
if (data["id"] != null && data["success"]) {
|
||||
Logger.d("[Received] <== Request id ${data['id']} was successful");
|
||||
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
||||
} else if (data["id"] != null) {
|
||||
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
||||
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
|
||||
}
|
||||
_messageResolver.remove("${data["id"]}");
|
||||
} else if (data["type"] == "event") {
|
||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||
onStateChangeCallback(data["event"]["data"]);
|
||||
} else if (data["event"] != null) {
|
||||
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
|
||||
} else {
|
||||
Logger.e("Event is null: $data");
|
||||
}
|
||||
} else {
|
||||
Logger.d("[Received unhandled] <== ${data.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSocketClose(Completer connectionCompleter) {
|
||||
Logger.d("Socket disconnected.");
|
||||
if (!connectionCompleter.isCompleted) {
|
||||
isConnected = false;
|
||||
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||
} else {
|
||||
_disconnect().then((_) {
|
||||
Timer(Duration(seconds: 5), () {
|
||||
Logger.d("Trying to reconnect...");
|
||||
_connect().catchError((e) {
|
||||
isConnected = false;
|
||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSocketError(e, Completer connectionCompleter) {
|
||||
Logger.e("Socket stream Error: $e");
|
||||
if (!connectionCompleter.isCompleted) {
|
||||
isConnected = false;
|
||||
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
|
||||
} else {
|
||||
_disconnect().then((_) {
|
||||
Timer(Duration(seconds: 5), () {
|
||||
Logger.d("Trying to reconnect...");
|
||||
_connect().catchError((e) {
|
||||
isConnected = false;
|
||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future _authenticate() {
|
||||
Completer completer = Completer();
|
||||
if (_token != null) {
|
||||
Logger.d( "Long-lived token exist");
|
||||
Logger.d( "[Sending] ==> auth request");
|
||||
sendSocketMessage(
|
||||
type: "auth",
|
||||
additionalData: {"access_token": "$_token"},
|
||||
auth: true
|
||||
).then((_) {
|
||||
completer.complete();
|
||||
}).catchError((e) => completer.completeError(e));
|
||||
} else if (_tempToken != null) {
|
||||
Logger.d("We have temp token. Loging in...");
|
||||
sendSocketMessage(
|
||||
type: "auth",
|
||||
additionalData: {"access_token": "$_tempToken"},
|
||||
auth: true
|
||||
).then((_) {
|
||||
Logger.d("Requesting long-lived token...");
|
||||
_getLongLivedToken().then((_) {
|
||||
Logger.d("getLongLivedToken finished");
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
Logger.e("Can't get long-lived token: $e");
|
||||
throw e;
|
||||
});
|
||||
}).catchError((e) => completer.completeError(e));
|
||||
} else {
|
||||
completer.completeError(HAError("General login error"));
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future logout() {
|
||||
Completer completer = Completer();
|
||||
_disconnect().whenComplete(() {
|
||||
_token = null;
|
||||
_tempToken = null;
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.delete(key: "hacl_llt").whenComplete((){
|
||||
completer.complete();
|
||||
});
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future _getLongLivedToken() {
|
||||
Completer completer = Completer();
|
||||
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
|
||||
Logger.d("Got long-lived token.");
|
||||
_token = data;
|
||||
_tempToken = null;
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.write(key: "hacl_llt", value: "$_token").then((_) {
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
throw e;
|
||||
});
|
||||
}).catchError((e) {
|
||||
logout();
|
||||
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()]));
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future sendSocketMessage({String type, Map additionalData, bool auth: false}) {
|
||||
Completer _completer = Completer();
|
||||
Map dataObject = {"type": "$type"};
|
||||
String callbackName;
|
||||
if (!auth) {
|
||||
_incrementMessageId();
|
||||
dataObject["id"] = _currentMessageId;
|
||||
callbackName = "$_currentMessageId";
|
||||
} else {
|
||||
callbackName = "auth";
|
||||
}
|
||||
if (additionalData != null) {
|
||||
dataObject.addAll(additionalData);
|
||||
}
|
||||
_messageResolver[callbackName] = _completer;
|
||||
String rawMessage = json.encode(dataObject);
|
||||
if (!isConnected) {
|
||||
_connect().timeout(connectTimeout, onTimeout: (){
|
||||
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||
}).then((_) {
|
||||
Logger.d("[Sending] ==> $rawMessage");
|
||||
_socket.sink.add(rawMessage);
|
||||
}).catchError((e) {
|
||||
_completer.completeError(e);
|
||||
});
|
||||
} else {
|
||||
Logger.d("[Sending] ==> $rawMessage");
|
||||
_socket.sink.add(rawMessage);
|
||||
}
|
||||
return _completer.future;
|
||||
}
|
||||
|
||||
void _incrementMessageId() {
|
||||
_currentMessageId += 1;
|
||||
}
|
||||
|
||||
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
|
||||
Map serviceData = {};
|
||||
if (entityId != null) {
|
||||
serviceData["entity_id"] = entityId;
|
||||
}
|
||||
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
|
||||
serviceData.addAll(additionalServiceData);
|
||||
}
|
||||
if (serviceData.isNotEmpty)
|
||||
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
|
||||
else
|
||||
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
|
||||
}
|
||||
|
||||
Future<List> getHistory(String entityId) async {
|
||||
DateTime now = DateTime.now();
|
||||
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||
Logger.d("[Sending] ==> $url");
|
||||
http.Response historyResponse;
|
||||
historyResponse = await http.get(url, headers: {
|
||||
"authorization": "Bearer $_token",
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
var history = json.decode(historyResponse.body);
|
||||
if (history is List) {
|
||||
Logger.d( "[Received] <== ${history.first.length} history recors");
|
||||
return history;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
|
||||
Completer completer = Completer();
|
||||
String url = "$httpWebHost$endPoint";
|
||||
Logger.d("[Sending] ==> $url");
|
||||
Map<String, String> headers = {};
|
||||
if (contentType != null) {
|
||||
headers["Content-Type"] = contentType;
|
||||
}
|
||||
if (includeAuthHeader) {
|
||||
headers["authorization"] = "Bearer $_token";
|
||||
}
|
||||
http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: data
|
||||
).then((response) {
|
||||
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
|
||||
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
||||
completer.complete(response.body);
|
||||
} else {
|
||||
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
|
||||
}
|
||||
}).catchError((e) {
|
||||
completer.completeError(e);
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
}
|
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}";
|
||||
});
|
||||
}
|
||||
}
|
@ -19,8 +19,8 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
TheLogger.debug("State change event handled by entity page: ${event.entityId}");
|
||||
if (event.entityId == widget.entityId) {
|
||||
Logger.d("State change event handled by entity page: ${event.entityId}");
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
@ -46,13 +46,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: new Text(_title),
|
||||
),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: HomeAssistantModel(
|
||||
homeAssistant: widget.homeAssistant,
|
||||
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
|
||||
)
|
||||
),
|
||||
body: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||
);
|
||||
}
|
||||
|
||||
|
13
lib/entity_class/alarm_control_panel.class.dart
Normal file
@ -0,0 +1,13 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class AlarmControlPanelEntity extends Entity {
|
||||
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return AlarmControlPanelControlsWidget(
|
||||
extended: false,
|
||||
);
|
||||
}
|
||||
}
|
27
lib/entity_class/automation_entity.dart
Normal file
@ -0,0 +1,27 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class AutomationEntity extends Entity {
|
||||
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return SwitchStateWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
FlatServiceButton(
|
||||
serviceDomain: domain,
|
||||
entityId: entityId,
|
||||
text: "TRIGGER",
|
||||
serviceName: "trigger",
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,16 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ButtonEntity extends Entity {
|
||||
ButtonEntity(Map rawData) : super(rawData);
|
||||
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return ButtonStateWidget();
|
||||
return FlatServiceButton(
|
||||
entityId: entityId,
|
||||
serviceDomain: domain,
|
||||
serviceName: 'turn_on',
|
||||
text: domain == "scene" ? "ACTIVATE" : "EXECUTE",
|
||||
);
|
||||
}
|
||||
}
|
17
lib/entity_class/camera_entity.class.dart
Normal file
@ -0,0 +1,17 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class CameraEntity extends Entity {
|
||||
|
||||
static const SUPPORT_ON_OFF = 1;
|
||||
|
||||
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportOnOff => ((supportedFeatures &
|
||||
CameraEntity.SUPPORT_ON_OFF) ==
|
||||
CameraEntity.SUPPORT_ON_OFF);
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return CameraStreamView();
|
||||
}
|
||||
}
|
@ -10,69 +10,57 @@ class ClimateEntity extends Entity {
|
||||
);
|
||||
|
||||
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
||||
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
|
||||
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
|
||||
static const SUPPORT_TARGET_HUMIDITY = 8;
|
||||
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
|
||||
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
|
||||
static const SUPPORT_FAN_MODE = 64;
|
||||
static const SUPPORT_OPERATION_MODE = 128;
|
||||
static const SUPPORT_HOLD_MODE = 256;
|
||||
static const SUPPORT_SWING_MODE = 512;
|
||||
static const SUPPORT_AWAY_MODE = 1024;
|
||||
static const SUPPORT_AUX_HEAT = 2048;
|
||||
static const SUPPORT_ON_OFF = 4096;
|
||||
static const SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
|
||||
static const SUPPORT_TARGET_HUMIDITY = 4;
|
||||
static const SUPPORT_FAN_MODE = 8;
|
||||
static const SUPPORT_PRESET_MODE = 16;
|
||||
static const SUPPORT_SWING_MODE = 32;
|
||||
static const SUPPORT_AUX_HEAT = 64;
|
||||
|
||||
bool get supportTargetTemperature => ((attributes["supported_features"] &
|
||||
|
||||
//static const SUPPORT_OPERATION_MODE = 16;
|
||||
//static const SUPPORT_HOLD_MODE = 256;
|
||||
//static const SUPPORT_AWAY_MODE = 1024;
|
||||
//static const SUPPORT_ON_OFF = 4096;
|
||||
|
||||
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportTargetTemperature => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
||||
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
|
||||
bool get supportTargetTemperatureLow => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
|
||||
bool get supportTargetHumidity => ((attributes["supported_features"] &
|
||||
bool get supportTargetTemperatureRange => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) ==
|
||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE);
|
||||
bool get supportTargetHumidity => ((supportedFeatures &
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
||||
bool get supportTargetHumidityHigh => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
|
||||
bool get supportTargetHumidityLow => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
|
||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
|
||||
bool get supportFanMode =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||
ClimateEntity.SUPPORT_FAN_MODE);
|
||||
bool get supportOperationMode => ((attributes["supported_features"] &
|
||||
ClimateEntity.SUPPORT_OPERATION_MODE) ==
|
||||
ClimateEntity.SUPPORT_OPERATION_MODE);
|
||||
bool get supportHoldMode =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) ==
|
||||
ClimateEntity.SUPPORT_HOLD_MODE);
|
||||
bool get supportSwingMode =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||
ClimateEntity.SUPPORT_SWING_MODE);
|
||||
bool get supportAwayMode =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
|
||||
ClimateEntity.SUPPORT_AWAY_MODE);
|
||||
bool get supportPresetMode =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
|
||||
ClimateEntity.SUPPORT_PRESET_MODE);
|
||||
bool get supportAuxHeat =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||
ClimateEntity.SUPPORT_AUX_HEAT);
|
||||
bool get supportOnOff =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) ==
|
||||
ClimateEntity.SUPPORT_ON_OFF);
|
||||
|
||||
List<String> get operationList => attributes["operation_list"] != null
|
||||
? (attributes["operation_list"] as List).cast<String>()
|
||||
List<String> get hvacModes => attributes["hvac_modes"] != null
|
||||
? (attributes["hvac_modes"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get fanList => attributes["fan_list"] != null
|
||||
? (attributes["fan_list"] as List).cast<String>()
|
||||
List<String> get fanModes => attributes["fan_modes"] != null
|
||||
? (attributes["fan_modes"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get swingList => attributes["swing_list"] != null
|
||||
? (attributes["swing_list"] as List).cast<String>()
|
||||
List<String> get presetModes => attributes["preset_modes"] != null
|
||||
? (attributes["preset_modes"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get swingModes => attributes["swing_modes"] != null
|
||||
? (attributes["swing_modes"] as List).cast<String>()
|
||||
: null;
|
||||
double get temperature => _getDoubleAttributeValue('temperature');
|
||||
double get currentTemperature => _getDoubleAttributeValue('current_temperature');
|
||||
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
||||
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
||||
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
||||
@ -80,25 +68,23 @@ class ClimateEntity extends Entity {
|
||||
double get targetHumidity => _getDoubleAttributeValue('humidity');
|
||||
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||
String get operationMode => attributes['operation_mode'];
|
||||
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
|
||||
String get hvacAction => attributes['hvac_action'];
|
||||
String get fanMode => attributes['fan_mode'];
|
||||
String get presetMode => attributes['preset_mode'];
|
||||
String get swingMode => attributes['swing_mode'];
|
||||
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";
|
||||
|
||||
ClimateEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
void update(Map rawData) {
|
||||
super.update(rawData);
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
if (supportTargetTemperature) {
|
||||
historyConfig.numericAttributesToShow.add("temperature");
|
||||
}
|
||||
if (supportTargetTemperatureHigh) {
|
||||
if (supportTargetTemperatureRange) {
|
||||
historyConfig.numericAttributesToShow.add("target_temp_high");
|
||||
}
|
||||
if (supportTargetTemperatureLow) {
|
||||
historyConfig.numericAttributesToShow.add("target_temp_low");
|
||||
}
|
||||
}
|
||||
|
@ -28,13 +28,52 @@ class EntityState {
|
||||
static const unavailable = 'unavailable';
|
||||
static const ok = 'ok';
|
||||
static const problem = 'problem';
|
||||
static const active = 'active';
|
||||
}
|
||||
|
||||
class EntityTapAction {
|
||||
class EntityUIAction {
|
||||
static const moreInfo = 'more-info';
|
||||
static const toggle = 'toggle';
|
||||
static const callService = 'call-service';
|
||||
static const navigate = 'navigate';
|
||||
static const none = 'none';
|
||||
|
||||
String tapAction = EntityUIAction.moreInfo;
|
||||
String tapNavigationPath;
|
||||
String tapService;
|
||||
Map<String, dynamic> tapServiceData;
|
||||
String holdAction = EntityUIAction.none;
|
||||
String holdNavigationPath;
|
||||
String holdService;
|
||||
Map<String, dynamic> holdServiceData;
|
||||
|
||||
EntityUIAction({rawEntityData}) {
|
||||
if (rawEntityData != null) {
|
||||
if (rawEntityData["tap_action"] != null) {
|
||||
if (rawEntityData["tap_action"] is String) {
|
||||
tapAction = rawEntityData["tap_action"];
|
||||
} else {
|
||||
tapAction =
|
||||
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
|
||||
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
|
||||
tapService = rawEntityData["tap_action"]["service"];
|
||||
tapServiceData = rawEntityData["tap_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
if (rawEntityData["hold_action"] != null) {
|
||||
if (rawEntityData["hold_action"] is String) {
|
||||
holdAction = rawEntityData["hold_action"];
|
||||
} else {
|
||||
holdAction =
|
||||
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
|
||||
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
|
||||
holdService = rawEntityData["hold_action"]["service"];
|
||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CardType {
|
||||
@ -56,4 +95,5 @@ class CardType {
|
||||
static const entityButton = "entity-button";
|
||||
static const conditional = "conditional";
|
||||
static const alarmPanel = "alarm-panel";
|
||||
static const markdown = "markdown";
|
||||
}
|
@ -11,29 +11,31 @@ class CoverEntity extends Entity {
|
||||
static const SUPPORT_STOP_TILT = 64;
|
||||
static const SUPPORT_SET_TILT_POSITION = 128;
|
||||
|
||||
bool get supportOpen => ((attributes["supported_features"] &
|
||||
CoverEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportOpen => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_OPEN) ==
|
||||
CoverEntity.SUPPORT_OPEN);
|
||||
bool get supportClose => ((attributes["supported_features"] &
|
||||
bool get supportClose => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_CLOSE) ==
|
||||
CoverEntity.SUPPORT_CLOSE);
|
||||
bool get supportSetPosition => ((attributes["supported_features"] &
|
||||
bool get supportSetPosition => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_SET_POSITION) ==
|
||||
CoverEntity.SUPPORT_SET_POSITION);
|
||||
bool get supportStop => ((attributes["supported_features"] &
|
||||
bool get supportStop => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_STOP) ==
|
||||
CoverEntity.SUPPORT_STOP);
|
||||
|
||||
bool get supportOpenTilt => ((attributes["supported_features"] &
|
||||
bool get supportOpenTilt => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_OPEN_TILT) ==
|
||||
CoverEntity.SUPPORT_OPEN_TILT);
|
||||
bool get supportCloseTilt => ((attributes["supported_features"] &
|
||||
bool get supportCloseTilt => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_CLOSE_TILT) ==
|
||||
CoverEntity.SUPPORT_CLOSE_TILT);
|
||||
bool get supportStopTilt => ((attributes["supported_features"] &
|
||||
bool get supportStopTilt => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_STOP_TILT) ==
|
||||
CoverEntity.SUPPORT_STOP_TILT);
|
||||
bool get supportSetTiltPosition => ((attributes["supported_features"] &
|
||||
bool get supportSetTiltPosition => ((supportedFeatures &
|
||||
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
|
||||
CoverEntity.SUPPORT_SET_TILT_POSITION);
|
||||
|
||||
@ -45,8 +47,6 @@ class CoverEntity extends Entity {
|
||||
bool get canTiltBeOpened => currentTiltPosition < 100;
|
||||
bool get canTiltBeClosed => currentTiltPosition > 0;
|
||||
|
||||
CoverEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return CoverStateWidget();
|
||||
|
@ -1,6 +1,8 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class DateTimeEntity extends Entity {
|
||||
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get hasDate => attributes["has_date"] ?? false;
|
||||
bool get hasTime => attributes["has_time"] ?? false;
|
||||
int get year => attributes["year"] ?? 1970;
|
||||
@ -12,8 +14,6 @@ class DateTimeEntity extends Entity {
|
||||
String get formattedState => _getFormattedState();
|
||||
DateTime get dateTimeState => _getDateTimeState();
|
||||
|
||||
DateTimeEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return DateTimeStateWidget();
|
||||
|
@ -1,5 +1,14 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class StatelessEntityType {
|
||||
static const NONE = 0;
|
||||
static const MISSED = 1;
|
||||
static const DIVIDER = 2;
|
||||
static const SECTION = 3;
|
||||
static const CALL_SERVICE = 4;
|
||||
static const WEBLINK = 5;
|
||||
}
|
||||
|
||||
class Entity {
|
||||
|
||||
static List badgeDomains = [
|
||||
@ -12,14 +21,66 @@ class Entity {
|
||||
"sensor"
|
||||
];
|
||||
|
||||
static Map StateByDeviceClass = {
|
||||
"battery.on": "Low",
|
||||
"battery.off": "Normal",
|
||||
"cold.on": "Cold",
|
||||
"cold.off": "Normal",
|
||||
"connectivity.on": "Connected",
|
||||
"connectivity.off": "Disconnected",
|
||||
"door.on": "Open",
|
||||
"door.off": "Closed",
|
||||
"garage_door.on": "Open",
|
||||
"garage_door.off": "Closed",
|
||||
"gas.on": "Detected",
|
||||
"gas.off": "Clear",
|
||||
"heat.on": "Hot",
|
||||
"heat.off": "Normal",
|
||||
"light.on": "Detected",
|
||||
"lignt.off": "No light",
|
||||
"lock.on": "Unlocked",
|
||||
"lock.off": "Locked",
|
||||
"moisture.on": "Wet",
|
||||
"moisture.off": "Dry",
|
||||
"motion.on": "Detected",
|
||||
"motion.off": "Clear",
|
||||
"moving.on": "Moving",
|
||||
"moving.off": "Stopped",
|
||||
"occupancy.on": "Occupied",
|
||||
"occupancy.off": "Clear",
|
||||
"opening.on": "Open",
|
||||
"opening.off": "Closed",
|
||||
"plug.on": "Plugged in",
|
||||
"plug.off": "Unplugged",
|
||||
"power.on": "Powered",
|
||||
"power.off": "No power",
|
||||
"presence.on": "Home",
|
||||
"presence.off": "Away",
|
||||
"problem.on": "Problem",
|
||||
"problem.off": "OK",
|
||||
"safety.on": "Unsafe",
|
||||
"safety.off": "Safe",
|
||||
"smoke.on": "Detected",
|
||||
"smoke.off": "Clear",
|
||||
"sound.on": "Detected",
|
||||
"sound.off": "Clear",
|
||||
"vibration.on": "Detected",
|
||||
"vibration.off": "Clear",
|
||||
"window.on": "Open",
|
||||
"window.off": "Closed"
|
||||
};
|
||||
|
||||
Map attributes;
|
||||
String domain;
|
||||
String entityId;
|
||||
String entityPicture;
|
||||
String state;
|
||||
String displayState;
|
||||
DateTime _lastUpdated;
|
||||
int statelessType = 0;
|
||||
|
||||
List<Entity> childEntities = [];
|
||||
List<String> attributesToShow = ["all"];
|
||||
String deviceClass;
|
||||
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||
chartType: EntityHistoryWidgetType.simple
|
||||
);
|
||||
@ -27,7 +88,6 @@ class Entity {
|
||||
String get displayName =>
|
||||
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
|
||||
|
||||
String get deviceClass => attributes["device_class"] ?? null;
|
||||
bool get isView =>
|
||||
(domain == "group") &&
|
||||
(attributes != null ? attributes["view"] ?? false : false);
|
||||
@ -35,23 +95,68 @@ class Entity {
|
||||
bool get isBadge => Entity.badgeDomains.contains(domain);
|
||||
String get icon => attributes["icon"] ?? "";
|
||||
bool get isOn => state == EntityState.on;
|
||||
String get entityPicture => attributes["entity_picture"];
|
||||
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
||||
List get childEntityIds => attributes["entity_id"] ?? [];
|
||||
String get lastUpdated => _getLastUpdatedFormatted();
|
||||
bool get isHidden => attributes["hidden"] ?? false;
|
||||
double get doubleState => double.tryParse(state) ?? 0.0;
|
||||
int get supportedFeatures => attributes["supported_features"] ?? 0;
|
||||
|
||||
Entity(Map rawData) {
|
||||
update(rawData);
|
||||
String _getEntityPictureUrl(String webHost) {
|
||||
String result = attributes["entity_picture"];
|
||||
if (result == null) return result;
|
||||
if (!result.startsWith("http")) {
|
||||
if (result.startsWith("/")) {
|
||||
result = "$webHost$result";
|
||||
} else {
|
||||
result = "$webHost/$result";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void update(Map rawData) {
|
||||
Entity(Map rawData, String webHost) {
|
||||
update(rawData, webHost);
|
||||
}
|
||||
|
||||
Entity.missed(String entityId) {
|
||||
statelessType = StatelessEntityType.MISSED;
|
||||
attributes = {"hidden": false};
|
||||
this.entityId = entityId;
|
||||
}
|
||||
|
||||
Entity.divider() {
|
||||
statelessType = StatelessEntityType.DIVIDER;
|
||||
attributes = {"hidden": false};
|
||||
}
|
||||
|
||||
Entity.section(String label) {
|
||||
statelessType = StatelessEntityType.SECTION;
|
||||
attributes = {"hidden": false, "friendly_name": "$label"};
|
||||
}
|
||||
|
||||
Entity.callService({String icon, String name, String service, String actionName}) {
|
||||
statelessType = StatelessEntityType.CALL_SERVICE;
|
||||
entityId = service;
|
||||
displayState = actionName?.toUpperCase() ?? "RUN";
|
||||
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
||||
}
|
||||
|
||||
Entity.weblink({String url, String name, String icon}) {
|
||||
statelessType = StatelessEntityType.WEBLINK;
|
||||
entityId = "custom.custom"; //TODO wtf??
|
||||
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||
}
|
||||
|
||||
void update(Map rawData, String webHost) {
|
||||
attributes = rawData["attributes"] ?? {};
|
||||
domain = rawData["entity_id"].split(".")[0];
|
||||
entityId = rawData["entity_id"];
|
||||
deviceClass = attributes["device_class"];
|
||||
state = rawData["state"];
|
||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||
entityPicture = _getEntityPictureUrl(webHost);
|
||||
}
|
||||
|
||||
double _getDoubleAttributeValue(String attributeName) {
|
||||
@ -110,7 +215,10 @@ class Entity {
|
||||
return EntityModel(
|
||||
entityWrapper: EntityWrapper(entity: this),
|
||||
child: EntityPageContainer(children: <Widget>[
|
||||
DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
||||
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
||||
),
|
||||
LastUpdatedWidget(),
|
||||
Divider(),
|
||||
_buildAdditionalControlsForPage(context),
|
||||
|
@ -4,12 +4,8 @@ class EntityWrapper {
|
||||
|
||||
String displayName;
|
||||
String icon;
|
||||
String tapAction;
|
||||
String holdAction;
|
||||
String tapActionService;
|
||||
Map<String, dynamic> tapActionServiceData;
|
||||
String holdActionService;
|
||||
Map<String, dynamic> holdActionServiceData;
|
||||
String entityPicture;
|
||||
EntityUIAction uiAction;
|
||||
Entity entity;
|
||||
|
||||
|
||||
@ -17,64 +13,98 @@ class EntityWrapper {
|
||||
this.entity,
|
||||
String icon,
|
||||
String displayName,
|
||||
this.tapAction: EntityTapAction.moreInfo,
|
||||
this.holdAction: EntityTapAction.none,
|
||||
this.tapActionService,
|
||||
this.tapActionServiceData,
|
||||
this.holdActionService,
|
||||
this.holdActionServiceData
|
||||
this.uiAction
|
||||
}) {
|
||||
this.icon = icon ?? entity.icon;
|
||||
this.displayName = displayName ?? entity.displayName;
|
||||
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||
this.icon = icon ?? entity.icon;
|
||||
if (icon == null) {
|
||||
entityPicture = entity.entityPicture;
|
||||
}
|
||||
this.displayName = displayName ?? entity.displayName;
|
||||
if (uiAction == null) {
|
||||
uiAction = EntityUIAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleTap() {
|
||||
TheLogger.debug(tapAction);
|
||||
switch (tapAction) {
|
||||
case EntityTapAction.toggle: {
|
||||
switch (uiAction.tapAction) {
|
||||
case EntityUIAction.toggle: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityTapAction.callService: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData));
|
||||
case EntityUIAction.callService: {
|
||||
if (uiAction.tapService != null) {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent(uiAction.tapService.split(".")[0],
|
||||
uiAction.tapService.split(".")[1], null,
|
||||
uiAction.tapServiceData));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityTapAction.none: {
|
||||
case EntityUIAction.none: {
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.moreInfo: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entity));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.tapService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
} else {
|
||||
HAUtils.launchURL(uiAction.tapService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entity));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleHold() {
|
||||
switch (holdAction) {
|
||||
case EntityTapAction.toggle: {
|
||||
switch (uiAction.holdAction) {
|
||||
case EntityUIAction.toggle: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityTapAction.callService: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData));
|
||||
case EntityUIAction.callService: {
|
||||
if (uiAction.holdService != null) {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent(uiAction.holdService.split(".")[0],
|
||||
uiAction.holdService.split(".")[1], null,
|
||||
uiAction.holdServiceData));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityTapAction.moreInfo: {
|
||||
case EntityUIAction.moreInfo: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entity));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.holdService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
} else {
|
||||
HAUtils.launchURL(uiAction.holdService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
|
@ -6,15 +6,15 @@ class FanEntity extends Entity {
|
||||
static const SUPPORT_OSCILLATE = 2;
|
||||
static const SUPPORT_DIRECTION = 4;
|
||||
|
||||
FanEntity(Map rawData) : super(rawData);
|
||||
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportSetSpeed => ((attributes["supported_features"] &
|
||||
bool get supportSetSpeed => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_SET_SPEED) ==
|
||||
FanEntity.SUPPORT_SET_SPEED);
|
||||
bool get supportOscillate => ((attributes["supported_features"] &
|
||||
bool get supportOscillate => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_OSCILLATE) ==
|
||||
FanEntity.SUPPORT_OSCILLATE);
|
||||
bool get supportDirection => ((attributes["supported_features"] &
|
||||
bool get supportDirection => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_DIRECTION) ==
|
||||
FanEntity.SUPPORT_DIRECTION);
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class GroupEntity extends Entity {
|
||||
GroupEntity(Map rawData) : super(rawData);
|
||||
|
||||
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
|
||||
String mutualDomain;
|
||||
bool switchable = false;
|
||||
|
||||
GroupEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
if (switchable) {
|
||||
@ -19,8 +20,8 @@ class GroupEntity extends Entity {
|
||||
}
|
||||
|
||||
@override
|
||||
void update(Map rawData) {
|
||||
super.update(rawData);
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
if (_isOneDomain()) {
|
||||
mutualDomain = attributes['entity_id'][0].split(".")[0];
|
||||
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
|
||||
|
@ -10,43 +10,50 @@ class LightEntity extends Entity {
|
||||
static const SUPPORT_TRANSITION = 32;
|
||||
static const SUPPORT_WHITE_VALUE = 128;
|
||||
|
||||
bool get supportBrightness => ((attributes["supported_features"] &
|
||||
bool get supportBrightness => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_BRIGHTNESS) ==
|
||||
LightEntity.SUPPORT_BRIGHTNESS);
|
||||
bool get supportColorTemp => ((attributes["supported_features"] &
|
||||
bool get supportColorTemp => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_COLOR_TEMP) ==
|
||||
LightEntity.SUPPORT_COLOR_TEMP);
|
||||
bool get supportEffect => ((attributes["supported_features"] &
|
||||
bool get supportEffect => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_EFFECT) ==
|
||||
LightEntity.SUPPORT_EFFECT);
|
||||
bool get supportFlash => ((attributes["supported_features"] &
|
||||
bool get supportFlash => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_FLASH) ==
|
||||
LightEntity.SUPPORT_FLASH);
|
||||
bool get supportColor => ((attributes["supported_features"] &
|
||||
bool get supportColor => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_COLOR) ==
|
||||
LightEntity.SUPPORT_COLOR);
|
||||
bool get supportTransition => ((attributes["supported_features"] &
|
||||
bool get supportTransition => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_TRANSITION) ==
|
||||
LightEntity.SUPPORT_TRANSITION);
|
||||
bool get supportWhiteValue => ((attributes["supported_features"] &
|
||||
bool get supportWhiteValue => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_WHITE_VALUE) ==
|
||||
LightEntity.SUPPORT_WHITE_VALUE);
|
||||
|
||||
int get brightness => _getIntAttributeValue("brightness");
|
||||
int get whiteValue => _getIntAttributeValue("white_value");
|
||||
String get effect => attributes["effect"];
|
||||
int get colorTemp => _getIntAttributeValue("color_temp");
|
||||
double get maxMireds => _getDoubleAttributeValue("max_mireds");
|
||||
double get minMireds => _getDoubleAttributeValue("min_mireds");
|
||||
Color get color => _getColor();
|
||||
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
|
||||
HSVColor get color => _getColor();
|
||||
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
|
||||
List<String> get effectList => getStringListAttributeValue("effect_list");
|
||||
|
||||
LightEntity(Map rawData) : super(rawData);
|
||||
LightEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
Color _getColor() {
|
||||
HSVColor _getColor() {
|
||||
List hs = attributes["hs_color"];
|
||||
List rgb = attributes["rgb_color"];
|
||||
try {
|
||||
if ((rgb != null) && (rgb.length > 0)) {
|
||||
return Color.fromARGB(255, rgb[0], rgb[1], rgb[2]);
|
||||
if (hs != null && hs.isNotEmpty) {
|
||||
double sat = hs[1]/100;
|
||||
String ssat = sat.toStringAsFixed(2);
|
||||
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
|
||||
} else if (rgb != null && rgb.isNotEmpty) {
|
||||
return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2]));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -62,7 +69,7 @@ class LightEntity extends Entity {
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
if (!isAdditionalControls) {
|
||||
if (!isAdditionalControls || state == EntityState.unavailable) {
|
||||
return Container(height: 0.0, width: 0.0);
|
||||
} else {
|
||||
return LightControlsWidget();
|
||||
|
@ -1,12 +1,21 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class LockEntity extends Entity {
|
||||
LockEntity(Map rawData) : super(rawData);
|
||||
LockEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get isLocked => state == "locked";
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return LockStateWidget();
|
||||
return LockStateWidget(
|
||||
assumedState: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildStatePartForPage(BuildContext context) {
|
||||
return LockStateWidget(
|
||||
assumedState: true,
|
||||
);
|
||||
}
|
||||
}
|
@ -20,55 +20,55 @@ class MediaPlayerEntity extends Entity {
|
||||
static const SUPPORT_SHUFFLE_SET = 32768;
|
||||
static const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||
|
||||
MediaPlayerEntity(Map rawData) : super(rawData);
|
||||
MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportPause => ((attributes["supported_features"] &
|
||||
bool get supportPause => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PAUSE) ==
|
||||
MediaPlayerEntity.SUPPORT_PAUSE);
|
||||
bool get supportSeek => ((attributes["supported_features"] &
|
||||
bool get supportSeek => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_SEEK) ==
|
||||
MediaPlayerEntity.SUPPORT_SEEK);
|
||||
bool get supportVolumeSet => ((attributes["supported_features"] &
|
||||
bool get supportVolumeSet => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_SET) ==
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_SET);
|
||||
bool get supportVolumeMute => ((attributes["supported_features"] &
|
||||
bool get supportVolumeMute => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_MUTE) ==
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_MUTE);
|
||||
bool get supportPreviousTrack => ((attributes["supported_features"] &
|
||||
bool get supportPreviousTrack => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) ==
|
||||
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK);
|
||||
bool get supportNextTrack => ((attributes["supported_features"] &
|
||||
bool get supportNextTrack => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_NEXT_TRACK) ==
|
||||
MediaPlayerEntity.SUPPORT_NEXT_TRACK);
|
||||
|
||||
bool get supportTurnOn => ((attributes["supported_features"] &
|
||||
bool get supportTurnOn => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_TURN_ON) ==
|
||||
MediaPlayerEntity.SUPPORT_TURN_ON);
|
||||
bool get supportTurnOff => ((attributes["supported_features"] &
|
||||
bool get supportTurnOff => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_TURN_OFF) ==
|
||||
MediaPlayerEntity.SUPPORT_TURN_OFF);
|
||||
bool get supportPlayMedia => ((attributes["supported_features"] &
|
||||
bool get supportPlayMedia => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PLAY_MEDIA) ==
|
||||
MediaPlayerEntity.SUPPORT_PLAY_MEDIA);
|
||||
bool get supportVolumeStep => ((attributes["supported_features"] &
|
||||
bool get supportVolumeStep => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_STEP) ==
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_STEP);
|
||||
bool get supportSelectSource => ((attributes["supported_features"] &
|
||||
bool get supportSelectSource => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_SELECT_SOURCE) ==
|
||||
MediaPlayerEntity.SUPPORT_SELECT_SOURCE);
|
||||
bool get supportStop => ((attributes["supported_features"] &
|
||||
bool get supportStop => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_STOP) ==
|
||||
MediaPlayerEntity.SUPPORT_STOP);
|
||||
bool get supportClearPlaylist => ((attributes["supported_features"] &
|
||||
bool get supportClearPlaylist => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) ==
|
||||
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST);
|
||||
bool get supportPlay => ((attributes["supported_features"] &
|
||||
bool get supportPlay => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PLAY) ==
|
||||
MediaPlayerEntity.SUPPORT_PLAY);
|
||||
bool get supportShuffleSet => ((attributes["supported_features"] &
|
||||
bool get supportShuffleSet => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_SHUFFLE_SET) ==
|
||||
MediaPlayerEntity.SUPPORT_SHUFFLE_SET);
|
||||
bool get supportSelectSoundMode => ((attributes["supported_features"] &
|
||||
bool get supportSelectSoundMode => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) ==
|
||||
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SunEntity extends Entity {
|
||||
SunEntity(Map rawData) : super(rawData);
|
||||
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
}
|
||||
|
||||
class SensorEntity extends Entity {
|
||||
@ -12,6 +12,6 @@ class SensorEntity extends Entity {
|
||||
numericState: true
|
||||
);
|
||||
|
||||
SensorEntity(Map rawData) : super(rawData);
|
||||
SensorEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
}
|
@ -5,7 +5,7 @@ class SelectEntity extends Entity {
|
||||
? (attributes["options"] as List).cast<String>()
|
||||
: [];
|
||||
|
||||
SelectEntity(Map rawData) : super(rawData);
|
||||
SelectEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SliderEntity extends Entity {
|
||||
SliderEntity(Map rawData) : super(rawData);
|
||||
SliderEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
double get minValue => _getDoubleAttributeValue("min") ?? 0.0;
|
||||
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SwitchEntity extends Entity {
|
||||
SwitchEntity(Map rawData) : super(rawData);
|
||||
SwitchEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class TextEntity extends Entity {
|
||||
TextEntity(Map rawData) : super(rawData);
|
||||
TextEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
int get valueMinLength => attributes["min"] ?? -1;
|
||||
int get valueMaxLength => attributes["max"] ?? -1;
|
||||
|
45
lib/entity_class/timer_entity.dart
Normal file
@ -0,0 +1,45 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class TimerEntity extends Entity {
|
||||
TimerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
Duration duration;
|
||||
|
||||
@override
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
String durationSource = "${attributes["duration"]}";
|
||||
if (durationSource != null && durationSource.isNotEmpty) {
|
||||
try {
|
||||
List<String> durationList = durationSource.split(":");
|
||||
if (durationList.length == 1) {
|
||||
duration = Duration(seconds: int.tryParse(durationList[0] ?? 0));
|
||||
} else if (durationList.length == 2) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0
|
||||
);
|
||||
} else if (durationList.length == 3) {
|
||||
duration = Duration(
|
||||
hours: int.tryParse(durationList[0]) ?? 0,
|
||||
minutes: int.tryParse(durationList[1]) ?? 0,
|
||||
seconds: int.tryParse(durationList[2]) ?? 0
|
||||
);
|
||||
} else {
|
||||
Logger.e("Strange $entityId duration format: $durationSource");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("Error parsing duration for $entityId: ${e.toString()}");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} else {
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return TimerState();
|
||||
}
|
||||
}
|
@ -2,13 +2,15 @@ part of 'main.dart';
|
||||
|
||||
class EntityCollection {
|
||||
|
||||
final homeAssistantWebHost;
|
||||
|
||||
Map<String, Entity> _allEntities;
|
||||
//Map<String, Entity> views;
|
||||
|
||||
bool get isEmpty => _allEntities.isEmpty;
|
||||
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
||||
|
||||
EntityCollection() {
|
||||
EntityCollection(this.homeAssistantWebHost) {
|
||||
_allEntities = {};
|
||||
//views = {};
|
||||
}
|
||||
@ -19,7 +21,7 @@ class EntityCollection {
|
||||
_allEntities.clear();
|
||||
//views.clear();
|
||||
|
||||
TheLogger.debug("Parsing ${rawData.length} Home Assistant entities");
|
||||
Logger.d("Parsing ${rawData.length} Home Assistant entities");
|
||||
rawData.forEach((rawEntityData) {
|
||||
addFromRaw(rawEntityData);
|
||||
});
|
||||
@ -33,67 +35,85 @@ class EntityCollection {
|
||||
});
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_allEntities.clear();
|
||||
}
|
||||
|
||||
Entity _createEntityInstance(rawEntityData) {
|
||||
switch (rawEntityData["entity_id"].split(".")[0]) {
|
||||
case 'sun': {
|
||||
return SunEntity(rawEntityData);
|
||||
return SunEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "media_player": {
|
||||
return MediaPlayerEntity(rawEntityData);
|
||||
return MediaPlayerEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case 'sensor': {
|
||||
return SensorEntity(rawEntityData);
|
||||
return SensorEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case 'lock': {
|
||||
return LockEntity(rawEntityData);
|
||||
return LockEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "automation":
|
||||
case "automation": {
|
||||
return AutomationEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
|
||||
case "input_boolean":
|
||||
case "switch": {
|
||||
return SwitchEntity(rawEntityData);
|
||||
return SwitchEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "light": {
|
||||
return LightEntity(rawEntityData);
|
||||
return LightEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "group": {
|
||||
return GroupEntity(rawEntityData);
|
||||
return GroupEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "script":
|
||||
case "scene": {
|
||||
return ButtonEntity(rawEntityData);
|
||||
return ButtonEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_datetime": {
|
||||
return DateTimeEntity(rawEntityData);
|
||||
return DateTimeEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_select": {
|
||||
return SelectEntity(rawEntityData);
|
||||
return SelectEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_number": {
|
||||
return SliderEntity(rawEntityData);
|
||||
return SliderEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_text": {
|
||||
return TextEntity(rawEntityData);
|
||||
return TextEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "climate": {
|
||||
return ClimateEntity(rawEntityData);
|
||||
return ClimateEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "cover": {
|
||||
return CoverEntity(rawEntityData);
|
||||
return CoverEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "fan": {
|
||||
return FanEntity(rawEntityData);
|
||||
return FanEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "camera": {
|
||||
return CameraEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "alarm_control_panel": {
|
||||
return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "timer": {
|
||||
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
default: {
|
||||
return Entity(rawEntityData);
|
||||
return Entity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateState(Map rawStateData) {
|
||||
bool updateState(Map rawStateData) {
|
||||
if (isExist(rawStateData["entity_id"])) {
|
||||
updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
|
||||
return false;
|
||||
} else {
|
||||
addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,14 +121,13 @@ class EntityCollection {
|
||||
_allEntities[entity.entityId] = entity;
|
||||
}
|
||||
|
||||
Entity addFromRaw(Map rawEntityData) {
|
||||
void addFromRaw(Map rawEntityData) {
|
||||
Entity entity = _createEntityInstance(rawEntityData);
|
||||
_allEntities[entity.entityId] = entity;
|
||||
return entity;
|
||||
}
|
||||
|
||||
void updateFromRaw(Map rawEntityData) {
|
||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
|
||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
|
||||
Entity get(String entityId) {
|
||||
|
@ -9,7 +9,12 @@ class ButtonEntityContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
@ -19,11 +24,11 @@ class ButtonEntityContainer extends StatelessWidget {
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitHeight,
|
||||
child: EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
iconSize: Sizes.iconSize,
|
||||
)
|
||||
fit: BoxFit.fitHeight,
|
||||
child: EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
size: Sizes.iconSize,
|
||||
)
|
||||
),
|
||||
),
|
||||
_buildName()
|
||||
|
@ -14,40 +14,60 @@ class BadgeWidget extends StatelessWidget {
|
||||
{
|
||||
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
||||
? Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
|
||||
MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
|
||||
size: iconSize,
|
||||
)
|
||||
: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
|
||||
MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
|
||||
size: iconSize,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "sensor":
|
||||
case "camera":
|
||||
case "media_player":
|
||||
case "binary_sensor":
|
||||
{
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.state}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 17.0),
|
||||
),
|
||||
badgeIcon = EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
color: Colors.black
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
{
|
||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||
entityModel.entityWrapper, iconSize, Colors.black);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.state;
|
||||
badgeIcon = EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
color: Colors.black
|
||||
);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||
entityModel.entityWrapper, iconSize, Colors.black);
|
||||
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;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: stateFontSize),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Widget onBadgeText;
|
||||
|
70
lib/entity_widgets/common/camera_stream_view.dart
Normal file
@ -0,0 +1,70 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class CameraStreamView extends StatefulWidget {
|
||||
|
||||
CameraStreamView({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CameraStreamViewState createState() => _CameraStreamViewState();
|
||||
}
|
||||
|
||||
class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
CameraEntity _entity;
|
||||
bool started = false;
|
||||
String streamUrl = "";
|
||||
|
||||
launchStream() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebviewScaffold(
|
||||
url: "$streamUrl",
|
||||
withZoom: true,
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context)
|
||||
),
|
||||
title: new Text("${_entity.displayName}"),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!started) {
|
||||
_entity = EntityModel
|
||||
.of(context)
|
||||
.entityWrapper
|
||||
.entity;
|
||||
started = true;
|
||||
}
|
||||
streamUrl = '${Connection().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
.entityId}?token=${_entity.attributes['access_token']}';
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
||||
iconSize: 50.0,
|
||||
onPressed: () => launchStream(),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -7,24 +7,16 @@ class EntityAttributesList extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
List<Widget> attrs = [];
|
||||
if ((entityModel.entityWrapper.entity.attributesToShow == null) ||
|
||||
(entityModel.entityWrapper.entity.attributesToShow.contains("all"))) {
|
||||
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
|
||||
attrs.add(_buildSingleAttribute("$name", "$value"));
|
||||
});
|
||||
} else {
|
||||
entityModel.entityWrapper.entity.attributesToShow.forEach((String attr) {
|
||||
String attrValue = entityModel.entityWrapper.entity.getAttribute("$attr");
|
||||
if (attrValue != null) {
|
||||
attrs.add(
|
||||
_buildSingleAttribute("$attr", "$attrValue"));
|
||||
}
|
||||
});
|
||||
}
|
||||
return Column(
|
||||
children: attrs,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
|
||||
attrs.add(_buildSingleAttribute("$name", "${value ?? '-'}"));
|
||||
});
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
child: Column(
|
||||
children: attrs,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -46,7 +38,7 @@ class EntityAttributesList extends StatelessWidget {
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||
child: Text(
|
||||
"$value",
|
||||
"${value}",
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
|
41
lib/entity_widgets/common/flat_service_button.dart
Normal file
@ -0,0 +1,41 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class FlatServiceButton extends StatelessWidget {
|
||||
|
||||
final String serviceDomain;
|
||||
final String serviceName;
|
||||
final String entityId;
|
||||
final String text;
|
||||
final double fontSize;
|
||||
|
||||
FlatServiceButton({
|
||||
Key key,
|
||||
@required this.serviceDomain,
|
||||
@required this.serviceName,
|
||||
@required this.entityId,
|
||||
@required this.text,
|
||||
this.fontSize: Sizes.stateFontSize
|
||||
}) : super(key: key);
|
||||
|
||||
void _setNewState() {
|
||||
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: fontSize*2.5,
|
||||
child: FlatButton(
|
||||
onPressed: (() {
|
||||
_setNewState();
|
||||
}),
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: fontSize, color: Colors.blue),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ class LastUpdatedWidget extends StatelessWidget {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
Sizes.leftWidgetPadding, 0.0, 0.0, 0.0),
|
||||
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, 0.0),
|
||||
child: Text(
|
||||
'${entityModel.entityWrapper.entity.lastUpdated}',
|
||||
textAlign: TextAlign.left,
|
||||
|
101
lib/entity_widgets/common/light_color_picker.dart
Normal file
@ -0,0 +1,101 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightColorPicker extends StatefulWidget {
|
||||
|
||||
final HSVColor color;
|
||||
final onColorSelected;
|
||||
final double hueStep;
|
||||
final double saturationStep;
|
||||
final EdgeInsets padding;
|
||||
|
||||
LightColorPicker({this.color, this.onColorSelected, this.hueStep: 15.0, this.saturationStep: 0.2, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)});
|
||||
|
||||
@override
|
||||
LightColorPickerState createState() => new LightColorPickerState();
|
||||
}
|
||||
|
||||
class LightColorPickerState extends State<LightColorPicker> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> colorRows = [];
|
||||
Border border;
|
||||
bool isSomethingSelected = false;
|
||||
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
|
||||
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
|
||||
List<Widget> rowChildren = [];
|
||||
//Logger.d("$saturation");
|
||||
double roundedSaturation = double.parse(widget.color.saturation.toStringAsFixed(1));
|
||||
//Logger.d("Rounded saturation=$roundedSaturation");
|
||||
for (double hue = 0; hue <= (365 - widget.hueStep);
|
||||
hue += widget.hueStep) {
|
||||
bool isExactHue = widget.color.hue.round() == hue;
|
||||
bool isHueInRange = widget.color.hue.round() > hue && widget.color.hue.round() < (hue+widget.hueStep);
|
||||
bool isExactSaturation = roundedSaturation == saturation;
|
||||
bool isSaturationInRange = roundedSaturation > saturation && roundedSaturation < double.parse((saturation+widget.saturationStep).toStringAsFixed(1));
|
||||
if ((isExactHue || isHueInRange) && (isExactSaturation || isSaturationInRange)) {
|
||||
//Logger.d("$isExactHue $isHueInRange $isExactSaturation $isSaturationInRange (${saturation+widget.saturationStep})");
|
||||
border = Border.all(
|
||||
width: 2.0,
|
||||
color: Colors.white,
|
||||
);
|
||||
isSomethingSelected = true;
|
||||
} else {
|
||||
border = null;
|
||||
}
|
||||
HSVColor currentColor = HSVColor.fromAHSV(1.0, hue, double.parse(saturation.toStringAsFixed(2)), 1.0);
|
||||
rowChildren.add(
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
child: Container(
|
||||
height: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
color: currentColor.toColor(),
|
||||
border: border,
|
||||
),
|
||||
),
|
||||
onTap: () => widget.onColorSelected(currentColor),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
colorRows.add(
|
||||
Row(
|
||||
children: rowChildren,
|
||||
)
|
||||
);
|
||||
}
|
||||
colorRows.add(
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
child: Container(
|
||||
height: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: isSomethingSelected ? null : Border.all(
|
||||
width: 2.0,
|
||||
color: Colors.amber[200],
|
||||
)
|
||||
),
|
||||
),
|
||||
onTap: () => widget.onColorSelected(HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0)),
|
||||
)
|
||||
)
|
||||
);
|
||||
return Padding(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: colorRows,
|
||||
),
|
||||
padding: widget.padding,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
@ -7,8 +7,8 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
final String value;
|
||||
final double captionFontSize;
|
||||
final double valueFontSize;
|
||||
final double bottomPadding;
|
||||
final onChange;
|
||||
final EdgeInsets padding;
|
||||
|
||||
ModeSelectorWidget({
|
||||
Key key,
|
||||
@ -18,45 +18,47 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
@required this.onChange,
|
||||
this.captionFontSize,
|
||||
this.valueFontSize,
|
||||
this.bottomPadding
|
||||
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("$caption", style: TextStyle(
|
||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||
)),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: ButtonTheme(
|
||||
alignedDropdown: true,
|
||||
child: DropdownButton<String>(
|
||||
value: value,
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
style: TextStyle(
|
||||
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
||||
color: Colors.black,
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("$caption", style: TextStyle(
|
||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||
)),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: ButtonTheme(
|
||||
alignedDropdown: true,
|
||||
child: DropdownButton<String>(
|
||||
value: value,
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
style: TextStyle(
|
||||
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
||||
color: Colors.black,
|
||||
),
|
||||
hint: Text("Select ${caption.toLowerCase()}"),
|
||||
items: options.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => onChange(mode),
|
||||
),
|
||||
hint: Text("Select ${caption.toLowerCase()}"),
|
||||
items: options.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => onChange(mode),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(height: bottomPadding ?? Sizes.rowPadding,)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ class ModeSwitchWidget extends StatelessWidget {
|
||||
final double captionFontSize;
|
||||
final bool value;
|
||||
final bool expanded;
|
||||
final EdgeInsets padding;
|
||||
|
||||
ModeSwitchWidget({
|
||||
Key key,
|
||||
@ -14,19 +15,23 @@ class ModeSwitchWidget extends StatelessWidget {
|
||||
@required this.onChange,
|
||||
this.captionFontSize,
|
||||
this.value,
|
||||
this.expanded: true
|
||||
this.expanded: true,
|
||||
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
_buildCaption(),
|
||||
Switch(
|
||||
onChanged: (value) => onChange(value),
|
||||
value: value ?? false,
|
||||
)
|
||||
],
|
||||
return Padding(
|
||||
padding: this.padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
_buildCaption(),
|
||||
Switch(
|
||||
onChanged: (value) => onChange(value),
|
||||
value: value ?? false,
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,9 @@ class UniversalSlider extends StatelessWidget {
|
||||
final double min;
|
||||
final double max;
|
||||
final double value;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value}) : super(key: key);
|
||||
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -33,21 +34,24 @@ class UniversalSlider extends StatelessWidget {
|
||||
if (closing != null) {
|
||||
row.add(closing);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text(
|
||||
"$title",
|
||||
style: TextStyle(fontSize: Sizes.stateFontSize),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: row,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,)
|
||||
],
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text(
|
||||
"$title",
|
||||
style: TextStyle(fontSize: Sizes.stateFontSize),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: row,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
262
lib/entity_widgets/controls/alarm_control_panel_controls.dart
Normal file
@ -0,0 +1,262 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class AlarmControlPanelControlsWidget extends StatefulWidget {
|
||||
|
||||
final bool extended;
|
||||
final List states;
|
||||
|
||||
const AlarmControlPanelControlsWidget({Key key, @required this.extended, this.states}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AlarmControlPanelControlsWidgetWidgetState createState() => _AlarmControlPanelControlsWidgetWidgetState();
|
||||
|
||||
}
|
||||
|
||||
class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPanelControlsWidget> {
|
||||
|
||||
String code = "";
|
||||
List supportedStates;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
supportedStates = widget.states ?? ["arm_home", "arm_away"];
|
||||
}
|
||||
|
||||
|
||||
void _callService(AlarmControlPanelEntity entity, String service) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, service, entity.entityId,
|
||||
{"code": "$code"}));
|
||||
setState(() {
|
||||
code = "";
|
||||
});
|
||||
}
|
||||
|
||||
void _pinPadHandler(value) {
|
||||
setState(() {
|
||||
code += "$value";
|
||||
});
|
||||
}
|
||||
|
||||
void _pinPadClear() {
|
||||
setState(() {
|
||||
code = "";
|
||||
});
|
||||
}
|
||||
|
||||
void _askToTrigger(AlarmControlPanelEntity entity) {
|
||||
// flutter defined function
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
// return object of type Dialog
|
||||
return AlertDialog(
|
||||
title: new Text("Are you sure?"),
|
||||
content: new Text("Are you sure want to trigger alarm ${entity.displayName}?"),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: new Text("Yes"),
|
||||
onPressed: () {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: new Text("No"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final AlarmControlPanelEntity entity = entityModel.entityWrapper.entity;
|
||||
List<Widget> buttons = [];
|
||||
if (entity.state == EntityState.alarm_disarmed) {
|
||||
if (supportedStates.contains("arm_home")) {
|
||||
buttons.add(
|
||||
RaisedButton(
|
||||
onPressed: () => _callService(entity, "alarm_arm_home"),
|
||||
child: Text("ARM HOME"),
|
||||
)
|
||||
);
|
||||
}
|
||||
if (supportedStates.contains("arm_away")) {
|
||||
buttons.add(
|
||||
RaisedButton(
|
||||
onPressed: () => _callService(entity, "alarm_arm_away"),
|
||||
child: Text("ARM AWAY"),
|
||||
)
|
||||
);
|
||||
}
|
||||
if (widget.extended) {
|
||||
if (supportedStates.contains("arm_night")) {
|
||||
buttons.add(
|
||||
RaisedButton(
|
||||
onPressed: () => _callService(entity, "alarm_arm_night"),
|
||||
child: Text("ARM NIGHT"),
|
||||
)
|
||||
);
|
||||
}
|
||||
if (supportedStates.contains("arm_custom_bypass")) {
|
||||
buttons.add(
|
||||
RaisedButton(
|
||||
onPressed: () =>
|
||||
_callService(entity, "alarm_arm_custom_bypass"),
|
||||
child: Text("ARM CUSTOM BYPASS"),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buttons.add(
|
||||
RaisedButton(
|
||||
onPressed: () => _callService(entity, "alarm_disarm"),
|
||||
child: Text("DISARM"),
|
||||
)
|
||||
);
|
||||
}
|
||||
Widget pinPad;
|
||||
if (entity.attributes["code_format"] == null) {
|
||||
pinPad = Container(width: 0.0, height: 0.0,);
|
||||
} else {
|
||||
pinPad = Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Wrap(
|
||||
spacing: 5.0,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("1"),
|
||||
child: Text("1"),
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("2"),
|
||||
child: Text("2"),
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("3"),
|
||||
child: Text("3"),
|
||||
)
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
spacing: 5.0,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("4"),
|
||||
child: Text("4"),
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("5"),
|
||||
child: Text("5"),
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("6"),
|
||||
child: Text("6"),
|
||||
)
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
spacing: 5.0,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("7"),
|
||||
child: Text("7"),
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("8"),
|
||||
child: Text("8"),
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("9"),
|
||||
child: Text("9"),
|
||||
)
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
spacing: 5.0,
|
||||
alignment: WrapAlignment.end,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadHandler("0"),
|
||||
child: Text("0"),
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () => _pinPadClear(),
|
||||
child: Text("CLEAR"),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
Widget inputWrapper;
|
||||
if (entity.attributes["code_format"] == null) {
|
||||
inputWrapper = Container(width: 0.0, height: 0.0,);
|
||||
} else {
|
||||
inputWrapper = Container(
|
||||
width: 150.0,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Alarm Code"
|
||||
),
|
||||
//focusNode: _focusNode,
|
||||
obscureText: true,
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: code,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: code.length)
|
||||
)
|
||||
),
|
||||
onChanged: (value) {
|
||||
code = value;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
Widget buttonsWrapper = Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 15.0,
|
||||
runSpacing: Sizes.rowPadding,
|
||||
children: buttons
|
||||
)
|
||||
);
|
||||
Widget triggerButton = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FlatButton(
|
||||
child: Text(
|
||||
"TRIGGER",
|
||||
style: TextStyle(color: Colors.redAccent)
|
||||
),
|
||||
onPressed: () => _askToTrigger(entity),
|
||||
)
|
||||
]
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
widget.extended ? buttonsWrapper : inputWrapper,
|
||||
widget.extended ? inputWrapper : buttonsWrapper,
|
||||
widget.extended ? pinPad : triggerButton
|
||||
]
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -13,26 +13,28 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
bool _showPending = false;
|
||||
bool _changedHere = false;
|
||||
Timer _resetTimer;
|
||||
Timer _tempThrottleTimer;
|
||||
Timer _targetTempThrottleTimer;
|
||||
double _tmpTemperature = 0.0;
|
||||
double _tmpTargetLow = 0.0;
|
||||
double _tmpTargetHigh = 0.0;
|
||||
double _tmpTargetHumidity = 0.0;
|
||||
String _tmpOperationMode;
|
||||
String _tmpHVACMode;
|
||||
String _tmpFanMode;
|
||||
String _tmpSwingMode;
|
||||
bool _tmpAwayMode = false;
|
||||
bool _tmpIsOff = false;
|
||||
String _tmpPresetMode;
|
||||
//bool _tmpIsOff = false;
|
||||
bool _tmpAuxHeat = false;
|
||||
|
||||
void _resetVars(ClimateEntity entity) {
|
||||
_tmpTemperature = entity.temperature;
|
||||
_tmpTargetHigh = entity.targetHigh;
|
||||
_tmpTargetLow = entity.targetLow;
|
||||
_tmpOperationMode = entity.operationMode;
|
||||
_tmpHVACMode = entity.state;
|
||||
_tmpFanMode = entity.fanMode;
|
||||
_tmpSwingMode = entity.swingMode;
|
||||
_tmpAwayMode = entity.awayMode;
|
||||
_tmpIsOff = entity.isOff;
|
||||
_tmpPresetMode = entity.presetMode;
|
||||
//_tmpIsOff = entity.isOff;
|
||||
_tmpAuxHeat = entity.auxHeat;
|
||||
_tmpTargetHumidity = entity.targetHumidity;
|
||||
|
||||
@ -40,52 +42,68 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_changedHere = false;
|
||||
}
|
||||
|
||||
void _temperatureUp(ClimateEntity entity, double step) {
|
||||
_tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp;
|
||||
void _temperatureUp(ClimateEntity entity) {
|
||||
_tmpTemperature = ((_tmpTemperature + entity.temperatureStep) <= entity.maxTemp) ? _tmpTemperature + entity.temperatureStep : entity.maxTemp;
|
||||
_setTemperature(entity);
|
||||
}
|
||||
|
||||
void _temperatureDown(ClimateEntity entity, double step) {
|
||||
_tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp;
|
||||
void _temperatureDown(ClimateEntity entity) {
|
||||
_tmpTemperature = ((_tmpTemperature - entity.temperatureStep) >= entity.minTemp) ? _tmpTemperature - entity.temperatureStep : entity.minTemp;
|
||||
_setTemperature(entity);
|
||||
}
|
||||
|
||||
void _targetLowUp(ClimateEntity entity, double step) {
|
||||
_tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp;
|
||||
void _targetLowUp(ClimateEntity entity) {
|
||||
_tmpTargetLow = ((_tmpTargetLow + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetLow + entity.temperatureStep : entity.maxTemp;
|
||||
_setTargetTemp(entity);
|
||||
}
|
||||
|
||||
void _targetLowDown(ClimateEntity entity, double step) {
|
||||
_tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp;
|
||||
void _targetLowDown(ClimateEntity entity) {
|
||||
_tmpTargetLow = ((_tmpTargetLow - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetLow - entity.temperatureStep : entity.minTemp;
|
||||
_setTargetTemp(entity);
|
||||
}
|
||||
|
||||
void _targetHighUp(ClimateEntity entity, double step) {
|
||||
_tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp;
|
||||
void _targetHighUp(ClimateEntity entity) {
|
||||
_tmpTargetHigh = ((_tmpTargetHigh + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetHigh + entity.temperatureStep : entity.maxTemp;
|
||||
_setTargetTemp(entity);
|
||||
}
|
||||
|
||||
void _targetHighDown(ClimateEntity entity, double step) {
|
||||
_tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp;
|
||||
void _targetHighDown(ClimateEntity entity) {
|
||||
_tmpTargetHigh = ((_tmpTargetHigh - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetHigh - entity.temperatureStep : entity.minTemp;
|
||||
_setTargetTemp(entity);
|
||||
}
|
||||
|
||||
void _setTemperature(ClimateEntity entity) {
|
||||
if (_tempThrottleTimer!=null) {
|
||||
_tempThrottleTimer.cancel();
|
||||
}
|
||||
setState(() {
|
||||
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
|
||||
_resetStateTimer(entity);
|
||||
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
||||
});
|
||||
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _setTargetTemp(ClimateEntity entity) {
|
||||
if (_targetTempThrottleTimer!=null) {
|
||||
_targetTempThrottleTimer.cancel();
|
||||
}
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
||||
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -98,11 +116,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
void _setOperationMode(ClimateEntity entity, value) {
|
||||
void _setHVACMode(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpOperationMode = value;
|
||||
_tmpHVACMode = value;
|
||||
_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);
|
||||
});
|
||||
}
|
||||
@ -125,23 +143,23 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
void _setAwayMode(ClimateEntity entity, value) {
|
||||
void _setPresetMode(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpAwayMode = value;
|
||||
_tmpPresetMode = value;
|
||||
_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);
|
||||
});
|
||||
}
|
||||
|
||||
void _setOnOf(ClimateEntity entity, value) {
|
||||
/*void _setOnOf(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
_tmpIsOff = !value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
}*/
|
||||
|
||||
void _setAuxHeat(ClimateEntity entity, value) {
|
||||
setState(() {
|
||||
@ -167,7 +185,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
||||
if (_changedHere) {
|
||||
_showPending = (_tmpTemperature != entity.temperature);
|
||||
_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
|
||||
_changedHere = false;
|
||||
} else {
|
||||
_resetTimer?.cancel();
|
||||
@ -178,33 +196,34 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_buildOnOffControl(entity),
|
||||
//_buildOnOffControl(entity),
|
||||
_buildTemperatureControls(entity),
|
||||
_buildTargetTemperatureControls(entity),
|
||||
_buildHumidityControls(entity),
|
||||
_buildOperationControl(entity),
|
||||
_buildFanControl(entity),
|
||||
_buildSwingControl(entity),
|
||||
_buildAwayModeControl(entity),
|
||||
_buildPresetModeControl(entity),
|
||||
_buildAuxHeatControl(entity)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAwayModeControl(ClimateEntity entity) {
|
||||
if (entity.supportAwayMode) {
|
||||
return ModeSwitchWidget(
|
||||
caption: "Away mode",
|
||||
onChange: (value) => _setAwayMode(entity, value),
|
||||
value: _tmpAwayMode,
|
||||
Widget _buildPresetModeControl(ClimateEntity entity) {
|
||||
if (entity.supportPresetMode) {
|
||||
return ModeSelectorWidget(
|
||||
options: entity.presetModes,
|
||||
onChange: (mode) => _setPresetMode(entity, mode),
|
||||
caption: "Preset",
|
||||
value: _tmpPresetMode,
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOnOffControl(ClimateEntity entity) {
|
||||
/*Widget _buildOnOffControl(ClimateEntity entity) {
|
||||
if (entity.supportOnOff) {
|
||||
return ModeSwitchWidget(
|
||||
onChange: (value) => _setOnOf(entity, value),
|
||||
@ -214,7 +233,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
||||
if (entity.supportAuxHeat ) {
|
||||
@ -229,12 +248,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
|
||||
Widget _buildOperationControl(ClimateEntity entity) {
|
||||
if (entity.supportOperationMode) {
|
||||
if (entity.hvacModes != null) {
|
||||
return ModeSelectorWidget(
|
||||
onChange: (mode) => _setOperationMode(entity, mode),
|
||||
options: entity.operationList,
|
||||
onChange: (mode) => _setHVACMode(entity, mode),
|
||||
options: entity.hvacModes,
|
||||
caption: "Operation",
|
||||
value: _tmpOperationMode,
|
||||
value: _tmpHVACMode,
|
||||
);
|
||||
} else {
|
||||
return Container(height: 0.0, width: 0.0);
|
||||
@ -244,7 +263,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
Widget _buildFanControl(ClimateEntity entity) {
|
||||
if (entity.supportFanMode) {
|
||||
return ModeSelectorWidget(
|
||||
options: entity.fanList,
|
||||
options: entity.fanModes,
|
||||
onChange: (mode) => _setFanMode(entity, mode),
|
||||
caption: "Fan mode",
|
||||
value: _tmpFanMode,
|
||||
@ -258,7 +277,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
if (entity.supportSwingMode) {
|
||||
return ModeSelectorWidget(
|
||||
onChange: (mode) => _setSwingMode(entity, mode),
|
||||
options: entity.swingList,
|
||||
options: entity.swingModes,
|
||||
value: _tmpSwingMode,
|
||||
caption: "Swing mode"
|
||||
);
|
||||
@ -278,10 +297,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTemperature,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
onLargeDec: () => _temperatureDown(entity, 0.5),
|
||||
onLargeInc: () => _temperatureUp(entity, 0.5),
|
||||
onSmallDec: () => _temperatureDown(entity, 0.1),
|
||||
onSmallInc: () => _temperatureUp(entity, 0.1),
|
||||
onDec: () => _temperatureDown(entity),
|
||||
onInc: () => _temperatureUp(entity),
|
||||
)
|
||||
],
|
||||
);
|
||||
@ -292,30 +309,26 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
|
||||
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
||||
List<Widget> controls = [];
|
||||
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
|
||||
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
||||
controls.addAll(<Widget>[
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetLow,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
onLargeDec: () => _targetLowDown(entity, 0.5),
|
||||
onLargeInc: () => _targetLowUp(entity, 0.5),
|
||||
onSmallDec: () => _targetLowDown(entity, 0.1),
|
||||
onSmallInc: () => _targetLowUp(entity, 0.1),
|
||||
onDec: () => _targetLowDown(entity),
|
||||
onInc: () => _targetLowUp(entity),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(height: 10.0),
|
||||
)
|
||||
]);
|
||||
}
|
||||
if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) {
|
||||
if ((entity.supportTargetTemperatureRange) && (entity.targetHigh != null)) {
|
||||
controls.add(
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetHigh,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
onLargeDec: () => _targetHighDown(entity, 0.5),
|
||||
onLargeInc: () => _targetHighUp(entity, 0.5),
|
||||
onSmallDec: () => _targetHighDown(entity, 0.1),
|
||||
onSmallInc: () => _targetHighUp(entity, 0.1),
|
||||
onDec: () => _targetHighDown(entity),
|
||||
onInc: () => _targetHighUp(entity),
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -401,18 +414,14 @@ class TemperatureControlWidget extends StatelessWidget {
|
||||
final double value;
|
||||
final double fontSize;
|
||||
final Color fontColor;
|
||||
final onSmallInc;
|
||||
final onLargeInc;
|
||||
final onSmallDec;
|
||||
final onLargeDec;
|
||||
final onInc;
|
||||
final onDec;
|
||||
|
||||
TemperatureControlWidget(
|
||||
{Key key,
|
||||
@required this.value,
|
||||
@required this.onSmallInc,
|
||||
@required this.onSmallDec,
|
||||
@required this.onLargeInc,
|
||||
@required this.onLargeDec,
|
||||
@required this.onInc,
|
||||
@required this.onDec,
|
||||
this.fontSize,
|
||||
this.fontColor})
|
||||
: super(key: key);
|
||||
@ -432,32 +441,16 @@ class TemperatureControlWidget extends StatelessWidget {
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
'mdi:chevron-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => onSmallInc(),
|
||||
onPressed: () => onInc(),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
'mdi:chevron-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => onSmallDec(),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
||||
'mdi:chevron-double-up')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => onLargeInc(),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
||||
'mdi:chevron-double-down')),
|
||||
iconSize: 30.0,
|
||||
onPressed: () => onLargeDec(),
|
||||
onPressed: () => onDec(),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -157,7 +157,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
|
||||
if (entity.supportOpenTilt) {
|
||||
buttons.add(IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName(
|
||||
MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:arrow-top-right"),
|
||||
size: Sizes.iconSize,
|
||||
),
|
||||
@ -170,7 +170,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
|
||||
if (entity.supportStopTilt) {
|
||||
buttons.add(IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
|
||||
size: Sizes.iconSize,
|
||||
),
|
||||
onPressed: () => _stop(entity)));
|
||||
@ -182,7 +182,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
|
||||
if (entity.supportCloseTilt) {
|
||||
buttons.add(IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName(
|
||||
MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:arrow-bottom-left"),
|
||||
size: Sizes.iconSize,
|
||||
),
|
||||
|
@ -10,16 +10,18 @@ class LightControlsWidget extends StatefulWidget {
|
||||
class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
|
||||
int _tmpBrightness;
|
||||
int _tmpColorTemp;
|
||||
Color _tmpColor;
|
||||
int _tmpWhiteValue;
|
||||
int _tmpColorTemp = 0;
|
||||
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
|
||||
bool _changedHere = false;
|
||||
String _tmpEffect;
|
||||
|
||||
void _resetState(LightEntity entity) {
|
||||
_tmpBrightness = entity.brightness ?? 0;
|
||||
_tmpColorTemp = entity.colorTemp;
|
||||
_tmpColor = entity.color;
|
||||
_tmpEffect = null;
|
||||
_tmpWhiteValue = entity.whiteValue ?? 0;
|
||||
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
|
||||
_tmpColor = entity.color ?? _tmpColor;
|
||||
_tmpEffect = entity.effect;
|
||||
}
|
||||
|
||||
void _setBrightness(LightEntity entity, double value) {
|
||||
@ -38,6 +40,17 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
void _setWhiteValue(LightEntity entity, double value) {
|
||||
setState(() {
|
||||
_tmpWhiteValue = value.round();
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"white_value": _tmpWhiteValue}));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
void _setColorTemp(LightEntity entity, double value) {
|
||||
setState(() {
|
||||
_tmpColorTemp = value.round();
|
||||
@ -48,20 +61,14 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
void _setColor(LightEntity entity, Color color) {
|
||||
void _setColor(LightEntity entity, HSVColor color) {
|
||||
setState(() {
|
||||
_tmpColor = color;
|
||||
_changedHere = true;
|
||||
TheLogger.debug( "Color: [${color.red}, ${color.green}, ${color.blue}]");
|
||||
if ((color == Colors.black) || ((color.red == color.green) && (color.green == color.blue))) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_off", entity.entityId,
|
||||
null));
|
||||
} else {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"rgb_color": [color.red, color.green, color.blue]}));
|
||||
}
|
||||
Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"hs_color": [color.hue, color.saturation*100]}));
|
||||
});
|
||||
}
|
||||
|
||||
@ -90,6 +97,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
_buildBrightnessControl(entity),
|
||||
_buildWhiteValueControl(entity),
|
||||
_buildColorTempControl(entity),
|
||||
_buildColorControl(entity),
|
||||
_buildEffectControl(entity)
|
||||
@ -98,7 +106,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
}
|
||||
|
||||
Widget _buildBrightnessControl(LightEntity entity) {
|
||||
if ((entity.supportBrightness) && (_tmpBrightness != null) && (entity.state != EntityState.unavailable)) {
|
||||
if ((entity.supportBrightness) && (_tmpBrightness != null)) {
|
||||
return UniversalSlider(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -109,7 +117,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
min: 0.0,
|
||||
max: 255.0,
|
||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||
value: _tmpBrightness.toDouble(),
|
||||
value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(),
|
||||
leading: Icon(Icons.brightness_5),
|
||||
title: "Brightness",
|
||||
);
|
||||
@ -118,12 +126,33 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildWhiteValueControl(LightEntity entity) {
|
||||
if ((entity.supportWhiteValue) && (_tmpWhiteValue != null)) {
|
||||
return UniversalSlider(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_tmpWhiteValue = value.round();
|
||||
});
|
||||
},
|
||||
min: 0.0,
|
||||
max: 255.0,
|
||||
onChangeEnd: (value) => _setWhiteValue(entity, value),
|
||||
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
|
||||
title: "White",
|
||||
);
|
||||
} else {
|
||||
return Container(width: 0.0, height: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildColorTempControl(LightEntity entity) {
|
||||
if ((entity.supportColorTemp) && (_tmpColorTemp != null)) {
|
||||
if (entity.supportColorTemp) {
|
||||
return UniversalSlider(
|
||||
title: "Color temperature",
|
||||
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
|
||||
value: _tmpColorTemp.toDouble(),
|
||||
value: _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(),
|
||||
onChangeEnd: (value) => _setColorTemp(entity, value),
|
||||
max: entity.maxMireds,
|
||||
min: entity.minMireds,
|
||||
@ -141,25 +170,36 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
}
|
||||
|
||||
Widget _buildColorControl(LightEntity entity) {
|
||||
if ((entity.supportColor) && (entity.color != null)) {
|
||||
if (entity.supportColor) {
|
||||
HSVColor savedColor = HomeAssistant().savedColor;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
RaisedButton(
|
||||
onPressed: () => _showColorPicker(entity),
|
||||
color: _tmpColor ?? Colors.black45,
|
||||
child: Text(
|
||||
"COLOR",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 50.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black12,
|
||||
),
|
||||
),
|
||||
LightColorPicker(
|
||||
color: _tmpColor,
|
||||
onColorSelected: (color) => _setColor(entity, color),
|
||||
),
|
||||
Container(height: 2*Sizes.rowPadding,),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
color: _tmpColor.toColor(),
|
||||
child: Text('Copy color'),
|
||||
onPressed: _tmpColor == null ? null : () {
|
||||
setState(() {
|
||||
HomeAssistant().savedColor = _tmpColor;
|
||||
});
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
color: savedColor?.toColor() ?? Colors.transparent,
|
||||
child: Text('Paste color'),
|
||||
onPressed: savedColor == null ? null : () {
|
||||
_setColor(entity, savedColor);
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
@ -167,28 +207,6 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showColorPicker(LightEntity entity) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
titlePadding: EdgeInsets.all(0.0),
|
||||
contentPadding: EdgeInsets.all(0.0),
|
||||
content: SingleChildScrollView(
|
||||
child: MaterialPicker(
|
||||
pickerColor: _tmpColor,
|
||||
onColorChanged: (color) {
|
||||
_setColor(entity, color);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
enableLabel: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEffectControl(LightEntity entity) {
|
||||
if ((entity.supportEffect) && (entity.effectList != null)) {
|
||||
return ModeSelectorWidget(
|
||||
|
@ -73,7 +73,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildImage(MediaPlayerEntity entity) {
|
||||
String state = entity.state;
|
||||
if (homeAssistantWebHost != null && entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Row(
|
||||
@ -81,7 +81,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Image(
|
||||
image: CachedNetworkImageProvider("$homeAssistantWebHost${entity.entityPicture}"),
|
||||
image: CachedNetworkImageProvider("${entity.entityPicture}"),
|
||||
height: 240.0,
|
||||
//width: 320.0,
|
||||
fit: BoxFit.contain,
|
||||
@ -95,7 +95,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:movie"),
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
||||
size: 150.0,
|
||||
color: EntityColor.stateColor("$state"),
|
||||
)
|
||||
@ -120,12 +120,12 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
||||
void _setPower(MediaPlayerEntity entity) {
|
||||
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
|
||||
if (entity.state == EntityState.off) {
|
||||
TheLogger.debug("${entity.entityId} turn_on");
|
||||
Logger.d("${entity.entityId} turn_on");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
null));
|
||||
} else {
|
||||
TheLogger.debug("${entity.entityId} turn_off");
|
||||
Logger.d("${entity.entityId} turn_off");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_off", entity.entityId,
|
||||
null));
|
||||
@ -134,7 +134,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _callAction(MediaPlayerEntity entity, String action) {
|
||||
TheLogger.debug("${entity.entityId} $action");
|
||||
Logger.d("${entity.entityId} $action");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "$action", entity.entityId,
|
||||
null));
|
||||
@ -227,7 +227,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
||||
if (showMenu) {
|
||||
result.add(
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical")),
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
|
||||
)
|
||||
@ -307,11 +307,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
||||
Widget muteWidget;
|
||||
Widget volumeStepWidget;
|
||||
if (entity.supportVolumeMute) {
|
||||
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
|
||||
bool isMuted = entity.attributes["is_volume_muted"] ?? false;
|
||||
muteWidget =
|
||||
IconButton(
|
||||
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up),
|
||||
icon: Icon(isMuted ? Icons.volume_up : Icons.volume_off),
|
||||
onPressed: () => _setVolumeMute(!isMuted, entity.entityId)
|
||||
);
|
||||
} else {
|
||||
@ -322,11 +322,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:plus")),
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||
onPressed: () => _setVolumeUp(entity.entityId)
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:minus")),
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||
onPressed: () => _setVolumeDown(entity.entityId)
|
||||
)
|
||||
],
|
||||
|
@ -11,6 +11,29 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
||||
return Divider(
|
||||
color: Colors.black45,
|
||||
);
|
||||
}
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Divider(
|
||||
color: Colors.black45,
|
||||
),
|
||||
Text(
|
||||
"${entityModel.entityWrapper.entity.displayName}",
|
||||
style: TextStyle(color: Colors.blue),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onLongPress: () {
|
||||
if (entityModel.handleTap) {
|
||||
@ -30,7 +53,9 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 3,
|
||||
child: EntityName(),
|
||||
child: EntityName(
|
||||
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
||||
),
|
||||
),
|
||||
state
|
||||
],
|
||||
|
@ -2,6 +2,8 @@ part of '../main.dart';
|
||||
|
||||
class EntityColor {
|
||||
|
||||
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
|
||||
|
||||
static const badgeColors = {
|
||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||
@ -10,19 +12,29 @@ class EntityColor {
|
||||
static const _stateColors = {
|
||||
EntityState.on: Colors.amber,
|
||||
"auto": Colors.amber,
|
||||
EntityState.idle: Colors.amber,
|
||||
EntityState.active: Colors.amber,
|
||||
EntityState.playing: Colors.amber,
|
||||
"above_horizon": Colors.amber,
|
||||
EntityState.home: Colors.amber,
|
||||
EntityState.open: Colors.amber,
|
||||
EntityState.off: Color.fromRGBO(68, 115, 158, 1.0),
|
||||
EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0),
|
||||
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0),
|
||||
"default": Color.fromRGBO(68, 115, 158, 1.0),
|
||||
EntityState.off: defaultStateColor,
|
||||
EntityState.closed: defaultStateColor,
|
||||
"below_horizon": defaultStateColor,
|
||||
"default": defaultStateColor,
|
||||
EntityState.idle: defaultStateColor,
|
||||
"heat": Colors.redAccent,
|
||||
"cool": Colors.lightBlue,
|
||||
EntityState.unavailable: Colors.black26,
|
||||
EntityState.unknown: Colors.black26,
|
||||
EntityState.alarm_disarmed: Colors.green,
|
||||
EntityState.alarm_armed_away: Colors.redAccent,
|
||||
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
|
||||
EntityState.alarm_armed_home: Colors.redAccent,
|
||||
EntityState.alarm_armed_night: Colors.redAccent,
|
||||
EntityState.alarm_triggered: Colors.redAccent,
|
||||
EntityState.alarm_arming: Colors.amber,
|
||||
EntityState.alarm_disarming: Colors.amber,
|
||||
EntityState.alarm_pending: Colors.amber,
|
||||
};
|
||||
|
||||
static Color stateColor(String state) {
|
||||
|
@ -3,20 +3,71 @@ part of '../main.dart';
|
||||
class EntityIcon extends StatelessWidget {
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
final double iconSize;
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB(
|
||||
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key);
|
||||
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
|
||||
|
||||
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||
String domain = entityId.split(".")[0];
|
||||
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
|
||||
String iconNameByDeviceClass;
|
||||
if (deviceClass != null) {
|
||||
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
|
||||
}
|
||||
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
|
||||
if (iconName != null) {
|
||||
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildIcon(EntityWrapper data, Color color) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
if (data.entityPicture != null) {
|
||||
return Container(
|
||||
height: size+12,
|
||||
width: size+12,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit:BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
"${data.entityPicture}"
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
String iconName = data.icon;
|
||||
int iconCode = 0;
|
||||
if (iconName.length > 0) {
|
||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||
} else {
|
||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||
data.entity.deviceClass, data.entity.state); //
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
|
||||
child: Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: color,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||
child: buildIcon(
|
||||
entityWrapper,
|
||||
iconSize,
|
||||
EntityColor.stateColor(entityWrapper.entity.state)
|
||||
color ?? EntityColor.stateColor(entityWrapper.entity.state)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ class EntityName extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
TextStyle textStyle = TextStyle(fontSize: fontSize);
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
|
||||
}
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Text(
|
||||
@ -21,7 +25,7 @@ class EntityName extends StatelessWidget {
|
||||
overflow: textOverflow,
|
||||
softWrap: wordsWrap,
|
||||
maxLines: maxLines,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
style: textStyle,
|
||||
textAlign: textAlign,
|
||||
),
|
||||
);
|
||||
|
@ -22,6 +22,12 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
List<Widget> result = [];
|
||||
if (!nameInTheBottom) {
|
||||
if (showName) {
|
||||
@ -35,7 +41,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
result.add(
|
||||
EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
iconSize: iconSize,
|
||||
size: iconSize,
|
||||
)
|
||||
);
|
||||
if (!nameInTheBottom) {
|
||||
|
@ -94,11 +94,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
}
|
||||
|
||||
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
|
||||
TheLogger.debug(" parsing history...");
|
||||
Logger.d(" parsing history...");
|
||||
Map<String, List<EntityHistoryMoment>> numericDataLists = {};
|
||||
int colorIdCounter = 0;
|
||||
widget.config.numericAttributesToShow.forEach((String attrName) {
|
||||
TheLogger.debug(" parsing attribute $attrName");
|
||||
Logger.d(" parsing attribute $attrName");
|
||||
List<EntityHistoryMoment> data = [];
|
||||
DateTime now = DateTime.now();
|
||||
for (var i = 0; i < widget.rawHistory.length; i++) {
|
||||
@ -152,7 +152,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
}
|
||||
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
||||
numericDataLists.forEach((attrName, dataList) {
|
||||
TheLogger.debug(" adding ${dataList.length} data values");
|
||||
Logger.d(" adding ${dataList.length} data values");
|
||||
result.add(
|
||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||
id: "value",
|
||||
|
@ -32,6 +32,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
List _history;
|
||||
bool _needToUpdateHistory;
|
||||
DateTime _historyLastUpdated;
|
||||
bool _disposed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -39,37 +40,40 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
_needToUpdateHistory = true;
|
||||
}
|
||||
|
||||
void _loadHistory(HomeAssistant ha, String entityId) {
|
||||
void _loadHistory(String entityId) {
|
||||
DateTime now = DateTime.now();
|
||||
if (_historyLastUpdated != null) {
|
||||
TheLogger.debug("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) {
|
||||
_historyLastUpdated = now;
|
||||
ha.getHistory(entityId).then((history){
|
||||
setState(() {
|
||||
_history = history.isNotEmpty ? history[0] : [];
|
||||
_needToUpdateHistory = false;
|
||||
});
|
||||
Connection().getHistory(entityId).then((history){
|
||||
if (!_disposed) {
|
||||
setState(() {
|
||||
_history = history.isNotEmpty ? history[0] : [];
|
||||
_needToUpdateHistory = false;
|
||||
});
|
||||
}
|
||||
}).catchError((e) {
|
||||
TheLogger.error("Error loading $entityId history: $e");
|
||||
setState(() {
|
||||
_history = [];
|
||||
_needToUpdateHistory = false;
|
||||
});
|
||||
Logger.e("Error loading $entityId history: $e");
|
||||
if (!_disposed) {
|
||||
setState(() {
|
||||
_history = [];
|
||||
_needToUpdateHistory = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
final Entity entity = entityModel.entityWrapper.entity;
|
||||
if (!_needToUpdateHistory) {
|
||||
_needToUpdateHistory = true;
|
||||
} else {
|
||||
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
|
||||
_loadHistory(entity.entityId);
|
||||
}
|
||||
return _buildChart();
|
||||
}
|
||||
@ -122,7 +126,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
}
|
||||
|
||||
default: {
|
||||
TheLogger.debug(" Simple selected as default");
|
||||
Logger.d(" Simple selected as default");
|
||||
return SimpleStateHistoryChartWidget(
|
||||
rawHistory: _history,
|
||||
);
|
||||
@ -131,4 +135,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
19
lib/entity_widgets/missed_entity.dart
Normal file
@ -0,0 +1,19 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MissedEntityWidget extends StatelessWidget {
|
||||
MissedEntityWidget({
|
||||
Key key
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
return Container(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5.0),
|
||||
child: Text("Entity not available: ${entityModel.entityWrapper.entity.entityId}"),
|
||||
),
|
||||
color: Colors.amber[100],
|
||||
);
|
||||
}
|
||||
}
|
@ -15,26 +15,6 @@ class EntityModel extends InheritedWidget {
|
||||
return context.inheritFromWidgetOfExactType(EntityModel);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||
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;
|
||||
|
@ -1,27 +0,0 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class ButtonStateWidget extends StatelessWidget {
|
||||
|
||||
void _setNewState(Entity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "turn_on", entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return SizedBox(
|
||||
height: 34.0,
|
||||
child: FlatButton(
|
||||
onPressed: (() {
|
||||
_setNewState(entityModel.entityWrapper.entity);
|
||||
}),
|
||||
child: Text(
|
||||
"EXECUTE",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -8,13 +8,19 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
String targetTemp = "-";
|
||||
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||
targetTemp = "${entity.temperature}";
|
||||
} else if ((entity.supportTargetTemperatureLow) &&
|
||||
(entity.targetLow != null)) {
|
||||
targetTemp = "${entity.targetLow}";
|
||||
if ((entity.supportTargetTemperatureHigh) &&
|
||||
(entity.targetHigh != null)) {
|
||||
targetTemp += " - ${entity.targetHigh}";
|
||||
}
|
||||
} else if ((entity.supportTargetTemperatureRange) &&
|
||||
(entity.targetLow != null) &&
|
||||
(entity.targetHigh != null)) {
|
||||
targetTemp = "${entity.targetLow} - ${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(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
@ -25,7 +31,7 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("${entity.state}",
|
||||
Text("$displayState",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -38,8 +44,8 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
))
|
||||
],
|
||||
),
|
||||
entity.attributes["current_temperature"] != null ?
|
||||
Text("Currently: ${entity.attributes["current_temperature"]}",
|
||||
entity.currentTemperature != null ?
|
||||
Text("Currently: ${entity.currentTemperature}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
|
@ -24,7 +24,7 @@ class CoverStateWidget extends StatelessWidget {
|
||||
if (entity.supportOpen) {
|
||||
buttons.add(IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"),
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-up"),
|
||||
size: Sizes.iconSize,
|
||||
),
|
||||
onPressed: entity.canBeOpened ? () => _open(entity) : null));
|
||||
@ -36,7 +36,7 @@ class CoverStateWidget extends StatelessWidget {
|
||||
if (entity.supportStop) {
|
||||
buttons.add(IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
|
||||
size: Sizes.iconSize,
|
||||
),
|
||||
onPressed: () => _stop(entity)));
|
||||
@ -48,7 +48,7 @@ class CoverStateWidget extends StatelessWidget {
|
||||
if (entity.supportClose) {
|
||||
buttons.add(IconButton(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"),
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-down"),
|
||||
size: Sizes.iconSize,
|
||||
),
|
||||
onPressed: entity.canBeClosed ? () => _close(entity) : null));
|
||||
|
@ -54,7 +54,7 @@ class DateTimeStateWidget extends StatelessWidget {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
TheLogger.warning( "${entity.entityId} has no date and no time");
|
||||
Logger.w( "${entity.entityId} has no date and no time");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,10 @@ part of '../../main.dart';
|
||||
|
||||
class LockStateWidget extends StatelessWidget {
|
||||
|
||||
final bool assumedState;
|
||||
|
||||
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
|
||||
|
||||
void _lock(Entity entity) {
|
||||
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
|
||||
}
|
||||
@ -14,19 +18,49 @@ class LockStateWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final LockEntity entity = entityModel.entityWrapper.entity;
|
||||
return SizedBox(
|
||||
height: 34.0,
|
||||
child: FlatButton(
|
||||
onPressed: (() {
|
||||
entity.isLocked ? _unlock(entity) : _lock(entity);
|
||||
}),
|
||||
child: Text(
|
||||
entity.isLocked ? "UNLOCK" : "LOCK",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
if (assumedState) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 34.0,
|
||||
child: FlatButton(
|
||||
onPressed: () => _unlock(entity),
|
||||
child: Text("UNLOCK",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
SizedBox(
|
||||
height: 34.0,
|
||||
child: FlatButton(
|
||||
onPressed: () => _lock(entity),
|
||||
child: Text("LOCK",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: 34.0,
|
||||
child: FlatButton(
|
||||
onPressed: (() {
|
||||
entity.isLocked ? _unlock(entity) : _lock(entity);
|
||||
}),
|
||||
child: Text(
|
||||
entity.isLocked ? "UNLOCK" : "LOCK",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,23 +6,38 @@ class SimpleEntityState extends StatelessWidget {
|
||||
final TextAlign textAlign;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final int maxLines;
|
||||
final String customValue;
|
||||
|
||||
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
String state;
|
||||
if (customValue == null) {
|
||||
state = entityModel.entityWrapper.entity.displayState ?? "";
|
||||
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
|
||||
} else {
|
||||
state = customValue;
|
||||
}
|
||||
TextStyle textStyle = TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
);
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||
textStyle = textStyle.apply(color: Colors.blue);
|
||||
}
|
||||
while (state.contains(" ")){
|
||||
state = state.replaceAll(" ", " ");
|
||||
}
|
||||
Widget result = Padding(
|
||||
padding: padding,
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.state} ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
||||
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
)
|
||||
style: textStyle
|
||||
)
|
||||
);
|
||||
if (expanded) {
|
||||
|
@ -71,13 +71,13 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
onPressed: () => _setNewState(false, entity),
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")),
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash-off")),
|
||||
color: newState == EntityState.on ? Colors.black : Colors.blue,
|
||||
iconSize: Sizes.iconSize,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _setNewState(true, entity),
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")),
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash")),
|
||||
color: newState == EntityState.on ? Colors.blue : Colors.black,
|
||||
iconSize: Sizes.iconSize
|
||||
)
|
||||
|
@ -85,7 +85,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
TheLogger.warning( "Unsupported input mode for ${entity.entityId}");
|
||||
Logger.w( "Unsupported input mode for ${entity.entityId}");
|
||||
return SimpleEntityState();
|
||||
}
|
||||
}
|
||||
|
65
lib/entity_widgets/state/timer_state.dart
Normal file
@ -0,0 +1,65 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class TimerState extends StatefulWidget {
|
||||
//final bool expanded;
|
||||
//final TextAlign textAlign;
|
||||
//final EdgeInsetsGeometry padding;
|
||||
//final int maxLines;
|
||||
|
||||
const TimerState({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_TimerStateState createState() => _TimerStateState();
|
||||
|
||||
}
|
||||
|
||||
class _TimerStateState extends State<TimerState> {
|
||||
|
||||
Timer timer;
|
||||
Duration remaining = Duration(seconds: 0);
|
||||
|
||||
void checkState(TimerEntity entity) {
|
||||
if (entity.state == EntityState.active) {
|
||||
//Logger.d("Timer is active");
|
||||
if (timer == null || !timer.isActive) {
|
||||
timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
setState(() {
|
||||
try {
|
||||
int passed = DateTime
|
||||
.now()
|
||||
.difference(entity._lastUpdated)
|
||||
.inSeconds;
|
||||
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
||||
} catch (e) {
|
||||
Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}");
|
||||
remaining = Duration(seconds: 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityModel model = EntityModel.of(context);
|
||||
TimerEntity entity = model.entityWrapper.entity;
|
||||
checkState(entity);
|
||||
if (entity.state != EntityState.active) {
|
||||
return SimpleEntityState();
|
||||
} else {
|
||||
return SimpleEntityState(
|
||||
customValue: "${remaining.toString().split('.')[0]}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
@ -1,47 +1,29 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class HomeAssistant {
|
||||
String _webSocketAPIEndpoint;
|
||||
String _password;
|
||||
String _authType;
|
||||
bool _useLovelace = false;
|
||||
|
||||
IOWebSocketChannel _hassioChannel;
|
||||
SendMessageQueue _messageQueue;
|
||||
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||
|
||||
factory HomeAssistant() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
int _currentMessageId = 0;
|
||||
int _statesMessageId = 0;
|
||||
int _servicesMessageId = 0;
|
||||
int _subscriptionMessageId = 0;
|
||||
int _configMessageId = 0;
|
||||
int _userInfoMessageId = 0;
|
||||
int _lovelaceMessageId = 0;
|
||||
EntityCollection entities;
|
||||
HomeAssistantUI ui;
|
||||
Map _instanceConfig = {};
|
||||
String _userName;
|
||||
HSVColor savedColor;
|
||||
|
||||
String fcmToken;
|
||||
|
||||
Map _rawLovelaceData;
|
||||
|
||||
Completer _fetchCompleter;
|
||||
Completer _statesCompleter;
|
||||
Completer _servicesCompleter;
|
||||
Completer _lovelaceCompleter;
|
||||
Completer _configCompleter;
|
||||
Completer _connectionCompleter;
|
||||
Completer _userInfoCompleter;
|
||||
Timer _connectionTimer;
|
||||
Timer _fetchTimer;
|
||||
bool autoReconnect = false;
|
||||
List<Panel> panels = [];
|
||||
|
||||
StreamSubscription _socketSubscription;
|
||||
|
||||
int messageExpirationTime = 30; //seconds
|
||||
Duration fetchTimeout = Duration(seconds: 30);
|
||||
Duration connectTimeout = Duration(seconds: 15);
|
||||
|
||||
String get locationName {
|
||||
if (_useLovelace) {
|
||||
if (Connection().useLovelace) {
|
||||
return ui?.title ?? "";
|
||||
} else {
|
||||
return _instanceConfig["location_name"] ?? "";
|
||||
@ -49,374 +31,259 @@ class HomeAssistant {
|
||||
}
|
||||
String get userName => _userName ?? locationName;
|
||||
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||
//int get viewsCount => entities.views.length ?? 0;
|
||||
bool get isNoEntities => entities == null || entities.isEmpty;
|
||||
bool get isNoViews => ui == null || ui.isEmpty;
|
||||
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
||||
|
||||
HomeAssistant() {
|
||||
entities = EntityCollection();
|
||||
_messageQueue = SendMessageQueue(messageExpirationTime);
|
||||
HomeAssistant._internal() {
|
||||
Connection().onStateChangeCallback = _handleEntityStateChange;
|
||||
Device().loadDeviceInfo();
|
||||
}
|
||||
|
||||
void updateSettings(String url, String password, String authType, bool useLovelace) {
|
||||
_webSocketAPIEndpoint = url;
|
||||
_password = password;
|
||||
_authType = authType;
|
||||
_useLovelace = useLovelace;
|
||||
TheLogger.debug( "Use lovelace is $_useLovelace");
|
||||
}
|
||||
Completer _fetchCompleter;
|
||||
|
||||
Future fetch() {
|
||||
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
|
||||
TheLogger.warning("Previous fetch is not complited");
|
||||
} else {
|
||||
_fetchCompleter = new Completer();
|
||||
_fetchTimer = Timer(fetchTimeout, () {
|
||||
TheLogger.error( "Data fetching timeout");
|
||||
disconnect().then((_) {
|
||||
_completeFetching({
|
||||
"errorCode": 9,
|
||||
"errorMessage": "Couldn't get data from server"
|
||||
});
|
||||
});
|
||||
});
|
||||
_connection().then((r) {
|
||||
_getData();
|
||||
}).catchError((e) {
|
||||
_completeFetching(e);
|
||||
});
|
||||
Future fetchData() {
|
||||
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||
Logger.w("Previous data fetch is not completed yet");
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
|
||||
disconnect() async {
|
||||
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
|
||||
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
|
||||
onTimeout: () => TheLogger.debug( "Socket sink closed")
|
||||
);
|
||||
await _socketSubscription.cancel();
|
||||
_hassioChannel = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Future _connection() {
|
||||
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
|
||||
TheLogger.debug("Previous connection is not complited");
|
||||
} else {
|
||||
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
|
||||
_connectionCompleter = new Completer();
|
||||
autoReconnect = false;
|
||||
disconnect().then((_){
|
||||
TheLogger.debug( "Socket connecting...");
|
||||
_connectionTimer = Timer(connectTimeout, () {
|
||||
TheLogger.error( "Socket connection timeout");
|
||||
_handleSocketError(null);
|
||||
});
|
||||
if (_socketSubscription != null) {
|
||||
_socketSubscription.cancel();
|
||||
}
|
||||
_hassioChannel = IOWebSocketChannel.connect(
|
||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
|
||||
_socketSubscription = _hassioChannel.stream.listen(
|
||||
(message) => _handleMessage(message),
|
||||
cancelOnError: true,
|
||||
onDone: () => _handleSocketClose(),
|
||||
onError: (e) => _handleSocketError(e)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
_completeConnecting(null);
|
||||
}
|
||||
}
|
||||
return _connectionCompleter.future;
|
||||
}
|
||||
|
||||
void _handleSocketClose() {
|
||||
TheLogger.debug("Socket disconnected. Automatic reconnect is $autoReconnect");
|
||||
if (autoReconnect) {
|
||||
_reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSocketError(e) {
|
||||
TheLogger.error("Socket stream Error: $e");
|
||||
TheLogger.debug("Automatic reconnect is $autoReconnect");
|
||||
if (autoReconnect) {
|
||||
_reconnect();
|
||||
} else {
|
||||
disconnect().then((_) {
|
||||
_completeConnecting({
|
||||
"errorCode": 1,
|
||||
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _reconnect() {
|
||||
disconnect().then((_) {
|
||||
_connection().catchError((e){
|
||||
_completeConnecting(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getData() async {
|
||||
if (entities == null) entities = EntityCollection(Connection().httpWebHost);
|
||||
_fetchCompleter = Completer();
|
||||
List<Future> futures = [];
|
||||
futures.add(_getStates());
|
||||
if (_useLovelace) {
|
||||
if (Connection().useLovelace) {
|
||||
futures.add(_getLovelace());
|
||||
}
|
||||
futures.add(_getConfig());
|
||||
futures.add(_getServices());
|
||||
futures.add(_getUserInfo());
|
||||
try {
|
||||
await Future.wait(futures);
|
||||
_createUI();
|
||||
_completeFetching(null);
|
||||
} catch (error) {
|
||||
_completeFetching(error);
|
||||
}
|
||||
}
|
||||
|
||||
void _completeFetching(error) {
|
||||
_fetchTimer.cancel();
|
||||
_completeConnecting(error);
|
||||
if (!_fetchCompleter.isCompleted) {
|
||||
if (error != null) {
|
||||
_fetchCompleter.completeError(error);
|
||||
} else {
|
||||
autoReconnect = true;
|
||||
TheLogger.debug( "Fetch complete successful");
|
||||
futures.add(_getPanels());
|
||||
futures.add(Connection().sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "state_changed"},
|
||||
));
|
||||
Future.wait(futures).then((_) {
|
||||
if (isMobileAppEnabled) {
|
||||
_createUI();
|
||||
_fetchCompleter.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _completeConnecting(error) {
|
||||
_connectionTimer.cancel();
|
||||
if (!_connectionCompleter.isCompleted) {
|
||||
if (error != null) {
|
||||
_connectionCompleter.completeError(error);
|
||||
checkAppRegistration();
|
||||
} else {
|
||||
_connectionCompleter.complete();
|
||||
_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")]));
|
||||
}
|
||||
} else if (error != null) {
|
||||
if (error is Error) {
|
||||
eventBus.fire(ShowErrorEvent(error.toString(), 12));
|
||||
} else {
|
||||
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_handleMessage(String message) {
|
||||
var data = json.decode(message);
|
||||
if (data["type"] == "auth_required") {
|
||||
_sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}');
|
||||
} else if (data["type"] == "auth_ok") {
|
||||
_completeConnecting(null);
|
||||
_sendSubscribe();
|
||||
} else if (data["type"] == "auth_invalid") {
|
||||
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
|
||||
} else if (data["type"] == "result") {
|
||||
TheLogger.debug("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
|
||||
if (data["id"] == _configMessageId) {
|
||||
_parseConfig(data);
|
||||
} else if (data["id"] == _statesMessageId) {
|
||||
_parseEntities(data);
|
||||
} else if (data["id"] == _lovelaceMessageId) {
|
||||
_handleLovelace(data);
|
||||
} else if (data["id"] == _servicesMessageId) {
|
||||
_parseServices(data);
|
||||
} else if (data["id"] == _userInfoMessageId) {
|
||||
_parseUserInfo(data);
|
||||
}
|
||||
} else if (data["type"] == "event") {
|
||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||
TheLogger.debug("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||
_handleEntityStateChange(data["event"]["data"]);
|
||||
} else if (data["event"] != null) {
|
||||
TheLogger.warning("Unhandled event type: ${data["event"]["event_type"]}");
|
||||
} else {
|
||||
TheLogger.error("Event is null: $message");
|
||||
}
|
||||
} else {
|
||||
TheLogger.warning("Unknown message type: $message");
|
||||
}
|
||||
}
|
||||
|
||||
void _sendSubscribe() {
|
||||
_incrementMessageId();
|
||||
_subscriptionMessageId = _currentMessageId;
|
||||
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
|
||||
}
|
||||
|
||||
Future _getConfig() {
|
||||
_configCompleter = new Completer();
|
||||
_incrementMessageId();
|
||||
_configMessageId = _currentMessageId;
|
||||
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
|
||||
|
||||
return _configCompleter.future;
|
||||
}
|
||||
|
||||
Future _getStates() {
|
||||
_statesCompleter = new Completer();
|
||||
_incrementMessageId();
|
||||
_statesMessageId = _currentMessageId;
|
||||
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
|
||||
|
||||
return _statesCompleter.future;
|
||||
}
|
||||
|
||||
Future _getLovelace() {
|
||||
_lovelaceCompleter = new Completer();
|
||||
_incrementMessageId();
|
||||
_lovelaceMessageId = _currentMessageId;
|
||||
_sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false);
|
||||
|
||||
return _lovelaceCompleter.future;
|
||||
}
|
||||
|
||||
Future _getUserInfo() {
|
||||
_userInfoCompleter = new Completer();
|
||||
_incrementMessageId();
|
||||
_userInfoMessageId = _currentMessageId;
|
||||
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
|
||||
|
||||
return _userInfoCompleter.future;
|
||||
}
|
||||
|
||||
Future _getServices() {
|
||||
_servicesCompleter = new Completer();
|
||||
_incrementMessageId();
|
||||
_servicesMessageId = _currentMessageId;
|
||||
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
|
||||
|
||||
return _servicesCompleter.future;
|
||||
}
|
||||
|
||||
_incrementMessageId() {
|
||||
_currentMessageId += 1;
|
||||
}
|
||||
|
||||
void _sendAuthMessageRaw(String message) {
|
||||
TheLogger.debug( "[Sending] ==> auth request");
|
||||
_hassioChannel.sink.add(message);
|
||||
}
|
||||
|
||||
_sendMessageRaw(String message, bool queued) {
|
||||
var sendCompleter = Completer();
|
||||
if (queued) _messageQueue.add(message);
|
||||
_connection().then((r) {
|
||||
_messageQueue.getActualMessages().forEach((message){
|
||||
TheLogger.debug( "[Sending queued] ==> $message");
|
||||
_hassioChannel.sink.add(message);
|
||||
});
|
||||
if (!queued) {
|
||||
TheLogger.debug( "[Sending] ==> $message");
|
||||
_hassioChannel.sink.add(message);
|
||||
}
|
||||
sendCompleter.complete();
|
||||
}).catchError((e){
|
||||
sendCompleter.completeError(e);
|
||||
}).catchError((e) {
|
||||
_fetchCompleter.completeError(e);
|
||||
});
|
||||
return sendCompleter.future;
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
|
||||
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
|
||||
_incrementMessageId();
|
||||
String message = "";
|
||||
if (entityId != null) {
|
||||
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
|
||||
if (additionalParams != null) {
|
||||
additionalParams.forEach((name, value) {
|
||||
if ((value is double) || (value is int) || (value is List)) {
|
||||
message += ', "$name" : $value';
|
||||
} else {
|
||||
message += ', "$name" : "$value"';
|
||||
}
|
||||
});
|
||||
}
|
||||
message += '}}';
|
||||
} else {
|
||||
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"';
|
||||
if (additionalParams != null && additionalParams.isNotEmpty) {
|
||||
message += ', "service_data": {';
|
||||
bool first = true;
|
||||
additionalParams.forEach((name, value) {
|
||||
if (!first) {
|
||||
message += ', ';
|
||||
}
|
||||
if ((value is double) || (value is int) || (value is List)) {
|
||||
message += '"$name" : $value';
|
||||
} else {
|
||||
message += '"$name" : "$value"';
|
||||
}
|
||||
first = false;
|
||||
});
|
||||
Future logout() async {
|
||||
Logger.d("Logging out...");
|
||||
await Connection().logout().then((_) {
|
||||
ui?.clear();
|
||||
entities?.clear();
|
||||
panels?.clear();
|
||||
});
|
||||
}
|
||||
|
||||
message += '}';
|
||||
Map _getAppRegistrationData() {
|
||||
return {
|
||||
"app_version": "$appVersion",
|
||||
"device_name": "$userName's ${Device().model}",
|
||||
"manufacturer": Device().manufacturer,
|
||||
"model": Device().model,
|
||||
"os_name": Device().osName,
|
||||
"os_version": Device().osVersion,
|
||||
"app_data": {
|
||||
"push_token": "$fcmToken",
|
||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
||||
}
|
||||
message += '}';
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return _sendMessageRaw(message, true);
|
||||
}
|
||||
|
||||
Future _getConfig() async {
|
||||
await Connection().sendSocketMessage(type: "get_config").then((data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
}).catchError((e) {
|
||||
throw HAError("Error getting config: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getStates() async {
|
||||
await Connection().sendSocketMessage(type: "get_states").then(
|
||||
(data) => entities.parse(data)
|
||||
).catchError((e) {
|
||||
throw HAError("Error getting states: $e");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getLovelace() async {
|
||||
await Connection().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
||||
throw HAError("Error getting lovelace config: $e");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getUserInfo() async {
|
||||
_userName = null;
|
||||
await Connection().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
||||
Logger.w("Can't get user info: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getServices() async {
|
||||
await Connection().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
||||
Logger.w("Can't get services: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getPanels() async {
|
||||
panels.clear();
|
||||
await Connection().sendSocketMessage(type: "get_panels").then((data) {
|
||||
data.forEach((k,v) {
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
panels.add(Panel(
|
||||
id: k,
|
||||
type: v["component_name"],
|
||||
title: title,
|
||||
urlPath: v["url_path"],
|
||||
config: v["config"],
|
||||
icon: v["icon"]
|
||||
)
|
||||
);
|
||||
});
|
||||
}).catchError((e) {
|
||||
throw HAError("Error getting panels list: $e");
|
||||
});
|
||||
}
|
||||
|
||||
void _handleEntityStateChange(Map eventData) {
|
||||
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||
Map data = Map.from(eventData);
|
||||
entities.updateState(data);
|
||||
eventBus.fire(new StateChangedEvent(data["entity_id"], null));
|
||||
}
|
||||
|
||||
void _parseConfig(Map data) {
|
||||
if (data["success"] == true) {
|
||||
_instanceConfig = Map.from(data["result"]);
|
||||
_configCompleter.complete();
|
||||
} else {
|
||||
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
|
||||
if (_fetchCompleter.isCompleted) {
|
||||
Map data = Map.from(eventData);
|
||||
eventBus.fire(new StateChangedEvent(
|
||||
entityId: data["entity_id"],
|
||||
needToRebuildUI: entities.updateState(data)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _parseUserInfo(Map data) {
|
||||
if (data["success"] == true) {
|
||||
_userName = data["result"]["name"];
|
||||
} else {
|
||||
_userName = null;
|
||||
TheLogger.warning("There was an error getting current user: $data");
|
||||
}
|
||||
_userInfoCompleter.complete();
|
||||
}
|
||||
|
||||
void _parseServices(response) {
|
||||
_servicesCompleter.complete();
|
||||
}
|
||||
|
||||
void _handleLovelace(response) {
|
||||
if (response["success"] == true) {
|
||||
_rawLovelaceData = response["result"];
|
||||
} else {
|
||||
TheLogger.error("There was an error getting Lovelace config: $response");
|
||||
_rawLovelaceData = null;
|
||||
}
|
||||
_lovelaceCompleter.complete();
|
||||
}
|
||||
|
||||
void _parseLovelace() {
|
||||
TheLogger.debug("--Title: ${_rawLovelaceData["title"]}");
|
||||
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
||||
ui.title = _rawLovelaceData["title"];
|
||||
int viewCounter = 0;
|
||||
TheLogger.debug("--Views count: ${_rawLovelaceData['views'].length}");
|
||||
Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
|
||||
_rawLovelaceData["views"].forEach((rawView){
|
||||
TheLogger.debug("----view id: ${rawView['id']}");
|
||||
Logger.d("----view id: ${rawView['id']}");
|
||||
HAView view = HAView(
|
||||
count: viewCounter,
|
||||
id: "${rawView['id']}",
|
||||
name: rawView['title'],
|
||||
iconName: rawView['icon']
|
||||
);
|
||||
|
||||
if (rawView['badges'] != null && rawView['badges'] is List) {
|
||||
rawView['badges'].forEach((entity) {
|
||||
if (entities.isExist(entity)) {
|
||||
Entity e = entities.get(entity);
|
||||
view.badges.add(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
|
||||
ui.views.add(
|
||||
view
|
||||
@ -428,116 +295,137 @@ class HomeAssistant {
|
||||
List<HACard> _createLovelaceCards(List rawCards) {
|
||||
List<HACard> result = [];
|
||||
rawCards.forEach((rawCard){
|
||||
bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
HACard card = HACard(
|
||||
id: "card",
|
||||
name: isThereCardOptionsInside ? rawCard["card"]["title"] ?? rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
|
||||
type: isThereCardOptionsInside ? rawCard["card"]['type'] : rawCard['type'],
|
||||
columnsCount: isThereCardOptionsInside ? rawCard["card"]['columns'] ?? 4 : rawCard['columns'] ?? 4,
|
||||
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ?? true : rawCard['show_name'] ?? true,
|
||||
showState: isThereCardOptionsInside ? rawCard["card"]['show_state'] ?? true : rawCard['show_state'] ?? true,
|
||||
showEmpty: rawCard['show_empty'] ?? true,
|
||||
stateFilter: rawCard['state_filter'] ?? []
|
||||
);
|
||||
if (rawCard["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCard["cards"]);
|
||||
}
|
||||
rawCard["entities"]?.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (entities.isExist(rawEntity)) {
|
||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = entities.get(rawEntity["entity"]);
|
||||
String tapAction = EntityTapAction.moreInfo;
|
||||
String holdAction = EntityTapAction.none;
|
||||
if (card.type == CardType.glance || card.type == CardType.entityButton) {
|
||||
if (rawEntity["tap_action"] != null) {
|
||||
if (rawEntity["tap_action"] is String) {
|
||||
tapAction = rawEntity["tap_action"];
|
||||
holdAction = rawEntity["hold_action"];
|
||||
} else {
|
||||
tapAction = rawEntity["tap_action"]["action"];
|
||||
holdAction = rawEntity["hold_action"]["action"];
|
||||
}
|
||||
}
|
||||
try {
|
||||
bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
HACard card = HACard(
|
||||
id: "card",
|
||||
name: isThereCardOptionsInside ? rawCard["card"]["title"] ??
|
||||
rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
|
||||
type: isThereCardOptionsInside
|
||||
? rawCard["card"]['type']
|
||||
: rawCard['type'],
|
||||
columnsCount: isThereCardOptionsInside
|
||||
? rawCard["card"]['columns'] ?? 4
|
||||
: rawCard['columns'] ?? 4,
|
||||
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ??
|
||||
true : rawCard['show_name'] ?? true,
|
||||
showState: isThereCardOptionsInside
|
||||
? rawCard["card"]['show_state'] ?? true
|
||||
: rawCard['show_state'] ?? true,
|
||||
showEmpty: rawCard['show_empty'] ?? true,
|
||||
stateFilter: rawCard['state_filter'] ?? [],
|
||||
states: rawCard['states'],
|
||||
content: rawCard['content']
|
||||
);
|
||||
if (rawCard["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCard["cards"]);
|
||||
}
|
||||
rawCard["entities"]?.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (entities.isExist(rawEntity)) {
|
||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||
} else {
|
||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||
}
|
||||
card.entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
displayName: rawEntity["name"],
|
||||
icon: rawEntity["icon"],
|
||||
tapAction: tapAction,
|
||||
holdAction: holdAction,
|
||||
//tapActionService: rawEntity["service"],
|
||||
//tapActionServiceData: rawEntity["service_data"] ?? {"entity_id": e.entityId}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (rawCard["entity"] != null) {
|
||||
var en = rawCard["entity"];
|
||||
String tapAction = EntityTapAction.moreInfo;
|
||||
String holdAction = EntityTapAction.none;
|
||||
if (rawCard["tap_action"] != null) {
|
||||
if (rawCard["tap_action"] is String) {
|
||||
tapAction = rawCard["tap_action"];
|
||||
holdAction = rawCard["hold_action"];
|
||||
} else {
|
||||
tapAction = rawCard["tap_action"]["action"];
|
||||
holdAction = rawCard["hold_action"]["action"];
|
||||
if (rawEntity["type"] == "divider") {
|
||||
card.entities.add(EntityWrapper(entity: Entity.divider()));
|
||||
} else if (rawEntity["type"] == "section") {
|
||||
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
||||
} else if (rawEntity["type"] == "call-service") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.callService,
|
||||
"service": rawEntity["service"],
|
||||
"service_data": rawEntity["service_data"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
card.entities.add(EntityWrapper(
|
||||
entity: Entity.callService(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
service: rawEntity["service"],
|
||||
actionName: rawEntity["action_name"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (rawEntity["type"] == "weblink") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.navigate,
|
||||
"service": rawEntity["url"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
card.entities.add(EntityWrapper(
|
||||
entity: Entity.weblink(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
url: rawEntity["url"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = entities.get(rawEntity["entity"]);
|
||||
card.entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
displayName: rawEntity["name"],
|
||||
icon: rawEntity["icon"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
if (rawCard["entity"] != null) {
|
||||
var en = rawCard["entity"];
|
||||
if (en is String) {
|
||||
if (entities.isExist(en)) {
|
||||
Entity e = entities.get(en);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: rawCard["icon"],
|
||||
displayName: rawCard["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
|
||||
}
|
||||
} else {
|
||||
if (entities.isExist(en["entity"])) {
|
||||
Entity e = entities.get(en["entity"]);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: en["icon"],
|
||||
displayName: en["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (en is String) {
|
||||
if (entities.isExist(en)) {
|
||||
Entity e = entities.get(en);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
tapAction: tapAction,
|
||||
holdAction: holdAction,
|
||||
//tapActionService: rawCard["service"],
|
||||
//tapActionServiceData: rawCard["service_data"] ?? {"entity_id": e.entityId}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (entities.isExist(en["entity"])) {
|
||||
Entity e = entities.get(en["entity"]);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: en["icon"],
|
||||
displayName: en["name"],
|
||||
tapAction: tapAction,
|
||||
holdAction: holdAction,
|
||||
tapActionService: rawCard["service"],
|
||||
tapActionServiceData: rawCard["service_data"] ?? {"entity_id": e.entityId}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
result.add(card);
|
||||
} catch (e) {
|
||||
Logger.e("There was an error parsing card: ${e.toString()}");
|
||||
}
|
||||
result.add(card);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void _parseEntities(response) async {
|
||||
if (response["success"] == false) {
|
||||
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
|
||||
return;
|
||||
}
|
||||
entities.parse(response["result"]);
|
||||
_statesCompleter.complete();
|
||||
}
|
||||
|
||||
void _createUI() {
|
||||
ui = HomeAssistantUI();
|
||||
if ((_useLovelace) && (_rawLovelaceData != null)) {
|
||||
TheLogger.debug("Creating Lovelace UI");
|
||||
if ((Connection().useLovelace) && (_rawLovelaceData != null)) {
|
||||
Logger.d("Creating Lovelace UI");
|
||||
_parseLovelace();
|
||||
} else {
|
||||
TheLogger.debug("Creating group-based UI");
|
||||
Logger.d("Creating group-based UI");
|
||||
int viewCounter = 0;
|
||||
if (!entities.hasDefaultView) {
|
||||
HAView view = HAView(
|
||||
@ -567,38 +455,12 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildViews(BuildContext context, bool lovelace) {
|
||||
return ui.build(context);
|
||||
}
|
||||
|
||||
Future<List> getHistory(String entityId) async {
|
||||
DateTime now = DateTime.now();
|
||||
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||
TheLogger.debug("[Sending] ==> $url");
|
||||
http.Response historyResponse;
|
||||
if (_authType == "access_token") {
|
||||
historyResponse = await http.get(url, headers: {
|
||||
"authorization": "Bearer $_password",
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
} else {
|
||||
historyResponse = await http.get(url, headers: {
|
||||
"X-HA-Access": "$_password",
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
}
|
||||
var history = json.decode(historyResponse.body);
|
||||
if (history is List) {
|
||||
TheLogger.debug( "[Received] <== ${history.first.length} history recors");
|
||||
return history;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
Widget buildViews(BuildContext context, TabController tabController) {
|
||||
return ui.build(context, tabController);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class SendMessageQueue {
|
||||
int _messageTimeout;
|
||||
List<HAMessage> _queue = [];
|
||||
@ -637,4 +499,4 @@ class HAMessage {
|
||||
bool isExpired() {
|
||||
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
@ -19,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> {
|
||||
}
|
||||
|
||||
_loadLog() async {
|
||||
_logData = TheLogger.getLog();
|
||||
_logData = Logger.getLog();
|
||||
}
|
||||
|
||||
@override
|
||||
|
668
lib/main.dart
@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -7,17 +8,25 @@ import 'package:web_socket_channel/io.dart';
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_colorpicker/material_picker.dart';
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
import 'package:progress_indicators/progress_indicators.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
//import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
part 'entity_class/const.dart';
|
||||
part 'entity_class/entity.class.dart';
|
||||
part 'entity_class/entity_wrapper.class.dart';
|
||||
part 'entity_class/timer_entity.dart';
|
||||
part 'entity_class/switch_entity.class.dart';
|
||||
part 'entity_class/button_entity.class.dart';
|
||||
part 'entity_class/text_entity.class.dart';
|
||||
@ -32,9 +41,13 @@ part 'entity_class/media_player_entity.class.dart';
|
||||
part 'entity_class/lock_entity.class.dart';
|
||||
part 'entity_class/group_entity.class.dart';
|
||||
part 'entity_class/fan_entity.class.dart';
|
||||
part 'entity_class/automation_entity.dart';
|
||||
part 'entity_class/camera_entity.class.dart';
|
||||
part 'entity_class/alarm_control_panel.class.dart';
|
||||
part 'entity_widgets/common/badge.dart';
|
||||
part 'entity_widgets/model_widgets.dart';
|
||||
part 'entity_widgets/default_entity_container.dart';
|
||||
part 'entity_widgets/missed_entity.dart';
|
||||
part 'entity_widgets/glance_entity_container.dart';
|
||||
part 'entity_widgets/button_entity_container.dart';
|
||||
part 'entity_widgets/common/entity_attributes_list.dart';
|
||||
@ -44,6 +57,9 @@ part 'entity_widgets/common/last_updated.dart';
|
||||
part 'entity_widgets/common/mode_swicth.dart';
|
||||
part 'entity_widgets/common/mode_selector.dart';
|
||||
part 'entity_widgets/common/universal_slider.dart';
|
||||
part 'entity_widgets/common/flat_service_button.dart';
|
||||
part 'entity_widgets/common/light_color_picker.dart';
|
||||
part 'entity_widgets/common/camera_stream_view.dart';
|
||||
part 'entity_widgets/entity_colors.class.dart';
|
||||
part 'entity_widgets/entity_page_container.dart';
|
||||
part 'entity_widgets/history_chart/entity_history.dart';
|
||||
@ -57,42 +73,49 @@ part 'entity_widgets/controls/slider_controls.dart';
|
||||
part 'entity_widgets/state/text_input_state.dart';
|
||||
part 'entity_widgets/state/select_state.dart';
|
||||
part 'entity_widgets/state/simple_state.dart';
|
||||
part 'entity_widgets/state/timer_state.dart';
|
||||
part 'entity_widgets/state/climate_state.dart';
|
||||
part 'entity_widgets/state/cover_state.dart';
|
||||
part 'entity_widgets/state/date_time_state.dart';
|
||||
part 'entity_widgets/state/button_state.dart';
|
||||
part 'entity_widgets/state/lock_state.dart';
|
||||
part 'entity_widgets/controls/climate_controls.dart';
|
||||
part 'entity_widgets/controls/cover_controls.dart';
|
||||
part 'entity_widgets/controls/light_controls.dart';
|
||||
part 'entity_widgets/controls/media_player_widgets.dart';
|
||||
part 'entity_widgets/controls/fan_controls.dart';
|
||||
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
|
||||
part 'settings.page.dart';
|
||||
part 'panel.page.dart';
|
||||
part 'home_assistant.class.dart';
|
||||
part 'log.page.dart';
|
||||
part 'entity.page.dart';
|
||||
part 'utils.class.dart';
|
||||
part 'mdi.class.dart';
|
||||
part 'entity_collection.class.dart';
|
||||
part 'auth_manager.class.dart';
|
||||
part 'connection.class.dart';
|
||||
part 'device.class.dart';
|
||||
part 'ui_class/ui.dart';
|
||||
part 'ui_class/view.class.dart';
|
||||
part 'ui_class/card.class.dart';
|
||||
part 'ui_class/sizes_class.dart';
|
||||
part 'ui_class/panel_class.dart';
|
||||
part 'ui_widgets/view.dart';
|
||||
part 'ui_widgets/card_widget.dart';
|
||||
part 'ui_widgets/card_header_widget.dart';
|
||||
part 'ui_widgets/config_panel_widget.dart';
|
||||
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = "HA Client";
|
||||
const appVersion = "0.3.11";
|
||||
|
||||
String homeAssistantWebHost;
|
||||
const appVersion = "0.6.0-alpha3";
|
||||
|
||||
void main() {
|
||||
FlutterError.onError = (errorDetails) {
|
||||
TheLogger.error( "${errorDetails.exception}");
|
||||
if (TheLogger.isInDebugMode) {
|
||||
Logger.e( "${errorDetails.exception}");
|
||||
if (Logger.isInDebugMode) {
|
||||
FlutterError.dumpErrorToConsole(errorDetails);
|
||||
}
|
||||
};
|
||||
@ -100,15 +123,17 @@ void main() {
|
||||
runZoned(() {
|
||||
runApp(new HAClientApp());
|
||||
}, onError: (error, stack) {
|
||||
TheLogger.error("$error");
|
||||
TheLogger.error("$stack");
|
||||
if (TheLogger.isInDebugMode) {
|
||||
Logger.e("$error");
|
||||
Logger.e("$stack");
|
||||
if (Logger.isInDebugMode) {
|
||||
debugPrint("$stack");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class HAClientApp extends StatelessWidget {
|
||||
|
||||
final HomeAssistant homeAssistant = HomeAssistant();
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -119,100 +144,184 @@ class HAClientApp extends StatelessWidget {
|
||||
),
|
||||
initialRoute: "/",
|
||||
routes: {
|
||||
"/": (context) => MainPage(title: 'HA Client'),
|
||||
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
|
||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||
"/log-view": (context) => LogViewPage(title: "Log")
|
||||
"/configuration": (context) => PanelPage(title: "Configuration"),
|
||||
"/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"),
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
MainPage({Key key, this.title}) : super(key: key);
|
||||
MainPage({Key key, this.title, this.homeAssistant}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final HomeAssistant homeAssistant;
|
||||
|
||||
@override
|
||||
_MainPageState createState() => new _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
HomeAssistant _homeAssistant;
|
||||
//Map _instanceConfig;
|
||||
String _webSocketApiEndpoint;
|
||||
String _password;
|
||||
String _authType;
|
||||
//int _uiViewsCount = 0;
|
||||
String _instanceHost;
|
||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _settingsSubscription;
|
||||
StreamSubscription _serviceCallSubscription;
|
||||
StreamSubscription _showEntityPageSubscription;
|
||||
StreamSubscription _refreshDataSubscription;
|
||||
StreamSubscription _showErrorSubscription;
|
||||
bool _settingsLoaded = false;
|
||||
bool _accountMenuExpanded = false;
|
||||
bool _useLovelaceUI;
|
||||
StreamSubscription _startAuthSubscription;
|
||||
StreamSubscription _showDialogSubscription;
|
||||
StreamSubscription _reloadUISubscription;
|
||||
int _previousViewCount;
|
||||
bool _showLoginButton = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_settingsLoaded = false;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
TheLogger.debug("<!!!> Creating new HomeAssistant instance");
|
||||
_homeAssistant = HomeAssistant();
|
||||
_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) {
|
||||
TheLogger.debug("Settings change event: reconnect=${event.reconnect}");
|
||||
Logger.d("Settings change event: reconnect=${event.reconnect}");
|
||||
if (event.reconnect) {
|
||||
_homeAssistant.disconnect().then((_){
|
||||
_initialLoad();
|
||||
});
|
||||
_fullLoad();
|
||||
}
|
||||
});
|
||||
_initialLoad();
|
||||
|
||||
_fullLoad();
|
||||
}
|
||||
|
||||
void _initialLoad() {
|
||||
_loadConnectionSettings().then((_){
|
||||
_subscribe();
|
||||
_refreshData();
|
||||
}, onError: (_) {
|
||||
_showErrorBottomBar(message: _, errorCode: 5);
|
||||
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,);
|
||||
_subscribe().then((_) {
|
||||
Connection().init(loadSettings: true, forceReconnect: true).then((__){
|
||||
_fetchData();
|
||||
}, onError: (e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _quickLoad() {
|
||||
_hideBottomBar();
|
||||
_showInfoBottomBar(progress: true,);
|
||||
Connection().init(loadSettings: false, forceReconnect: false).then((_){
|
||||
_fetchData();
|
||||
}, onError: (e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
}
|
||||
|
||||
_fetchData() async {
|
||||
await widget.homeAssistant.fetchData().then((_) {
|
||||
_hideBottomBar();
|
||||
int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0;
|
||||
if (_previousViewCount != currentViewCount) {
|
||||
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
||||
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
||||
_previousViewCount = currentViewCount;
|
||||
}
|
||||
}).catchError((e) {
|
||||
if (e is HAError) {
|
||||
_setErrorState(e);
|
||||
} else {
|
||||
_setErrorState(HAError(e.toString()));
|
||||
}
|
||||
});
|
||||
eventBus.fire(RefreshDataFinishedEvent());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
TheLogger.debug("$state");
|
||||
if (state == AppLifecycleState.resumed && _settingsLoaded) {
|
||||
_refreshData();
|
||||
Logger.d("$state");
|
||||
if (state == AppLifecycleState.resumed && Connection().settingsLoaded) {
|
||||
_quickLoad();
|
||||
}
|
||||
}
|
||||
|
||||
_loadConnectionSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String domain = prefs.getString('hassio-domain');
|
||||
String port = prefs.getString('hassio-port');
|
||||
_instanceHost = "$domain:$port";
|
||||
_webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
|
||||
homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
|
||||
_password = prefs.getString('hassio-password');
|
||||
_authType = prefs.getString('hassio-auth-type');
|
||||
_useLovelaceUI = prefs.getBool('use-lovelace') ?? false;
|
||||
if ((domain == null) || (port == null) || (_password == null) ||
|
||||
(domain.length == 0) || (port.length == 0) || (_password.length == 0)) {
|
||||
throw("Check connection settings");
|
||||
} else {
|
||||
_settingsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
_subscribe() {
|
||||
Future _subscribe() {
|
||||
Completer completer = Completer();
|
||||
if (_stateSubscription == null) {
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
setState(() {});
|
||||
if (event.needToRebuildUI) {
|
||||
Logger.d("New entity. Need to rebuild UI");
|
||||
_quickLoad();
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (_reloadUISubscription == null) {
|
||||
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
||||
_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) {
|
||||
@ -230,60 +339,97 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
});
|
||||
}
|
||||
|
||||
if (_refreshDataSubscription == null) {
|
||||
_refreshDataSubscription = eventBus.on<RefreshDataEvent>().listen((event){
|
||||
_refreshData();
|
||||
});
|
||||
}
|
||||
|
||||
if (_showErrorSubscription == null) {
|
||||
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
||||
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
|
||||
_showErrorBottomBar(event.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_refreshData() async {
|
||||
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _authType, _useLovelaceUI);
|
||||
_hideBottomBar();
|
||||
_showInfoBottomBar(progress: true,);
|
||||
await _homeAssistant.fetch().then((result) {
|
||||
_hideBottomBar();
|
||||
}).catchError((e) {
|
||||
_setErrorState(e);
|
||||
if (_startAuthSubscription == null) {
|
||||
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
|
||||
setState(() {
|
||||
_showLoginButton = event.showButton;
|
||||
});
|
||||
if (event.showButton) {
|
||||
_showOAuth();
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_firebaseMessaging.getToken().then((String token) {
|
||||
HomeAssistant().fcmToken = token;
|
||||
completer.complete();
|
||||
});
|
||||
eventBus.fire(RefreshDataFinishedEvent());
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
_setErrorState(e) {
|
||||
if (e is Error) {
|
||||
TheLogger.error(e.toString());
|
||||
TheLogger.error("${e.stackTrace}");
|
||||
void _showOAuth() {
|
||||
Logger.d("_showOAuth: ${Connection().oauthUrl}");
|
||||
Navigator.of(context).pushNamed('/login');
|
||||
}
|
||||
|
||||
_setErrorState(HAError e) {
|
||||
if (e == null) {
|
||||
_showErrorBottomBar(
|
||||
message: "There was some error",
|
||||
errorCode: 13
|
||||
HAError("Unknown error")
|
||||
);
|
||||
} else {
|
||||
_showErrorBottomBar(
|
||||
message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error",
|
||||
errorCode: e["errorCode"] != null ? e["errorCode"] : 99
|
||||
);
|
||||
_showErrorBottomBar(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
|
||||
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) {
|
||||
_showInfoBottomBar(
|
||||
message: "Calling $domain.$service",
|
||||
duration: Duration(seconds: 3)
|
||||
);
|
||||
_homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e));
|
||||
Connection().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
|
||||
}
|
||||
|
||||
void _showEntityPage(String entityId) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant),
|
||||
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant),
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -291,8 +437,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
List<Tab> buildUIViewTabs() {
|
||||
List<Tab> result = [];
|
||||
|
||||
if (_homeAssistant.ui.views.isNotEmpty) {
|
||||
_homeAssistant.ui.views.forEach((HAView view) {
|
||||
if (widget.homeAssistant.ui.views.isNotEmpty) {
|
||||
widget.homeAssistant.ui.views.forEach((HAView view) {
|
||||
result.add(view.buildTab());
|
||||
});
|
||||
}
|
||||
@ -304,16 +450,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
List<Widget> menuItems = [];
|
||||
menuItems.add(
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: Text(_homeAssistant.userName),
|
||||
accountEmail: Text(_instanceHost ?? "Not configured"),
|
||||
onDetailsPressed: () {
|
||||
accountName: Text(widget.homeAssistant.userName),
|
||||
accountEmail: Text(Connection().displayHostname ?? "Not configured"),
|
||||
/*onDetailsPressed: () {
|
||||
setState(() {
|
||||
_accountMenuExpanded = !_accountMenuExpanded;
|
||||
});
|
||||
},
|
||||
},*/
|
||||
currentAccountPicture: CircleAvatar(
|
||||
child: Text(
|
||||
_homeAssistant.userAvatarText,
|
||||
widget.homeAssistant.userAvatarText,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0
|
||||
),
|
||||
@ -321,20 +467,43 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
),
|
||||
)
|
||||
);
|
||||
if (_accountMenuExpanded) {
|
||||
if (widget.homeAssistant.panels.isNotEmpty) {
|
||||
widget.homeAssistant.panels.forEach((Panel panel) {
|
||||
if (!panel.isHidden) {
|
||||
menuItems.add(
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
||||
title: Text("${panel.title}"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
panel.handleOpen(context);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
//TODO check for loaded
|
||||
menuItems.add(
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
|
||||
title: Text("Open Web UI"),
|
||||
onTap: () => HAUtils.launchURL(Connection().httpWebHost),
|
||||
)
|
||||
);
|
||||
menuItems.addAll([
|
||||
Divider(),
|
||||
ListTile(
|
||||
leading: Icon(Icons.settings),
|
||||
title: Text("Settings"),
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
|
||||
title: Text("Connection settings"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/connection-settings');
|
||||
Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
)
|
||||
]);
|
||||
} else {
|
||||
menuItems.addAll([
|
||||
Divider(),
|
||||
new ListTile(
|
||||
leading: Icon(Icons.insert_drive_file),
|
||||
title: Text("Log"),
|
||||
@ -344,7 +513,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")),
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
|
||||
title: Text("Report an issue"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
@ -352,28 +521,74 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
},
|
||||
),
|
||||
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(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
||||
title: Text("Join Discord channel"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
||||
},
|
||||
),
|
||||
new AboutListTile(
|
||||
aboutBoxChildren: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
HAUtils.launchURL("http://www.keyboardcrumbs.io/");
|
||||
HAUtils.launchURL("http://ha-client.homemade.systems/");
|
||||
},
|
||||
child: Text(
|
||||
"www.keyboardcrumbs.io",
|
||||
"ha-client.homemade.systems",
|
||||
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/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,
|
||||
applicationVersion: appVersion,
|
||||
applicationLegalese: "Keyboard Crumbs",
|
||||
applicationVersion: appVersion
|
||||
)
|
||||
]);
|
||||
}
|
||||
return new Drawer(
|
||||
child: ListView(
|
||||
children: menuItems,
|
||||
@ -411,101 +626,100 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
void _showErrorBottomBar({Key key, @required String message, @required int errorCode}) {
|
||||
void _showErrorBottomBar(HAError error) {
|
||||
TextStyle textStyle = TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: Sizes.nameFontSize
|
||||
color: Colors.blue,
|
||||
fontSize: Sizes.nameFontSize
|
||||
);
|
||||
_bottomBarColor = Colors.red.shade100;
|
||||
switch (errorCode) {
|
||||
case 9:
|
||||
case 11:
|
||||
case 7:
|
||||
case 1: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Retry", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 5: {
|
||||
message = "Check connection settings";
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Open", style: textStyle),
|
||||
List<Widget> actions = [];
|
||||
error.actions.forEach((HAErrorAction action) {
|
||||
switch (action.type) {
|
||||
case HAErrorActionType.FULL_RELOAD: {
|
||||
actions.add(FlatButton(
|
||||
child: Text("${action.title}", style: textStyle),
|
||||
onPressed: () {
|
||||
_fullLoad();
|
||||
},
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
case HAErrorActionType.QUICK_RELOAD: {
|
||||
actions.add(FlatButton(
|
||||
child: Text("${action.title}", style: textStyle),
|
||||
onPressed: () {
|
||||
_quickLoad();
|
||||
},
|
||||
));
|
||||
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: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
Navigator.pushNamed(context, '/connection-settings');
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 6: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Settings", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
Navigator.pushNamed(context, '/connection-settings');
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 10: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Refresh", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 8: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Reconnect", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
_bottomBarAction = FlatButton(
|
||||
child: Text("Reload", style: textStyle),
|
||||
onPressed: () {
|
||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||
_refreshData();
|
||||
},
|
||||
);
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_bottomBarProgress = false;
|
||||
_bottomBarText = "$message (code: $errorCode)";
|
||||
_showBottomBar = true;
|
||||
});
|
||||
/*_scaffoldKey.currentState.hideCurrentSnackBar();
|
||||
_scaffoldKey.currentState.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("$message (code: $errorCode)"),
|
||||
action: action,
|
||||
duration: Duration(hours: 1),
|
||||
)
|
||||
);*/
|
||||
});
|
||||
if (actions.isNotEmpty) {
|
||||
_bottomBarAction = Row(
|
||||
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>();
|
||||
|
||||
Widget _buildScaffoldBody(bool empty) {
|
||||
List<PopupMenuItem<String>> popupMenuItems = [];
|
||||
popupMenuItems.add(PopupMenuItem<String>(
|
||||
child: new Text("Reload"),
|
||||
value: "reload",
|
||||
));
|
||||
List<Widget> emptyBody = [
|
||||
Text("."),
|
||||
];
|
||||
if (Connection().isAuthenticated) {
|
||||
_showLoginButton = false;
|
||||
popupMenuItems.add(
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("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(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
@ -513,17 +727,36 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
floating: true,
|
||||
pinned: true,
|
||||
primary: true,
|
||||
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
|
||||
title: Text(widget.homeAssistant.locationName ?? ""),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical"), color: Colors.white,),
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
||||
context: context,
|
||||
items: popupMenuItems
|
||||
).then((String val) {
|
||||
if (val == "reload") {
|
||||
_quickLoad();
|
||||
} else if (val == "logout") {
|
||||
widget.homeAssistant.logout().then((_) {
|
||||
_quickLoad();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
],
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
setState(() {
|
||||
_accountMenuExpanded = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
bottom: empty ? null : TabBar(
|
||||
controller: _viewsTabController,
|
||||
tabs: buildUIViewTabs(),
|
||||
isScrollable: true,
|
||||
),
|
||||
@ -535,20 +768,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
|
||||
size: 100.0,
|
||||
color: Colors.blue,
|
||||
),
|
||||
]
|
||||
children: emptyBody
|
||||
),
|
||||
)
|
||||
:
|
||||
_homeAssistant.buildViews(context, _useLovelaceUI),
|
||||
widget.homeAssistant.buildViews(context, _viewsTabController),
|
||||
);
|
||||
}
|
||||
|
||||
TabController _viewsTabController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget bottomBar;
|
||||
@ -600,7 +829,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
// This method is rerun every time setState is called.
|
||||
if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) {
|
||||
if (widget.homeAssistant.isNoViews) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
primary: false,
|
||||
@ -614,24 +843,27 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
|
||||
drawer: _buildAppDrawer(),
|
||||
primary: false,
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: DefaultTabController(
|
||||
length: _homeAssistant.ui?.views?.length ?? 0,
|
||||
child: _buildScaffoldBody(false),
|
||||
),
|
||||
body: _buildScaffoldBody(false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
flutterWebviewPlugin.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||
if (_settingsSubscription != null) _settingsSubscription.cancel();
|
||||
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
|
||||
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
|
||||
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
||||
if (_showErrorSubscription != null) _showErrorSubscription.cancel();
|
||||
_homeAssistant.disconnect();
|
||||
_viewsTabController?.dispose();
|
||||
_stateSubscription?.cancel();
|
||||
_settingsSubscription?.cancel();
|
||||
_serviceCallSubscription?.cancel();
|
||||
_showDialogSubscription?.cancel();
|
||||
_showEntityPageSubscription?.cancel();
|
||||
_showErrorSubscription?.cancel();
|
||||
_startAuthSubscription?.cancel();
|
||||
_reloadUISubscription?.cancel();
|
||||
//TODO disconnect
|
||||
//widget.homeAssistant?.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
1025
lib/mdi.class.dart
40
lib/panel.page.dart
Normal file
@ -0,0 +1,40 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class PanelPage extends StatefulWidget {
|
||||
PanelPage({Key key, this.title, this.panel}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final Panel panel;
|
||||
|
||||
@override
|
||||
_PanelPageState createState() => new _PanelPageState();
|
||||
}
|
||||
|
||||
class _PanelPageState extends State<PanelPage> {
|
||||
|
||||
List<ConfigurationItem> _items;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text(widget.title),
|
||||
),
|
||||
body: widget.panel.getWidget(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -14,19 +14,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
String _newHassioDomain = "";
|
||||
String _hassioPort = "";
|
||||
String _newHassioPort = "";
|
||||
String _hassioPassword = "";
|
||||
String _newHassioPassword = "";
|
||||
String _socketProtocol = "wss";
|
||||
String _newSocketProtocol = "wss";
|
||||
String _authType = "access_token";
|
||||
String _newAuthType = "access_token";
|
||||
bool _useLovelace = false;
|
||||
bool _newUseLovelace = false;
|
||||
bool _useLovelace = true;
|
||||
bool _newUseLovelace = true;
|
||||
|
||||
String oauthUrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
|
||||
}
|
||||
|
||||
_loadSettings() async {
|
||||
@ -35,23 +34,20 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
setState(() {
|
||||
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
||||
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
||||
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
|
||||
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
||||
_authType = _newAuthType = prefs.getString("hassio-auth-type") ?? 'access_token';
|
||||
try {
|
||||
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? false;
|
||||
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
|
||||
} catch (e) {
|
||||
_useLovelace = _newUseLovelace = false;
|
||||
_useLovelace = _newUseLovelace = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _checkConfigChanged() {
|
||||
return ((_newHassioPassword != _hassioPassword) ||
|
||||
return (
|
||||
(_newHassioPort != _hassioPort) ||
|
||||
(_newHassioDomain != _hassioDomain) ||
|
||||
(_newSocketProtocol != _socketProtocol) ||
|
||||
(_newAuthType != _authType) ||
|
||||
(_newUseLovelace != _useLovelace));
|
||||
|
||||
}
|
||||
@ -63,10 +59,8 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString("hassio-domain", _newHassioDomain);
|
||||
prefs.setString("hassio-port", _newHassioPort);
|
||||
prefs.setString("hassio-password", _newHassioPassword);
|
||||
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||
prefs.setString("hassio-auth-type", _newAuthType);
|
||||
prefs.setBool("use-lovelace", _newUseLovelace);
|
||||
}
|
||||
|
||||
@ -83,13 +77,13 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
icon: Icon(Icons.check),
|
||||
onPressed: (){
|
||||
if (_checkConfigChanged()) {
|
||||
TheLogger.debug("Settings changed. Saving...");
|
||||
Logger.d("Settings changed. Saving...");
|
||||
_saveSettings().then((r) {
|
||||
Navigator.pop(context);
|
||||
eventBus.fire(SettingsChangedEvent(true));
|
||||
});
|
||||
} else {
|
||||
TheLogger.debug("Settings was not changed");
|
||||
Logger.d("Settings was not changed");
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
@ -150,40 +144,9 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
_newHassioPort = value;
|
||||
}
|
||||
),
|
||||
new Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
"Login with access token (HA >= 0.78.0)",
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: (_newAuthType == "access_token"),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_newAuthType = value ? "access_token" : "api_password";
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
new TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: _newAuthType == "access_token" ? "Access token" : "API password"
|
||||
),
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioPassword,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioPassword.length)
|
||||
)
|
||||
),
|
||||
onChanged: (value) {
|
||||
_newHassioPassword = value;
|
||||
}
|
||||
new Text(
|
||||
"Try ports 80 and 443 if default is not working and you don't know why.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20.0),
|
||||
|
@ -12,6 +12,8 @@ class HACard {
|
||||
bool showEmpty;
|
||||
int columnsCount;
|
||||
List stateFilter;
|
||||
List states;
|
||||
String content;
|
||||
|
||||
HACard({
|
||||
this.name,
|
||||
@ -22,6 +24,8 @@ class HACard {
|
||||
this.showState: true,
|
||||
this.stateFilter: const [],
|
||||
this.showEmpty: true,
|
||||
this.content,
|
||||
this.states,
|
||||
@required this.type
|
||||
});
|
||||
|
||||
|
57
lib/ui_class/panel_class.dart
Normal file
@ -0,0 +1,57 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Panel {
|
||||
|
||||
static const iconsByComponent = {
|
||||
"config": "mdi:settings",
|
||||
"history": "mdi:poll-box",
|
||||
"map": "mdi:tooltip-account",
|
||||
"logbook": "mdi:format-list-bulleted-type",
|
||||
"custom": "mdi:home-assistant"
|
||||
};
|
||||
|
||||
final String id;
|
||||
final String type;
|
||||
final String title;
|
||||
final String urlPath;
|
||||
final Map config;
|
||||
String icon;
|
||||
bool isHidden = true;
|
||||
|
||||
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
||||
if (icon == null || !icon.startsWith("mdi:")) {
|
||||
icon = Panel.iconsByComponent[type];
|
||||
}
|
||||
isHidden = (type != "iframe" && type != "config");
|
||||
}
|
||||
|
||||
void handleOpen(BuildContext context) {
|
||||
if (type == "iframe") {
|
||||
Logger.d("Launching custom tab with ${config["url"]}");
|
||||
HAUtils.launchURLInCustomTab(context: context, url: config["url"]);
|
||||
} else if (type == "config") {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PanelPage(title: "$title", panel: this),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
String url = "${Connection().httpWebHost}/$urlPath";
|
||||
Logger.d("Launching custom tab with $url");
|
||||
HAUtils.launchURLInCustomTab(context: context, url: url);
|
||||
}
|
||||
}
|
||||
|
||||
Widget getWidget() {
|
||||
switch (type) {
|
||||
case "config": {
|
||||
return ConfigPanelWidget();
|
||||
}
|
||||
|
||||
default: {
|
||||
return Text("Unsupported panel component: $type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Sizes {
|
||||
static const rightWidgetPadding = 14.0;
|
||||
static const leftWidgetPadding = 8.0;
|
||||
static const rightWidgetPadding = 10.0;
|
||||
static const leftWidgetPadding = 10.0;
|
||||
static const buttonPadding = 4.0;
|
||||
static const extendedWidgetHeight = 50.0;
|
||||
static const iconSize = 28.0;
|
||||
|
@ -4,12 +4,15 @@ class HomeAssistantUI {
|
||||
List<HAView> views;
|
||||
String title;
|
||||
|
||||
bool get isEmpty => views == null || views.isEmpty;
|
||||
|
||||
HomeAssistantUI() {
|
||||
views = [];
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, TabController tabController) {
|
||||
return TabBarView(
|
||||
controller: tabController,
|
||||
children: _buildViews(context)
|
||||
);
|
||||
}
|
||||
@ -24,4 +27,8 @@ class HomeAssistantUI {
|
||||
return result;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
views.clear();
|
||||
}
|
||||
|
||||
}
|
@ -77,7 +77,7 @@ class HAView {
|
||||
Tab(
|
||||
icon:
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName(
|
||||
MaterialDesignIcons.getIconDataFromIconName(
|
||||
iconName ?? "mdi:home-assistant"),
|
||||
size: 24.0,
|
||||
)
|
||||
@ -92,7 +92,7 @@ class HAView {
|
||||
if (linkedEntity.icon != null && linkedEntity.icon.length > 0) {
|
||||
return Tab(
|
||||
icon: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName(
|
||||
MaterialDesignIcons.getIconDataFromIconName(
|
||||
linkedEntity.icon),
|
||||
size: 24.0,
|
||||
)
|
||||
|
@ -3,14 +3,18 @@ part of '../main.dart';
|
||||
class CardHeaderWidget extends StatelessWidget {
|
||||
|
||||
final String name;
|
||||
final Widget trailing;
|
||||
final Widget subtitle;
|
||||
|
||||
const CardHeaderWidget({Key key, this.name}) : super(key: key);
|
||||
const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var result;
|
||||
if ((name != null) && (name.trim().length > 0)) {
|
||||
result = new ListTile(
|
||||
trailing: trailing,
|
||||
subtitle: subtitle,
|
||||
title: Text("$name",
|
||||
textAlign: TextAlign.left,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
@ -11,8 +11,17 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if ((card.linkedEntityWrapper!= null) && (card.linkedEntityWrapper.entity.isHidden)) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
if (card.linkedEntityWrapper!= null) {
|
||||
if (card.linkedEntityWrapper.entity.isHidden) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
switch (card.type) {
|
||||
@ -33,16 +42,26 @@ class CardWidget extends StatelessWidget {
|
||||
return _buildEntityButtonCard(context);
|
||||
}
|
||||
|
||||
case CardType.markdown: {
|
||||
return _buildMarkdownCard(context);
|
||||
}
|
||||
|
||||
case CardType.alarmPanel: {
|
||||
return _buildAlarmPanelCard(context);
|
||||
}
|
||||
|
||||
case CardType.horizontalStack: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
children.add(
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: card.build(context),
|
||||
)
|
||||
);
|
||||
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
|
||||
children.add(
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: card.build(context),
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
@ -93,7 +112,7 @@ class CardWidget extends StatelessWidget {
|
||||
if (!entity.entity.isHidden) {
|
||||
body.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
handleTap: true,
|
||||
@ -103,7 +122,73 @@ class CardWidget extends StatelessWidget {
|
||||
}
|
||||
});
|
||||
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),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMarkdownCard(BuildContext context) {
|
||||
if (card.content == null) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name));
|
||||
body.add(MarkdownBody(data: card.content));
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(
|
||||
name: card.name ?? "",
|
||||
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||
style: TextStyle(
|
||||
color: Colors.grey
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
EntityIcon(
|
||||
size: 50.0,
|
||||
),
|
||||
Container(
|
||||
width: 26.0,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
alignment: Alignment.centerRight,
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical")),
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
));
|
||||
body.add(
|
||||
AlarmControlPanelControlsWidget(
|
||||
extended: true,
|
||||
states: card.states,
|
||||
)
|
||||
);
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
handleTap: null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: body
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -151,16 +236,17 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildMediaControlsCard(BuildContext context) {
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
handleTap: null,
|
||||
child: MediaPlayerWidget()
|
||||
)
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
handleTap: null,
|
||||
child: MediaPlayerWidget()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntityButtonCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ?? card.linkedEntityWrapper.displayName.toUpperCase();
|
||||
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
||||
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
|
331
lib/ui_widgets/config_panel_widget.dart
Normal file
@ -0,0 +1,331 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ConfigPanelWidget extends StatefulWidget {
|
||||
ConfigPanelWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState();
|
||||
}
|
||||
|
||||
class ConfigurationItem {
|
||||
ConfigurationItem({ this.isExpanded: false, this.header, this.body });
|
||||
|
||||
bool isExpanded;
|
||||
final String header;
|
||||
final Widget body;
|
||||
}
|
||||
|
||||
class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
||||
|
||||
List<ConfigurationItem> _items;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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(
|
||||
header: 'General',
|
||||
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/core");
|
||||
},
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("Control your Home Assistant server from HA Client."),
|
||||
Divider(),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('Restart', style: TextStyle(color: Colors.blue)),
|
||||
onPressed: () => restart(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text("Stop", style: TextStyle(color: Colors.blue)),
|
||||
onPressed: () => stop(),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
new ExpansionPanelList(
|
||||
expansionCallback: (int index, bool isExpanded) {
|
||||
setState(() {
|
||||
_items[index].isExpanded = !_items[index].isExpanded;
|
||||
});
|
||||
},
|
||||
children: _items.map((ConfigurationItem item) {
|
||||
return new ExpansionPanel(
|
||||
headerBuilder: (BuildContext context, bool isExpanded) {
|
||||
return CardHeaderWidget(
|
||||
name: item.header,
|
||||
);
|
||||
},
|
||||
isExpanded: item.isExpanded,
|
||||
body: new Container(
|
||||
child: item.body,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -17,29 +17,17 @@ class ViewWidget extends StatefulWidget {
|
||||
|
||||
class ViewWidgetState extends State<ViewWidget> {
|
||||
|
||||
StreamSubscription _refreshDataSubscription;
|
||||
Completer _refreshCompleter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
|
||||
_refreshCompleter.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
color: Colors.amber,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: _buildChildren(context),
|
||||
),
|
||||
onRefresh: () => _refreshData(),
|
||||
return ListView(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
//physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: _buildChildren(context),
|
||||
);
|
||||
}
|
||||
|
||||
@ -57,12 +45,22 @@ class ViewWidgetState extends State<ViewWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> cards = [];
|
||||
widget.view.cards.forEach((HACard card){
|
||||
result.add(
|
||||
card.build(context)
|
||||
cards.add(
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 500),
|
||||
child: card.build(context),
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
result.add(
|
||||
Column (
|
||||
children: cards,
|
||||
)
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -76,19 +74,8 @@ class ViewWidgetState extends State<ViewWidget> {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future _refreshData() {
|
||||
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
|
||||
TheLogger.debug("Previous data refresh is still in progress");
|
||||
} else {
|
||||
_refreshCompleter = Completer();
|
||||
eventBus.fire(RefreshDataEvent());
|
||||
}
|
||||
return _refreshCompleter.future;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshDataSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class TheLogger {
|
||||
class Logger {
|
||||
|
||||
static List<String> _log = [];
|
||||
|
||||
@ -20,15 +20,15 @@ class TheLogger {
|
||||
return inDebugMode;
|
||||
}
|
||||
|
||||
static void error(String message) {
|
||||
static void e(String message) {
|
||||
_writeToLog("Error", message);
|
||||
}
|
||||
|
||||
static void warning(String message) {
|
||||
static void w(String message) {
|
||||
_writeToLog("Warning", message);
|
||||
}
|
||||
|
||||
static void debug(String message) {
|
||||
static void d(String message) {
|
||||
_writeToLog("Debug", message);
|
||||
}
|
||||
|
||||
@ -45,12 +45,88 @@ class TheLogger {
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
static void launchURL(String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
if (await urlLauncher.canLaunch(url)) {
|
||||
await urlLauncher.launch(url);
|
||||
} else {
|
||||
TheLogger.error( "Could not launch $url");
|
||||
Logger.e( "Could not launch $url");
|
||||
}
|
||||
}
|
||||
|
||||
static void launchURLInCustomTab({BuildContext context, String url, bool enableDefaultShare: true, bool showPageTitle: true}) async {
|
||||
try {
|
||||
await launch(
|
||||
"$url",
|
||||
option: new CustomTabsOption(
|
||||
toolbarColor: Theme.of(context).primaryColor,
|
||||
enableDefaultShare: enableDefaultShare,
|
||||
enableUrlBarHiding: true,
|
||||
showPageTitle: showPageTitle,
|
||||
animation: new CustomTabsAnimation.slideIn()
|
||||
// or user defined animation.
|
||||
/*animation: new CustomTabsAnimation(
|
||||
startEnter: 'slide_up',
|
||||
startExit: 'android:anim/fade_out',
|
||||
endEnter: 'android:anim/fade_in',
|
||||
endExit: 'slide_down',
|
||||
)*/,
|
||||
extraCustomTabs: <String>[
|
||||
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
|
||||
'org.mozilla.firefox',
|
||||
// ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx
|
||||
'com.microsoft.emmx',
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Logger.w("Can't open custom tab: ${e.toString()}");
|
||||
Logger.w("Launching in default browser");
|
||||
HAUtils.launchURL(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,8 +134,13 @@ class HAUtils {
|
||||
class StateChangedEvent {
|
||||
String entityId;
|
||||
String newState;
|
||||
bool needToRebuildUI;
|
||||
|
||||
StateChangedEvent(this.entityId, this.newState);
|
||||
StateChangedEvent({
|
||||
this.entityId,
|
||||
this.newState,
|
||||
this.needToRebuildUI: false
|
||||
});
|
||||
}
|
||||
|
||||
class SettingsChangedEvent {
|
||||
@ -68,14 +149,21 @@ class SettingsChangedEvent {
|
||||
SettingsChangedEvent(this.reconnect);
|
||||
}
|
||||
|
||||
class RefreshDataEvent {
|
||||
RefreshDataEvent();
|
||||
}
|
||||
|
||||
class RefreshDataFinishedEvent {
|
||||
RefreshDataFinishedEvent();
|
||||
}
|
||||
|
||||
class ReloadUIEvent {
|
||||
ReloadUIEvent();
|
||||
}
|
||||
|
||||
class StartAuthEvent {
|
||||
String oauthUrl;
|
||||
bool showButton;
|
||||
|
||||
StartAuthEvent(this.oauthUrl, this.showButton);
|
||||
}
|
||||
|
||||
class ServiceCallEvent {
|
||||
String domain;
|
||||
String service;
|
||||
@ -85,6 +173,17 @@ class ServiceCallEvent {
|
||||
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 {
|
||||
Entity entity;
|
||||
|
||||
@ -92,8 +191,7 @@ class ShowEntityPageEvent {
|
||||
}
|
||||
|
||||
class ShowErrorEvent {
|
||||
String text;
|
||||
int errorCode;
|
||||
final HAError error;
|
||||
|
||||
ShowErrorEvent(this.text, this.errorCode);
|
||||
ShowErrorEvent(this.error);
|
||||
}
|
153
pubspec.lock
@ -1,5 +1,5 @@
|
||||
# Generated by pub
|
||||
# See https://www.dartlang.org/tools/pub/glossary#lockfile
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
archive:
|
||||
dependency: transitive
|
||||
@ -7,21 +7,21 @@ packages:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
version: "2.0.10"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
version: "1.5.2"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
version: "2.2.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -35,7 +35,7 @@ packages:
|
||||
name: cached_network_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
version: "1.1.1"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -49,14 +49,14 @@ packages:
|
||||
name: charts_common
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.6.0"
|
||||
charts_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: charts_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.6.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -70,37 +70,42 @@ packages:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.1.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
dart_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: e26916e095244a7e5ea61315b030d298d127ed26
|
||||
url: "https://github.com/MarkOSullivan94/dart_config.git"
|
||||
source: git
|
||||
version: "0.5.0"
|
||||
version: "2.1.1+1"
|
||||
date_format:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: date_format
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: event_bus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.1.0"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0+4"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -112,33 +117,61 @@ packages:
|
||||
name: flutter_cache_manager
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0+1"
|
||||
flutter_colorpicker:
|
||||
version: "1.1.1"
|
||||
flutter_custom_tabs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_colorpicker
|
||||
name: flutter_custom_tabs
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
version: "0.6.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
url: "https://pub.dartlang.org"
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.1+1"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_webview_plugin:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webview_plugin
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.7"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.0"
|
||||
version: "0.12.0+2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -152,14 +185,14 @@ packages:
|
||||
name: image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.1.4"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.15.7"
|
||||
version: "0.15.8"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -167,13 +200,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.11.3+2"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.3+1"
|
||||
version: "0.12.5"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -194,35 +234,49 @@ packages:
|
||||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
version: "1.2.0"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.4.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
progress_indicators:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: progress_indicators
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
version: "0.1.4"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "2.0.3"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
version: "0.5.3+4"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -234,7 +288,14 @@ packages:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
version: "1.5.5"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6+3"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -248,7 +309,7 @@ packages:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.8"
|
||||
version: "2.0.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -262,21 +323,21 @@ packages:
|
||||
name: synchronized
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.3"
|
||||
version: "2.1.0+1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.1.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
version: "0.2.5"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -290,14 +351,14 @@ packages:
|
||||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
version: "5.1.2"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "2.0.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -311,21 +372,21 @@ packages:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
version: "1.0.15"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
version: "3.2.5"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.15"
|
||||
version: "2.1.16"
|
||||
sdks:
|
||||
dart: ">=2.0.0 <3.0.0"
|
||||
flutter: ">=0.5.6 <2.0.0"
|
||||
dart: ">=2.4.0 <3.0.0"
|
||||
flutter: ">=1.5.0 <2.0.0"
|
||||
|
17
pubspec.yaml
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 0.3.11+78
|
||||
version: 0.6.0+603
|
||||
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||
@ -16,12 +16,15 @@ dependencies:
|
||||
cached_network_image: any
|
||||
url_launcher: any
|
||||
date_format: any
|
||||
flutter_colorpicker: any
|
||||
charts_flutter: any
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
#cupertino_icons: ^0.1.2
|
||||
flutter_markdown: any
|
||||
# flutter_svg: ^0.10.3
|
||||
flutter_custom_tabs: ^0.6.0
|
||||
firebase_messaging: ^4.0.0+1
|
||||
flutter_webview_plugin: ^0.3.1
|
||||
flutter_secure_storage: ^3.2.0
|
||||
device_info: ^0.4.0+1
|
||||
flutter_local_notifications: ^0.7.1+3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -62,7 +65,7 @@ flutter:
|
||||
fonts:
|
||||
- family: "Material Design Icons"
|
||||
fonts:
|
||||
- asset: fonts/materialdesignicons-webfont.ttf
|
||||
- asset: fonts/materialdesignicons-webfont-3-6-95.ttf
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
|