Compare commits
217 Commits
0.3.9
...
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 | |||
a8500d44e1 | |||
b4d4c5abec | |||
c19a3f272a | |||
b264534858 | |||
ab53f77f9e | |||
c73956720c | |||
051041e794 | |||
5c83be9fee | |||
4bece42693 | |||
4ae107fe4c | |||
9523ed2562 | |||
9c403480e2 | |||
20b1b90e39 | |||
5633e30448 | |||
4492fb9f0c | |||
36410752e4 | |||
0219f7bfbb | |||
5f3c77f4b9 | |||
a36c7a9ca3 | |||
56ce6dfeeb | |||
67c214454f | |||
73398378c4 | |||
215871ce9e | |||
fd8ea6befd | |||
809a1a1c8c | |||
fc8f2f200f | |||
f41c9f9197 | |||
cdf55ce68b | |||
12088d9516 | |||
a0235ee385 | |||
67fbdb13c6 | |||
c5960de0be | |||
da15e880ec | |||
efbe33f4e3 | |||
af84c99a2d | |||
438449cad8 | |||
d9ca55c3b7 | |||
f248268984 | |||
8ee096595c |
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
1
docs/empty
Normal file
@ -0,0 +1 @@
|
||||
|
BIN
docs/ha_access_tokens.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
docs/ha_profile-300x247.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/settings-869x1024.png
Normal file
After Width: | Height: | Size: 102 KiB |
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}";
|
||||
});
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class EntityViewPage extends StatefulWidget {
|
||||
EntityViewPage({Key key, @required this.entity, @required this.homeAssistant }) : super(key: key);
|
||||
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
|
||||
|
||||
final Entity entity;
|
||||
final String entityId;
|
||||
final HomeAssistant homeAssistant;
|
||||
|
||||
@override
|
||||
@ -12,30 +12,26 @@ class EntityViewPage extends StatefulWidget {
|
||||
|
||||
class _EntityViewPageState extends State<EntityViewPage> {
|
||||
String _title;
|
||||
StreamSubscription _refreshDataSubscription;
|
||||
StreamSubscription _stateSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.entityId == widget.entity.entityId) {
|
||||
if (event.entityId == widget.entityId) {
|
||||
Logger.d("State change event handled by entity page: ${event.entityId}");
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||
setState(() {});
|
||||
});
|
||||
_prepareData();
|
||||
_getHistory();
|
||||
}
|
||||
|
||||
void _prepareData() async {
|
||||
_title = widget.entity.displayName;
|
||||
}
|
||||
|
||||
void _getHistory() {
|
||||
/* widget.homeAssistant.getHistory(widget.entity.entityId).then((List history) {
|
||||
if (history != null) {
|
||||
|
||||
}
|
||||
});*/
|
||||
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
|
||||
}
|
||||
|
||||
|
||||
@ -50,19 +46,14 @@ 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.entity.buildEntityPageWidget(context)
|
||||
)
|
||||
),
|
||||
body: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
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,10 +28,72 @@ 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 {
|
||||
static const horizontalStack = "horizontal-stack";
|
||||
static const verticalStack = "vertical-stack";
|
||||
static const entities = "entities";
|
||||
static const glance = "glance";
|
||||
static const mediaControl = "media-control";
|
||||
static const weatherForecast = "weather-forecast";
|
||||
static const thermostat = "thermostat";
|
||||
static const sensor = "sensor";
|
||||
static const plantStatus = "plant-status";
|
||||
static const pictureEntity = "picture-entity";
|
||||
static const pictureElements = "picture-elements";
|
||||
static const picture = "picture";
|
||||
static const map = "map";
|
||||
static const iframe = "iframe";
|
||||
static const gauge = "gauge";
|
||||
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) {
|
||||
@ -76,19 +181,21 @@ class Entity {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> getStringListAttributeValue(String attribute) {
|
||||
if (attributes["$attribute"] != null) {
|
||||
List<String> result = (attributes["$attribute"] as List).cast<String>();
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildDefaultWidget(BuildContext context) {
|
||||
return DefaultEntityContainer(
|
||||
state: _buildStatePart(context)
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGlanceWidget(BuildContext context, bool showName, bool showState) {
|
||||
return GlanceEntityContainer(
|
||||
showName: showName,
|
||||
showState: showState,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return SimpleEntityState();
|
||||
}
|
||||
@ -108,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,10 +4,8 @@ class EntityWrapper {
|
||||
|
||||
String displayName;
|
||||
String icon;
|
||||
String tapAction;
|
||||
String holdAction;
|
||||
String actionService;
|
||||
Map<String, dynamic> actionServiceData;
|
||||
String entityPicture;
|
||||
EntityUIAction uiAction;
|
||||
Entity entity;
|
||||
|
||||
|
||||
@ -15,13 +13,102 @@ class EntityWrapper {
|
||||
this.entity,
|
||||
String icon,
|
||||
String displayName,
|
||||
this.tapAction: EntityTapAction.moreInfo,
|
||||
this.holdAction,
|
||||
this.actionService,
|
||||
this.actionServiceData
|
||||
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() {
|
||||
switch (uiAction.tapAction) {
|
||||
case EntityUIAction.toggle: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.callService: {
|
||||
if (uiAction.tapService != null) {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent(uiAction.tapService.split(".")[0],
|
||||
uiAction.tapService.split(".")[1], null,
|
||||
uiAction.tapServiceData));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
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: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleHold() {
|
||||
switch (uiAction.holdAction) {
|
||||
case EntityUIAction.toggle: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.callService: {
|
||||
if (uiAction.holdService != null) {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent(uiAction.holdService.split(".")[0],
|
||||
uiAction.holdService.split(".")[1], null,
|
||||
uiAction.holdServiceData));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
32
lib/entity_class/fan_entity.class.dart
Normal file
@ -0,0 +1,32 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class FanEntity extends Entity {
|
||||
|
||||
static const SUPPORT_SET_SPEED = 1;
|
||||
static const SUPPORT_OSCILLATE = 2;
|
||||
static const SUPPORT_DIRECTION = 4;
|
||||
|
||||
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportSetSpeed => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_SET_SPEED) ==
|
||||
FanEntity.SUPPORT_SET_SPEED);
|
||||
bool get supportOscillate => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_OSCILLATE) ==
|
||||
FanEntity.SUPPORT_OSCILLATE);
|
||||
bool get supportDirection => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_DIRECTION) ==
|
||||
FanEntity.SUPPORT_DIRECTION);
|
||||
|
||||
List<String> get speedList => getStringListAttributeValue("speed_list");
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return SwitchStateWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return FanControlsWidget();
|
||||
}
|
||||
}
|
44
lib/entity_class/group_entity.class.dart
Normal file
@ -0,0 +1,44 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class GroupEntity extends Entity {
|
||||
|
||||
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) {
|
||||
return SwitchStateWidget(
|
||||
domainForService: "homeassistant",
|
||||
);
|
||||
} else {
|
||||
return super._buildStatePart(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
if (_isOneDomain()) {
|
||||
mutualDomain = attributes['entity_id'][0].split(".")[0];
|
||||
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isOneDomain() {
|
||||
bool result = false;
|
||||
if (attributes['entity_id'] != null && attributes['entity_id'] is List && attributes['entity_id'].isNotEmpty) {
|
||||
String firstChildDomain = attributes['entity_id'][0].split(".")[0];
|
||||
result = true;
|
||||
attributes['entity_id'].forEach((childEntityId){
|
||||
if (childEntityId.split(".")[0] != firstChildDomain) {
|
||||
result = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -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));
|
||||
List<String> get effectList => _getEffectList();
|
||||
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;
|
||||
}
|
||||
@ -55,15 +62,6 @@ class LightEntity extends Entity {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _getEffectList() {
|
||||
if (attributes["effect_list"] != null) {
|
||||
List<String> result = (attributes["effect_list"] as List).cast<String>();
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return SwitchStateWidget();
|
||||
@ -71,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();
|
||||
|
21
lib/entity_class/lock_entity.class.dart
Normal file
@ -0,0 +1,21 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class LockEntity extends Entity {
|
||||
LockEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get isLocked => state == "locked";
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return LockStateWidget(
|
||||
assumedState: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildStatePartForPage(BuildContext context) {
|
||||
return LockStateWidget(
|
||||
assumedState: true,
|
||||
);
|
||||
}
|
||||
}
|
@ -20,58 +20,61 @@ 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);
|
||||
|
||||
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
|
||||
List<String> get sourceList => getStringListAttributeValue("source_list");
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return MediaPlayerControls();
|
||||
|
@ -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,58 +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 "automation":
|
||||
case 'lock': {
|
||||
return LockEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
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, 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, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,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) {
|
||||
@ -126,7 +154,7 @@ class EntityCollection {
|
||||
List<Entity> groups = [];
|
||||
List<Entity> nonGroupEntities = [];
|
||||
_allEntities.forEach((id, entity){
|
||||
if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) {
|
||||
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
|
||||
groups.add(entity);
|
||||
}
|
||||
if (!entity.isGroup) {
|
||||
|
50
lib/entity_widgets/button_entity_container.dart
Normal file
@ -0,0 +1,50 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ButtonEntityContainer extends StatelessWidget {
|
||||
|
||||
ButtonEntityContainer({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@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(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitHeight,
|
||||
child: EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
size: Sizes.iconSize,
|
||||
)
|
||||
),
|
||||
),
|
||||
_buildName()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName() {
|
||||
return EntityName(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
wordsWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: Sizes.nameFontSize,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,56 +7,58 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
final String value;
|
||||
final double captionFontSize;
|
||||
final double valueFontSize;
|
||||
final double bottomPadding;
|
||||
final onChange;
|
||||
final EdgeInsets padding;
|
||||
|
||||
ModeSelectorWidget({
|
||||
Key key,
|
||||
this.caption,
|
||||
@required this.caption,
|
||||
@required this.options,
|
||||
this.value,
|
||||
@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,)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,33 +6,48 @@ class ModeSwitchWidget extends StatelessWidget {
|
||||
final onChange;
|
||||
final double captionFontSize;
|
||||
final bool value;
|
||||
final bool expanded;
|
||||
final EdgeInsets padding;
|
||||
|
||||
ModeSwitchWidget({
|
||||
Key key,
|
||||
@required this.caption,
|
||||
@required this.onChange,
|
||||
this.captionFontSize,
|
||||
this.value
|
||||
this.value,
|
||||
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>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
"$caption",
|
||||
style: TextStyle(
|
||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCaption() {
|
||||
Widget captionWidget = Text(
|
||||
"$caption",
|
||||
style: TextStyle(
|
||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||
),
|
||||
);
|
||||
if (expanded) {
|
||||
return Expanded(
|
||||
child: captionWidget,
|
||||
);
|
||||
}
|
||||
return captionWidget;
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
),
|
||||
|
123
lib/entity_widgets/controls/fan_controls.dart
Normal file
@ -0,0 +1,123 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class FanControlsWidget extends StatefulWidget {
|
||||
|
||||
@override
|
||||
_FanControlsWidgetState createState() => _FanControlsWidgetState();
|
||||
|
||||
}
|
||||
|
||||
class _FanControlsWidgetState extends State<FanControlsWidget> {
|
||||
|
||||
bool _tmpOscillate;
|
||||
bool _tmpDirectionForward;
|
||||
bool _changedHere = false;
|
||||
String _tmpSpeed;
|
||||
|
||||
void _resetState(FanEntity entity) {
|
||||
_tmpOscillate = entity.attributes["oscillating"] ?? false;
|
||||
_tmpDirectionForward = entity.attributes["direction"] == "forward";
|
||||
_tmpSpeed = entity.attributes["speed"];
|
||||
}
|
||||
|
||||
void _setOscillate(FanEntity entity, bool oscillate) {
|
||||
setState(() {
|
||||
_tmpOscillate = oscillate;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
"fan", "oscillate", entity.entityId,
|
||||
{"oscillating": oscillate}));
|
||||
});
|
||||
}
|
||||
|
||||
void _setDirection(FanEntity entity, bool forward) {
|
||||
setState(() {
|
||||
_tmpDirectionForward = forward;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
"fan", "set_direction", entity.entityId,
|
||||
{"direction": forward ? "forward" : "reverse"}));
|
||||
});
|
||||
}
|
||||
|
||||
void _setSpeed(FanEntity entity, String value) {
|
||||
setState(() {
|
||||
_tmpSpeed = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
"fan", "set_speed", entity.entityId,
|
||||
{"speed": value}));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final FanEntity entity = entityModel.entityWrapper.entity;
|
||||
if (!_changedHere) {
|
||||
_resetState(entity);
|
||||
} else {
|
||||
_changedHere = false;
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
_buildSpeedControl(entity),
|
||||
_buildOscillateControl(entity),
|
||||
_buildDirectionControl(entity)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpeedControl(FanEntity entity) {
|
||||
if (entity.supportSetSpeed && entity.speedList != null && entity.speedList.isNotEmpty) {
|
||||
return ModeSelectorWidget(
|
||||
onChange: (effect) => _setSpeed(entity, effect),
|
||||
caption: "Speed",
|
||||
options: entity.speedList,
|
||||
value: _tmpSpeed
|
||||
);
|
||||
} else {
|
||||
return Container(width: 0.0, height: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOscillateControl(FanEntity entity) {
|
||||
if (entity.supportOscillate) {
|
||||
return ModeSwitchWidget(
|
||||
onChange: (value) => _setOscillate(entity, value),
|
||||
caption: "Oscillate",
|
||||
value: _tmpOscillate,
|
||||
expanded: false,
|
||||
);
|
||||
} else {
|
||||
return Container(width: 0.0, height: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDirectionControl(FanEntity entity) {
|
||||
if (entity.supportDirection) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
onPressed: _tmpDirectionForward ?
|
||||
() => _setDirection(entity, false) :
|
||||
null,
|
||||
icon: Icon(Icons.rotate_left),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: !_tmpDirectionForward ?
|
||||
() => _setDirection(entity, true) :
|
||||
null,
|
||||
icon: Icon(Icons.rotate_right),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Container(width: 0.0, height: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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,17 +73,19 @@ 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(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Image(
|
||||
image: CachedNetworkImageProvider("$homeAssistantWebHost${entity.entityPicture}"),
|
||||
height: 240.0,
|
||||
width: 320.0,
|
||||
fit: BoxFit.contain,
|
||||
Flexible(
|
||||
child: Image(
|
||||
image: CachedNetworkImageProvider("${entity.entityPicture}"),
|
||||
height: 240.0,
|
||||
//width: 320.0,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -93,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"),
|
||||
)
|
||||
@ -118,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));
|
||||
@ -132,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));
|
||||
@ -225,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))
|
||||
)
|
||||
@ -255,6 +257,8 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
|
||||
double _newVolumeLevel;
|
||||
bool _changedHere = false;
|
||||
String _newSoundMode;
|
||||
String _newSource;
|
||||
|
||||
void _setVolume(double value, String entityId) {
|
||||
setState(() {
|
||||
@ -276,6 +280,22 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
eventBus.fire(ServiceCallEvent("media_player", "volume_down", entityId, null));
|
||||
}
|
||||
|
||||
void _setSoundMode(String value, String entityId) {
|
||||
setState(() {
|
||||
_newSoundMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(ServiceCallEvent("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"}));
|
||||
});
|
||||
}
|
||||
|
||||
void _setSource(String source, String entityId) {
|
||||
setState(() {
|
||||
_newSource = source;
|
||||
_changedHere = true;
|
||||
eventBus.fire(ServiceCallEvent("media_player", "select_source", entityId, {"source": "$source"}));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final MediaPlayerEntity entity = EntityModel.of(context).entityWrapper.entity;
|
||||
@ -287,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 {
|
||||
@ -302,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)
|
||||
)
|
||||
],
|
||||
@ -347,6 +367,38 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
));
|
||||
}
|
||||
|
||||
if (entity.supportSelectSoundMode && entity.soundModeList != null) {
|
||||
if (!_changedHere) {
|
||||
_newSoundMode = entity.attributes["sound_mode"];
|
||||
} else {
|
||||
_changedHere = false;
|
||||
}
|
||||
children.add(
|
||||
ModeSelectorWidget(
|
||||
options: entity.soundModeList,
|
||||
caption: "Sound mode",
|
||||
value: _newSoundMode,
|
||||
onChange: (value) => _setSoundMode(value, entity.entityId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (entity.supportSelectSource && entity.sourceList != null) {
|
||||
if (!_changedHere) {
|
||||
_newSource = entity.attributes["source"];
|
||||
} else {
|
||||
_changedHere = false;
|
||||
}
|
||||
children.add(
|
||||
ModeSelectorWidget(
|
||||
options: entity.sourceList,
|
||||
caption: "Source",
|
||||
value: _newSource,
|
||||
onChange: (value) => _setSource(value, entity.entityId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
return Column(
|
||||
children: children,
|
||||
|
@ -10,15 +10,56 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
EntityIcon(),
|
||||
Expanded(
|
||||
child: EntityName(),
|
||||
),
|
||||
state
|
||||
],
|
||||
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) {
|
||||
entityModel.entityWrapper.handleHold();
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleTap();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
EntityIcon(),
|
||||
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 3,
|
||||
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,66 +3,72 @@ 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 entityModel = EntityModel.of(context);
|
||||
return GestureDetector(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||
entityModel.entityWrapper,
|
||||
iconSize,
|
||||
EntityColor.stateColor(entityModel.entityWrapper.entity.state)
|
||||
),
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: buildIcon(
|
||||
entityWrapper,
|
||||
color ?? EntityColor.stateColor(entityWrapper.entity.state)
|
||||
),
|
||||
onLongPress: () {
|
||||
if (entityModel.handleTap) {
|
||||
switch (entityModel.entityWrapper.holdAction) {
|
||||
case EntityTapAction.toggle: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent("homeassistant", "toggle", entityModel.entityWrapper.entity.entityId, null));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entityModel.entityWrapper.entity));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (entityModel.handleTap) {
|
||||
switch (entityModel.entityWrapper.tapAction) {
|
||||
case EntityTapAction.toggle: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent("homeassistant", "toggle", entityModel.entityWrapper.entity.entityId, null));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityTapAction.callService: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent(entityModel.entityWrapper.actionService.split(".")[0], entityModel.entityWrapper.actionService.split(".")[1], null, entityModel.entityWrapper.actionServiceData));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entityModel.entityWrapper.entity));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
}
|
@ -7,27 +7,27 @@ class EntityName extends StatelessWidget {
|
||||
final bool wordsWrap;
|
||||
final double fontSize;
|
||||
final TextAlign textAlign;
|
||||
final int maxLines;
|
||||
|
||||
const EntityName({Key key, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
|
||||
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return GestureDetector(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.displayName}",
|
||||
overflow: textOverflow,
|
||||
softWrap: wordsWrap,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
textAlign: textAlign,
|
||||
),
|
||||
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(
|
||||
"${entityWrapper.displayName}",
|
||||
overflow: textOverflow,
|
||||
softWrap: wordsWrap,
|
||||
maxLines: maxLines,
|
||||
style: textStyle,
|
||||
textAlign: textAlign,
|
||||
),
|
||||
onTap: () =>
|
||||
entityModel.handleTap
|
||||
? eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity))
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
@ -4,39 +4,87 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
|
||||
final bool showName;
|
||||
final bool showState;
|
||||
final bool nameInTheBottom;
|
||||
final double iconSize;
|
||||
final double nameFontSize;
|
||||
final bool wordsWrapInName;
|
||||
|
||||
GlanceEntityContainer({
|
||||
Key key, @required this.showName, @required this.showState,
|
||||
Key key,
|
||||
@required this.showName,
|
||||
@required this.showState,
|
||||
this.nameInTheBottom: false,
|
||||
this.iconSize: Sizes.iconSize,
|
||||
this.nameFontSize: Sizes.smallFontSize,
|
||||
this.wordsWrapInName: false
|
||||
}) : super(key: key);
|
||||
|
||||
@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 (showName) {
|
||||
result.add(EntityName(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
wordsWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: Sizes.smallFontSize,
|
||||
));
|
||||
if (!nameInTheBottom) {
|
||||
if (showName) {
|
||||
result.add(_buildName());
|
||||
}
|
||||
} else {
|
||||
if (showState) {
|
||||
result.add(_buildState());
|
||||
}
|
||||
}
|
||||
result.add(EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
iconSize: Sizes.largeIconSize,
|
||||
));
|
||||
if (showState) {
|
||||
result.add(SimpleEntityState(
|
||||
textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
));
|
||||
result.add(
|
||||
EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
)
|
||||
);
|
||||
if (!nameInTheBottom) {
|
||||
if (showState) {
|
||||
result.add(_buildState());
|
||||
}
|
||||
} else {
|
||||
result.add(_buildName());
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: result,
|
||||
|
||||
return Center(
|
||||
child: InkResponse(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
//mainAxisAlignment: MainAxisAlignment.start,
|
||||
//crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: result,
|
||||
),
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName() {
|
||||
return EntityName(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
wordsWrap: wordsWrapInName,
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: nameFontSize,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildState() {
|
||||
return SimpleEntityState(
|
||||
textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,50 +8,51 @@ 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(
|
||||
0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
||||
child: GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("${entity.state}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: Sizes.stateFontSize,
|
||||
)),
|
||||
Text(" $targetTemp",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
))
|
||||
],
|
||||
),
|
||||
entity.attributes["current_temperature"] != null ?
|
||||
Text("Currently: ${entity.attributes["current_temperature"]}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("$displayState",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: Sizes.stateFontSize,
|
||||
color: Colors.black45)
|
||||
) :
|
||||
Container(height: 0.0,)
|
||||
],
|
||||
),
|
||||
onTap: () => entityModel.handleTap
|
||||
? eventBus.fire(new ShowEntityPageEvent(entity))
|
||||
: null,
|
||||
)),
|
||||
Text(" $targetTemp",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
))
|
||||
],
|
||||
),
|
||||
entity.currentTemperature != null ?
|
||||
Text("Currently: ${entity.currentTemperature}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
color: Colors.black45)
|
||||
) :
|
||||
Container(height: 0.0,)
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
66
lib/entity_widgets/state/lock_state.dart
Normal file
@ -0,0 +1,66 @@
|
||||
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));
|
||||
}
|
||||
|
||||
void _unlock(Entity entity) {
|
||||
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final LockEntity entity = entityModel.entityWrapper.entity;
|
||||
if (assumedState) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 34.0,
|
||||
child: FlatButton(
|
||||
onPressed: () => _unlock(entity),
|
||||
child: Text("UNLOCK",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
),
|
||||
)
|
||||
),
|
||||
SizedBox(
|
||||
height: 34.0,
|
||||
child: FlatButton(
|
||||
onPressed: () => _lock(entity),
|
||||
child: Text("LOCK",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
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),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ class _SelectStateWidgetState extends State<SelectStateWidget> {
|
||||
if (entity.listOptions.isNotEmpty) {
|
||||
ctrl = DropdownButton<String>(
|
||||
value: entity.state,
|
||||
isExpanded: true,
|
||||
items: entity.listOptions.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
@ -36,7 +37,9 @@ class _SelectStateWidgetState extends State<SelectStateWidget> {
|
||||
} else {
|
||||
ctrl = Text('---');
|
||||
}
|
||||
return Expanded(
|
||||
return Flexible(
|
||||
flex: 2,
|
||||
fit: FlexFit.tight,
|
||||
//width: Entity.INPUT_WIDTH,
|
||||
child: ctrl,
|
||||
);
|
||||
|
@ -5,32 +5,45 @@ class SimpleEntityState extends StatelessWidget {
|
||||
final bool expanded;
|
||||
final TextAlign textAlign;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final int maxLines;
|
||||
final String customValue;
|
||||
|
||||
const SimpleEntityState({Key key, 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: GestureDetector(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.state}${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
||||
textAlign: textAlign,
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
)),
|
||||
onTap: () => entityModel.handleTap
|
||||
? eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity))
|
||||
: null,
|
||||
)
|
||||
padding: padding,
|
||||
child: Text(
|
||||
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: textStyle
|
||||
)
|
||||
);
|
||||
if (expanded) {
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.3,
|
||||
return Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 2,
|
||||
child: result,
|
||||
);
|
||||
} else {
|
||||
|
@ -1,6 +1,11 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class SwitchStateWidget extends StatefulWidget {
|
||||
|
||||
final String domainForService;
|
||||
|
||||
const SwitchStateWidget({Key key, this.domainForService}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SwitchStateWidgetState createState() => _SwitchStateWidgetState();
|
||||
}
|
||||
@ -24,11 +29,17 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
|
||||
setState(() {
|
||||
newState = entity.state;
|
||||
updatedHere = true;
|
||||
TheLogger.debug("Timer@!!");
|
||||
//TheLogger.debug("Timer@!!");
|
||||
});
|
||||
});
|
||||
String domain;
|
||||
if (widget.domainForService != null) {
|
||||
domain = widget.domainForService;
|
||||
} else {
|
||||
domain = entity.domain;
|
||||
}
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
|
||||
domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -60,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
|
||||
)
|
||||
|
@ -66,7 +66,9 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
||||
_tmpValue = entity.state;
|
||||
}
|
||||
if (entity.isTextField || entity.isPasswordField) {
|
||||
return Expanded(
|
||||
return Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 2,
|
||||
//width: Entity.INPUT_WIDTH,
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
@ -83,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,36 +295,91 @@ class HomeAssistant {
|
||||
List<HACard> _createLovelaceCards(List rawCards) {
|
||||
List<HACard> result = [];
|
||||
rawCards.forEach((rawCard){
|
||||
if (rawCard["cards"] != null) {
|
||||
result.addAll(_createLovelaceCards(rawCard["cards"]));
|
||||
} else {
|
||||
try {
|
||||
bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
HACard card = HACard(
|
||||
id: "card",
|
||||
name: rawCard["title"],
|
||||
type: rawCard['type'],
|
||||
columnsCount: rawCard['columns'] ?? 4,
|
||||
showName: rawCard['show_name'] ?? true,
|
||||
showState: rawCard['show_state'] ?? true,
|
||||
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)));
|
||||
}
|
||||
} else {
|
||||
if (entities.isExist(rawEntity["entity"])) {
|
||||
if (rawEntity["type"] == "divider") {
|
||||
card.entities.add(EntityWrapper(entity: Entity.divider()));
|
||||
} else if (rawEntity["type"] == "section") {
|
||||
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
||||
} else if (rawEntity["type"] == "call-service") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.callService,
|
||||
"service": rawEntity["service"],
|
||||
"service_data": rawEntity["service_data"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
card.entities.add(EntityWrapper(
|
||||
entity: Entity.callService(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
service: rawEntity["service"],
|
||||
actionName: rawEntity["action_name"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (rawEntity["type"] == "weblink") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.navigate,
|
||||
"service": rawEntity["url"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
card.entities.add(EntityWrapper(
|
||||
entity: Entity.weblink(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
url: rawEntity["url"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = entities.get(rawEntity["entity"]);
|
||||
card.entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
displayName: rawEntity["name"],
|
||||
icon: rawEntity["icon"],
|
||||
tapAction: rawEntity["tap_action"] ?? EntityTapAction.moreInfo,
|
||||
holdAction: rawEntity["hold_action"] ?? EntityTapAction.moreInfo,
|
||||
actionService: rawEntity["service"],
|
||||
actionServiceData: rawEntity["service_data"] ?? {"entity_id": e.entityId}
|
||||
entity: e,
|
||||
displayName: rawEntity["name"],
|
||||
icon: rawEntity["icon"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -465,41 +387,45 @@ class HomeAssistant {
|
||||
var en = rawCard["entity"];
|
||||
if (en is String) {
|
||||
if (entities.isExist(en)) {
|
||||
card.linkedEntity = EntityWrapper(entity: entities.get(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"])) {
|
||||
card.linkedEntity = EntityWrapper(
|
||||
entity: entities.get(en["entity"]),
|
||||
icon: en["icon"],
|
||||
displayName: en["name"]
|
||||
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"]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
result.add(card);
|
||||
} catch (e) {
|
||||
Logger.e("There was an error parsing card: ${e.toString()}");
|
||||
}
|
||||
});
|
||||
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(
|
||||
@ -529,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 = [];
|
||||
@ -599,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
|
||||
|
795
lib/main.dart
1026
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);
|
||||
}
|
||||
}
|
||||
@ -97,6 +91,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
children: <Widget>[
|
||||
Text(
|
||||
@ -149,33 +144,9 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
_newHassioPort = value;
|
||||
}
|
||||
),
|
||||
new Row(
|
||||
children: [
|
||||
Text("Login with access token (HA >= 0.78.0)"),
|
||||
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),
|
||||
|
@ -2,76 +2,49 @@ part of '../main.dart';
|
||||
|
||||
class HACard {
|
||||
List<EntityWrapper> entities = [];
|
||||
EntityWrapper linkedEntity;
|
||||
List<HACard> childCards = [];
|
||||
EntityWrapper linkedEntityWrapper;
|
||||
String name;
|
||||
String id;
|
||||
String type;
|
||||
bool showName;
|
||||
bool showState;
|
||||
bool showEmpty;
|
||||
int columnsCount;
|
||||
List stateFilter;
|
||||
List states;
|
||||
String content;
|
||||
|
||||
HACard({
|
||||
this.name,
|
||||
this.id,
|
||||
this.linkedEntity,
|
||||
this.linkedEntityWrapper,
|
||||
this.columnsCount: 4,
|
||||
this.showName: true,
|
||||
this.showState: true,
|
||||
this.stateFilter: const [],
|
||||
this.showEmpty: true,
|
||||
this.content,
|
||||
this.states,
|
||||
@required this.type
|
||||
});
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
switch (type) {
|
||||
|
||||
case "entities": {
|
||||
return EntitiesCardWidget(
|
||||
card: this,
|
||||
);
|
||||
}
|
||||
|
||||
case "glance": {
|
||||
return GlanceCardWidget(
|
||||
card: this,
|
||||
);
|
||||
}
|
||||
|
||||
case "media-control": {
|
||||
return MediaControlCardWidget(
|
||||
card: this,
|
||||
);
|
||||
}
|
||||
|
||||
case "weather-forecast":
|
||||
case "thermostat":
|
||||
case "sensor":
|
||||
case "plant-status":
|
||||
case "picture-entity":
|
||||
case "picture-elements":
|
||||
case "picture":
|
||||
case "map":
|
||||
case "iframe":
|
||||
case "gauge":
|
||||
case "entity-button":
|
||||
case "conditional":
|
||||
case "alarm-panel": {
|
||||
return UnsupportedCardWidget(
|
||||
card: this,
|
||||
);
|
||||
}
|
||||
|
||||
default: {
|
||||
if ((linkedEntity == null) && (entities.isNotEmpty)) {
|
||||
return EntitiesCardWidget(
|
||||
card: this,
|
||||
);
|
||||
} else {
|
||||
return UnsupportedCardWidget(
|
||||
card: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<EntityWrapper> getEntitiesToShow() {
|
||||
return entities.where((entityWrapper) {
|
||||
if (entityWrapper.entity.isHidden) {
|
||||
return false;
|
||||
}
|
||||
if (stateFilter.isNotEmpty) {
|
||||
return stateFilter.contains(entityWrapper.entity.state);
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return CardWidget(
|
||||
card: this,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
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,13 +1,14 @@
|
||||
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;
|
||||
static const largeIconSize = 34.0;
|
||||
static const stateFontSize = 16.0;
|
||||
static const nameFontSize = 16.0;
|
||||
static const largeIconSize = 46.0;
|
||||
static const stateFontSize = 15.0;
|
||||
static const nameFontSize = 15.0;
|
||||
static const smallFontSize = 14.0;
|
||||
static const largeFontSize = 24.0;
|
||||
static const inputWidth = 160.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();
|
||||
}
|
||||
|
||||
}
|
@ -28,8 +28,8 @@ class HAView {
|
||||
HACard card = HACard(
|
||||
name: e.displayName,
|
||||
id: e.entityId,
|
||||
linkedEntity: EntityWrapper(entity: e),
|
||||
type: "media-control"
|
||||
linkedEntityWrapper: EntityWrapper(entity: e),
|
||||
type: CardType.mediaControl
|
||||
);
|
||||
cards.add(card);
|
||||
});
|
||||
@ -40,7 +40,7 @@ class HAView {
|
||||
HACard card = HACard(
|
||||
id: groupIdToAdd,
|
||||
name: entity.domain,
|
||||
type: "entities"
|
||||
type: CardType.entities
|
||||
);
|
||||
card.entities.add(EntityWrapper(entity: entity));
|
||||
autoGeneratedCards.add(card);
|
||||
@ -51,16 +51,16 @@ class HAView {
|
||||
HACard card = HACard(
|
||||
name: entity.displayName,
|
||||
id: entity.entityId,
|
||||
linkedEntity: EntityWrapper(entity: entity),
|
||||
type: "entities"
|
||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
||||
type: CardType.entities
|
||||
);
|
||||
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
|
||||
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
|
||||
HACard mediaCard = HACard(
|
||||
name: entity.displayName,
|
||||
id: entity.entityId,
|
||||
linkedEntity: EntityWrapper(entity: entity),
|
||||
type: "media-control"
|
||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
||||
type: CardType.mediaControl
|
||||
);
|
||||
cards.add(mediaCard);
|
||||
});
|
||||
@ -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,18 +3,22 @@ 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,
|
||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 25.0)),
|
||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
||||
);
|
||||
} else {
|
||||
result = new Container(width: 0.0, height: 0.0);
|
||||
|
292
lib/ui_widgets/card_widget.dart
Normal file
@ -0,0 +1,292 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class CardWidget extends StatelessWidget {
|
||||
|
||||
final HACard card;
|
||||
|
||||
const CardWidget({
|
||||
Key key,
|
||||
this.card
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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) {
|
||||
|
||||
case CardType.entities: {
|
||||
return _buildEntitiesCard(context);
|
||||
}
|
||||
|
||||
case CardType.glance: {
|
||||
return _buildGlanceCard(context);
|
||||
}
|
||||
|
||||
case CardType.mediaControl: {
|
||||
return _buildMediaControlsCard(context);
|
||||
}
|
||||
|
||||
case CardType.entityButton: {
|
||||
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) {
|
||||
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
|
||||
children.add(
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: card.build(context),
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
case CardType.verticalStack: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
children.add(
|
||||
card.build(context)
|
||||
);
|
||||
});
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
default: {
|
||||
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
|
||||
return _buildEntitiesCard(context);
|
||||
} else {
|
||||
return _buildUnsupportedCard(context);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildEntitiesCard(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name));
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
if (!entity.entity.isHidden) {
|
||||
body.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
handleTap: true,
|
||||
child: entity.entity.buildDefaultWidget(context)
|
||||
),
|
||||
));
|
||||
}
|
||||
});
|
||||
return Card(
|
||||
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
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGlanceCard(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> rows = [];
|
||||
rows.add(CardHeaderWidget(name: card.name));
|
||||
|
||||
List<Widget> result = [];
|
||||
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
result.add(
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1/columnsCount,
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: GlanceEntityContainer(
|
||||
showName: card.showName,
|
||||
showState: card.showState,
|
||||
),
|
||||
handleTap: true
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
rows.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
|
||||
child: Wrap(
|
||||
//alignment: WrapAlignment.spaceAround,
|
||||
runSpacing: Sizes.rowPadding*2,
|
||||
children: result,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return Card(
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaControlsCard(BuildContext context) {
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
handleTap: null,
|
||||
child: MediaPlayerWidget()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntityButtonCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
||||
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: ButtonEntityContainer(),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnsupportedCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name ?? ""));
|
||||
List<Widget> result = [];
|
||||
if (card.linkedEntityWrapper != null) {
|
||||
result.addAll(<Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
handleTap: true,
|
||||
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
|
||||
),
|
||||
)
|
||||
]);
|
||||
} else {
|
||||
result.addAll(<Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Text("'${card.type}' card is not supported yet"),
|
||||
),
|
||||
]);
|
||||
}
|
||||
body.addAll(result);
|
||||
return Card(
|
||||
child: new Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: body
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
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();
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class EntitiesCardWidget extends StatelessWidget {
|
||||
|
||||
final HACard card;
|
||||
|
||||
const EntitiesCardWidget({
|
||||
Key key,
|
||||
this.card
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if ((card.linkedEntity!= null) && (card.linkedEntity.entity.isHidden)) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name));
|
||||
body.addAll(_buildCardBody(context));
|
||||
return Card(
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildCardBody(BuildContext context) {
|
||||
List<Widget> result = [];
|
||||
card.entities.forEach((EntityWrapper entity) {
|
||||
if (!entity.entity.isHidden) {
|
||||
result.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
handleTap: true,
|
||||
child: entity.entity.buildDefaultWidget(context)
|
||||
),
|
||||
));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class GlanceCardWidget extends StatelessWidget {
|
||||
|
||||
final HACard card;
|
||||
|
||||
const GlanceCardWidget({
|
||||
Key key,
|
||||
this.card
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if ((card.linkedEntity!= null) && (card.linkedEntity.entity.isHidden)) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
List<Widget> rows = [];
|
||||
rows.add(CardHeaderWidget(name: card.name));
|
||||
rows.add(_buildRows(context));
|
||||
return Card(
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRows(BuildContext context) {
|
||||
List<Widget> result = [];
|
||||
double width = MediaQuery.of(context).size.width - Sizes.leftWidgetPadding - (2*Sizes.rightWidgetPadding);
|
||||
List<EntityWrapper> toShow = card.entities.where((entity) {return !entity.entity.isHidden;}).toList();
|
||||
int columnsCount = toShow.length >= card.columnsCount ? card.columnsCount : toShow.length;
|
||||
card.entities.forEach((EntityWrapper entity) {
|
||||
if (!entity.entity.isHidden) {
|
||||
result.add(
|
||||
SizedBox(
|
||||
width: width / columnsCount,
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: entity.entity.buildGlanceWidget(context, card.showName, card.showState),
|
||||
handleTap: true
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceAround,
|
||||
runSpacing: Sizes.rowPadding*2,
|
||||
children: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MediaControlCardWidget extends StatelessWidget {
|
||||
|
||||
final HACard card;
|
||||
|
||||
const MediaControlCardWidget({
|
||||
Key key,
|
||||
this.card
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if ((card.linkedEntity == null) || (card.linkedEntity.entity.isHidden)) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntity,
|
||||
handleTap: null,
|
||||
child: MediaPlayerWidget()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|