Compare commits
192 Commits
Author | SHA1 | Date | |
---|---|---|---|
7beab9ae93 | |||
8c0d1f90a3 | |||
05c05ba768 | |||
67e885e76a | |||
594bce0b8d | |||
7f6569e0db | |||
1c829c8364 | |||
7ca4b02e6d | |||
fadfefd836 | |||
37155901ef | |||
fbbb96409d | |||
5126c54914 | |||
916d0b7e3c | |||
0815840a9c | |||
bc237796b2 | |||
7f44800f64 | |||
85ac746e9d | |||
8215175098 | |||
39ee8b1799 | |||
c76d3d68c8 | |||
cde257922b | |||
be0c9d3372 | |||
66cd7ea307 | |||
b704ce6984 | |||
247c856a41 | |||
9afaebfa12 | |||
929abea5d3 | |||
5c31ddd00f | |||
8f55be187d | |||
1fe82d8b0d | |||
cbc56a8105 | |||
b63cddfa46 | |||
91db82f730 | |||
0c4d1b78ff | |||
5af2fd0562 | |||
2375543ebf | |||
de187f3ed5 | |||
9266ffacf3 | |||
3c0ca5d16d | |||
caabf25260 | |||
0af2afbb80 | |||
12d226509d | |||
3417c38426 | |||
c7fc5afbb8 | |||
11f565a9dc | |||
53240faac3 | |||
95d4878785 | |||
ef15026203 | |||
ad6355503b | |||
491c2b0dc0 | |||
5b99ade088 | |||
e1d9d9f304 | |||
209ccd4f7f | |||
5a8a207f2e | |||
19c85d9c16 | |||
a916ddfa50 | |||
8c1ad9c7f9 | |||
93af1eca7e | |||
cabf836fa3 | |||
15b3d31a6f | |||
9b98689012 | |||
84ebd0c33c | |||
ccd7774931 | |||
b2773635f5 | |||
8b046b7313 | |||
885a516676 | |||
921b0e09b0 | |||
277c67fc6f | |||
2a01ff8a03 | |||
b246b7bc1d | |||
e1868b9a14 | |||
125f3ac16c | |||
be502b5668 | |||
6f33fdca9f | |||
a7cda2a35e | |||
102b10ade0 | |||
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 |
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
1232
android/hs_err_pid766.log
Normal file
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 24 KiB |
45
lib/auth_manager.class.dart
Normal file
@ -0,0 +1,45 @@
|
||||
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) {
|
||||
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("Got temp 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");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
}
|
416
lib/connection.class.dart
Normal file
@ -0,0 +1,416 @@
|
||||
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((_) {
|
||||
completer?.complete();
|
||||
}).catchError((e) {
|
||||
completer?.completeError(e);
|
||||
});
|
||||
} else {
|
||||
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...");
|
||||
_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((_) {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool("oauth-used", true);
|
||||
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] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||
_socket.sink.add(rawMessage);
|
||||
}).catchError((e) {
|
||||
_completer.completeError(e);
|
||||
});
|
||||
} else {
|
||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : 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] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
|
||||
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] <== HTTP ${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] ==> HTTP $endPoint");
|
||||
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] <== HTTP ${response.statusCode}");
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of 'main.dart';
|
||||
|
||||
class EntityState {
|
||||
static const on = 'on';
|
||||
@ -28,6 +28,7 @@ class EntityState {
|
||||
static const unavailable = 'unavailable';
|
||||
static const ok = 'ok';
|
||||
static const problem = 'problem';
|
||||
static const active = 'active';
|
||||
}
|
||||
|
||||
class EntityUIAction {
|
||||
@ -94,4 +95,5 @@ class CardType {
|
||||
static const entityButton = "entity-button";
|
||||
static const conditional = "conditional";
|
||||
static const alarmPanel = "alarm-panel";
|
||||
static const markdown = "markdown";
|
||||
}
|
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}";
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
]
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
27
lib/entities/automation/automation_entity.class.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",
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
16
lib/entities/button/button_entity.class.dart
Normal file
@ -0,0 +1,16 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class ButtonEntity extends Entity {
|
||||
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return FlatServiceButton(
|
||||
entityId: entityId,
|
||||
serviceDomain: domain,
|
||||
serviceName: 'turn_on',
|
||||
text: domain == "scene" ? "ACTIVATE" : "EXECUTE",
|
||||
);
|
||||
}
|
||||
}
|
17
lib/entities/camera/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();
|
||||
}
|
||||
}
|
114
lib/entities/climate/climate_entity.class.dart
Normal file
@ -0,0 +1,114 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class ClimateEntity extends Entity {
|
||||
|
||||
@override
|
||||
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||
chartType: EntityHistoryWidgetType.numericAttributes,
|
||||
numericState: false,
|
||||
numericAttributesToShow: ["current_temperature"]
|
||||
);
|
||||
|
||||
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
||||
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;
|
||||
|
||||
|
||||
//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 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 supportFanMode =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||
ClimateEntity.SUPPORT_FAN_MODE);
|
||||
bool get supportSwingMode =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||
ClimateEntity.SUPPORT_SWING_MODE);
|
||||
bool get supportPresetMode =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
|
||||
ClimateEntity.SUPPORT_PRESET_MODE);
|
||||
bool get supportAuxHeat =>
|
||||
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||
ClimateEntity.SUPPORT_AUX_HEAT);
|
||||
|
||||
List<String> get hvacModes => attributes["hvac_modes"] != null
|
||||
? (attributes["hvac_modes"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get fanModes => attributes["fan_modes"] != null
|
||||
? (attributes["fan_modes"] as List).cast<String>()
|
||||
: null;
|
||||
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;
|
||||
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
|
||||
double get targetHumidity => _getDoubleAttributeValue('humidity');
|
||||
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||
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 auxHeat => attributes['aux_heat'] == "on";
|
||||
|
||||
@override
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
if (supportTargetTemperature) {
|
||||
historyConfig.numericAttributesToShow.add("temperature");
|
||||
}
|
||||
if (supportTargetTemperatureRange) {
|
||||
historyConfig.numericAttributesToShow.add("target_temp_high");
|
||||
historyConfig.numericAttributesToShow.add("target_temp_low");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return ClimateStateWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return ClimateControlWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
double _getDoubleAttributeValue(String attributeName) {
|
||||
var temp1 = attributes["$attributeName"];
|
||||
if (temp1 is int) {
|
||||
return temp1.toDouble();
|
||||
} else if (temp1 is double) {
|
||||
return temp1;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class ClimateControlWidget extends StatefulWidget {
|
||||
|
||||
@ -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(),
|
||||
)
|
||||
],
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class ClimateStateWidget extends StatelessWidget {
|
||||
@override
|
||||
@ -8,13 +8,19 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
String targetTemp = "-";
|
||||
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||
targetTemp = "${entity.temperature}";
|
||||
} else if ((entity.supportTargetTemperatureLow) &&
|
||||
(entity.targetLow != null)) {
|
||||
targetTemp = "${entity.targetLow}";
|
||||
if ((entity.supportTargetTemperatureHigh) &&
|
||||
(entity.targetHigh != null)) {
|
||||
targetTemp += " - ${entity.targetHigh}";
|
||||
}
|
||||
} else if ((entity.supportTargetTemperatureRange) &&
|
||||
(entity.targetLow != null) &&
|
||||
(entity.targetHigh != null)) {
|
||||
targetTemp = "${entity.targetLow} - ${entity.targetHigh}";
|
||||
}
|
||||
String displayState = '';
|
||||
if (entity.hvacAction != null) {
|
||||
displayState = "${entity.hvacAction} (${entity.displayState})";
|
||||
} else {
|
||||
displayState = "${entity.displayState}";
|
||||
}
|
||||
if (entity.presetMode != null) {
|
||||
displayState += " - ${entity.presetMode}";
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
@ -25,7 +31,7 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("${entity.state}",
|
||||
Text("$displayState",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -38,8 +44,8 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
))
|
||||
],
|
||||
),
|
||||
entity.attributes["current_temperature"] != null ?
|
||||
Text("Currently: ${entity.attributes["current_temperature"]}",
|
||||
entity.currentTemperature != null ?
|
||||
Text("Currently: ${entity.currentTemperature}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class CoverEntity extends Entity {
|
||||
|
||||
@ -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,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class CoverControlWidget extends StatefulWidget {
|
||||
|
||||
@ -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,
|
||||
),
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class CoverStateWidget extends StatelessWidget {
|
||||
void _open(CoverEntity entity) {
|
||||
@ -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));
|
@ -1,6 +1,8 @@
|
||||
part of '../main.dart';
|
||||
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,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class DateTimeStateWidget extends StatelessWidget {
|
||||
@override
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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,65 @@ 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
|
||||
@ -35,24 +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) {
|
||||
@ -111,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,6 +4,7 @@ class EntityWrapper {
|
||||
|
||||
String displayName;
|
||||
String icon;
|
||||
String entityPicture;
|
||||
EntityUIAction uiAction;
|
||||
Entity entity;
|
||||
|
||||
@ -14,10 +15,15 @@ class EntityWrapper {
|
||||
String displayName,
|
||||
this.uiAction
|
||||
}) {
|
||||
this.icon = icon ?? entity.icon;
|
||||
this.displayName = displayName ?? entity.displayName;
|
||||
if (this.uiAction == null) {
|
||||
this.uiAction = EntityUIAction();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +55,16 @@ class EntityWrapper {
|
||||
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;
|
||||
}
|
||||
@ -79,6 +95,16 @@ class EntityWrapper {
|
||||
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;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class FanEntity extends Entity {
|
||||
|
||||
@ -6,15 +6,15 @@ class FanEntity extends Entity {
|
||||
static const SUPPORT_OSCILLATE = 2;
|
||||
static const SUPPORT_DIRECTION = 4;
|
||||
|
||||
FanEntity(Map rawData) : super(rawData);
|
||||
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportSetSpeed => ((attributes["supported_features"] &
|
||||
bool get supportSetSpeed => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_SET_SPEED) ==
|
||||
FanEntity.SUPPORT_SET_SPEED);
|
||||
bool get supportOscillate => ((attributes["supported_features"] &
|
||||
bool get supportOscillate => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_OSCILLATE) ==
|
||||
FanEntity.SUPPORT_OSCILLATE);
|
||||
bool get supportDirection => ((attributes["supported_features"] &
|
||||
bool get supportDirection => ((supportedFeatures &
|
||||
FanEntity.SUPPORT_DIRECTION) ==
|
||||
FanEntity.SUPPORT_DIRECTION);
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class FanControlsWidget extends StatefulWidget {
|
||||
|
@ -1,12 +1,13 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class GroupEntity extends Entity {
|
||||
GroupEntity(Map rawData) : super(rawData);
|
||||
|
||||
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
|
||||
String mutualDomain;
|
||||
bool switchable = false;
|
||||
|
||||
GroupEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
if (switchable) {
|
||||
@ -19,8 +20,8 @@ class GroupEntity extends Entity {
|
||||
}
|
||||
|
||||
@override
|
||||
void update(Map rawData) {
|
||||
super.update(rawData);
|
||||
void update(Map rawData, String webHost) {
|
||||
super.update(rawData, webHost);
|
||||
if (_isOneDomain()) {
|
||||
mutualDomain = attributes['entity_id'][0].split(".")[0];
|
||||
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightEntity extends Entity {
|
||||
|
||||
@ -10,43 +10,50 @@ class LightEntity extends Entity {
|
||||
static const SUPPORT_TRANSITION = 32;
|
||||
static const SUPPORT_WHITE_VALUE = 128;
|
||||
|
||||
bool get supportBrightness => ((attributes["supported_features"] &
|
||||
bool get supportBrightness => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_BRIGHTNESS) ==
|
||||
LightEntity.SUPPORT_BRIGHTNESS);
|
||||
bool get supportColorTemp => ((attributes["supported_features"] &
|
||||
bool get supportColorTemp => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_COLOR_TEMP) ==
|
||||
LightEntity.SUPPORT_COLOR_TEMP);
|
||||
bool get supportEffect => ((attributes["supported_features"] &
|
||||
bool get supportEffect => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_EFFECT) ==
|
||||
LightEntity.SUPPORT_EFFECT);
|
||||
bool get supportFlash => ((attributes["supported_features"] &
|
||||
bool get supportFlash => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_FLASH) ==
|
||||
LightEntity.SUPPORT_FLASH);
|
||||
bool get supportColor => ((attributes["supported_features"] &
|
||||
bool get supportColor => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_COLOR) ==
|
||||
LightEntity.SUPPORT_COLOR);
|
||||
bool get supportTransition => ((attributes["supported_features"] &
|
||||
bool get supportTransition => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_TRANSITION) ==
|
||||
LightEntity.SUPPORT_TRANSITION);
|
||||
bool get supportWhiteValue => ((attributes["supported_features"] &
|
||||
bool get supportWhiteValue => ((supportedFeatures &
|
||||
LightEntity.SUPPORT_WHITE_VALUE) ==
|
||||
LightEntity.SUPPORT_WHITE_VALUE);
|
||||
|
||||
int get brightness => _getIntAttributeValue("brightness");
|
||||
int get whiteValue => _getIntAttributeValue("white_value");
|
||||
String get effect => attributes["effect"];
|
||||
int get colorTemp => _getIntAttributeValue("color_temp");
|
||||
double get maxMireds => _getDoubleAttributeValue("max_mireds");
|
||||
double get minMireds => _getDoubleAttributeValue("min_mireds");
|
||||
Color get color => _getColor();
|
||||
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
|
||||
HSVColor get color => _getColor();
|
||||
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
|
||||
List<String> get effectList => getStringListAttributeValue("effect_list");
|
||||
|
||||
LightEntity(Map rawData) : super(rawData);
|
||||
LightEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
Color _getColor() {
|
||||
HSVColor _getColor() {
|
||||
List hs = attributes["hs_color"];
|
||||
List rgb = attributes["rgb_color"];
|
||||
try {
|
||||
if ((rgb != null) && (rgb.length > 0)) {
|
||||
return Color.fromARGB(255, rgb[0], rgb[1], rgb[2]);
|
||||
if (hs != null && hs.isNotEmpty) {
|
||||
double sat = hs[1]/100;
|
||||
String ssat = sat.toStringAsFixed(2);
|
||||
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
|
||||
} else if (rgb != null && rgb.isNotEmpty) {
|
||||
return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2]));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -62,7 +69,7 @@ class LightEntity extends Entity {
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
if (!isAdditionalControls) {
|
||||
if (!isAdditionalControls || state == EntityState.unavailable) {
|
||||
return Container(height: 0.0, width: 0.0);
|
||||
} else {
|
||||
return LightControlsWidget();
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class LightControlsWidget extends StatefulWidget {
|
||||
|
||||
@ -10,31 +10,38 @@ 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;
|
||||
_tmpBrightness = entity.brightness ?? 1;
|
||||
_tmpWhiteValue = entity.whiteValue ?? 0;
|
||||
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
|
||||
_tmpColor = entity.color ?? _tmpColor;
|
||||
_tmpEffect = entity.effect;
|
||||
}
|
||||
|
||||
void _setBrightness(LightEntity entity, double value) {
|
||||
setState(() {
|
||||
_tmpBrightness = value.round();
|
||||
_changedHere = true;
|
||||
if (_tmpBrightness > 0) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"brightness": _tmpBrightness}));
|
||||
});
|
||||
}
|
||||
|
||||
void _setWhiteValue(LightEntity entity, double value) {
|
||||
setState(() {
|
||||
_tmpWhiteValue = value.round();
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"brightness": _tmpBrightness}));
|
||||
} else {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_off", entity.entityId,
|
||||
null));
|
||||
}
|
||||
{"white_value": _tmpWhiteValue}));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@ -48,20 +55,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 +91,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
_buildBrightnessControl(entity),
|
||||
_buildWhiteValueControl(entity),
|
||||
_buildColorTempControl(entity),
|
||||
_buildColorControl(entity),
|
||||
_buildEffectControl(entity)
|
||||
@ -98,7 +100,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(() {
|
||||
@ -106,10 +108,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
_tmpBrightness = value.round();
|
||||
});
|
||||
},
|
||||
min: 0.0,
|
||||
min: 1.0,
|
||||
max: 255.0,
|
||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||
value: _tmpBrightness.toDouble(),
|
||||
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(),
|
||||
leading: Icon(Icons.brightness_5),
|
||||
title: "Brightness",
|
||||
);
|
||||
@ -118,12 +120,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 +164,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 +201,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(
|
21
lib/entities/lock/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,
|
||||
);
|
||||
}
|
||||
}
|
66
lib/entities/lock/widgets/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),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class MediaPlayerEntity extends Entity {
|
||||
|
||||
@ -20,55 +20,55 @@ class MediaPlayerEntity extends Entity {
|
||||
static const SUPPORT_SHUFFLE_SET = 32768;
|
||||
static const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||
|
||||
MediaPlayerEntity(Map rawData) : super(rawData);
|
||||
MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportPause => ((attributes["supported_features"] &
|
||||
bool get supportPause => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PAUSE) ==
|
||||
MediaPlayerEntity.SUPPORT_PAUSE);
|
||||
bool get supportSeek => ((attributes["supported_features"] &
|
||||
bool get supportSeek => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_SEEK) ==
|
||||
MediaPlayerEntity.SUPPORT_SEEK);
|
||||
bool get supportVolumeSet => ((attributes["supported_features"] &
|
||||
bool get supportVolumeSet => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_SET) ==
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_SET);
|
||||
bool get supportVolumeMute => ((attributes["supported_features"] &
|
||||
bool get supportVolumeMute => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_MUTE) ==
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_MUTE);
|
||||
bool get supportPreviousTrack => ((attributes["supported_features"] &
|
||||
bool get supportPreviousTrack => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) ==
|
||||
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK);
|
||||
bool get supportNextTrack => ((attributes["supported_features"] &
|
||||
bool get supportNextTrack => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_NEXT_TRACK) ==
|
||||
MediaPlayerEntity.SUPPORT_NEXT_TRACK);
|
||||
|
||||
bool get supportTurnOn => ((attributes["supported_features"] &
|
||||
bool get supportTurnOn => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_TURN_ON) ==
|
||||
MediaPlayerEntity.SUPPORT_TURN_ON);
|
||||
bool get supportTurnOff => ((attributes["supported_features"] &
|
||||
bool get supportTurnOff => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_TURN_OFF) ==
|
||||
MediaPlayerEntity.SUPPORT_TURN_OFF);
|
||||
bool get supportPlayMedia => ((attributes["supported_features"] &
|
||||
bool get supportPlayMedia => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PLAY_MEDIA) ==
|
||||
MediaPlayerEntity.SUPPORT_PLAY_MEDIA);
|
||||
bool get supportVolumeStep => ((attributes["supported_features"] &
|
||||
bool get supportVolumeStep => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_STEP) ==
|
||||
MediaPlayerEntity.SUPPORT_VOLUME_STEP);
|
||||
bool get supportSelectSource => ((attributes["supported_features"] &
|
||||
bool get supportSelectSource => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_SELECT_SOURCE) ==
|
||||
MediaPlayerEntity.SUPPORT_SELECT_SOURCE);
|
||||
bool get supportStop => ((attributes["supported_features"] &
|
||||
bool get supportStop => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_STOP) ==
|
||||
MediaPlayerEntity.SUPPORT_STOP);
|
||||
bool get supportClearPlaylist => ((attributes["supported_features"] &
|
||||
bool get supportClearPlaylist => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) ==
|
||||
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST);
|
||||
bool get supportPlay => ((attributes["supported_features"] &
|
||||
bool get supportPlay => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_PLAY) ==
|
||||
MediaPlayerEntity.SUPPORT_PLAY);
|
||||
bool get supportShuffleSet => ((attributes["supported_features"] &
|
||||
bool get supportShuffleSet => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_SHUFFLE_SET) ==
|
||||
MediaPlayerEntity.SUPPORT_SHUFFLE_SET);
|
||||
bool get supportSelectSoundMode => ((attributes["supported_features"] &
|
||||
bool get supportSelectSoundMode => ((supportedFeatures &
|
||||
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) ==
|
||||
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE);
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class MediaPlayerWidget extends StatelessWidget {
|
||||
|
||||
@ -73,7 +73,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildImage(MediaPlayerEntity entity) {
|
||||
String state = entity.state;
|
||||
if (homeAssistantWebHost != null && entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Row(
|
||||
@ -81,7 +81,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Image(
|
||||
image: CachedNetworkImageProvider("$homeAssistantWebHost${entity.entityPicture}"),
|
||||
image: CachedNetworkImageProvider("${entity.entityPicture}"),
|
||||
height: 240.0,
|
||||
//width: 320.0,
|
||||
fit: BoxFit.contain,
|
||||
@ -95,7 +95,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconName("mdi:movie"),
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
||||
size: 150.0,
|
||||
color: EntityColor.stateColor("$state"),
|
||||
)
|
||||
@ -120,12 +120,12 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
||||
void _setPower(MediaPlayerEntity entity) {
|
||||
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
|
||||
if (entity.state == EntityState.off) {
|
||||
TheLogger.debug("${entity.entityId} turn_on");
|
||||
Logger.d("${entity.entityId} turn_on");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
null));
|
||||
} else {
|
||||
TheLogger.debug("${entity.entityId} turn_off");
|
||||
Logger.d("${entity.entityId} turn_off");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_off", entity.entityId,
|
||||
null));
|
||||
@ -134,7 +134,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _callAction(MediaPlayerEntity entity, String action) {
|
||||
TheLogger.debug("${entity.entityId} $action");
|
||||
Logger.d("${entity.entityId} $action");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "$action", entity.entityId,
|
||||
null));
|
||||
@ -227,7 +227,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
||||
if (showMenu) {
|
||||
result.add(
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical")),
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
|
||||
)
|
||||
@ -307,11 +307,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
||||
Widget muteWidget;
|
||||
Widget volumeStepWidget;
|
||||
if (entity.supportVolumeMute) {
|
||||
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
|
||||
bool isMuted = entity.attributes["is_volume_muted"] ?? false;
|
||||
muteWidget =
|
||||
IconButton(
|
||||
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up),
|
||||
icon: Icon(isMuted ? Icons.volume_up : Icons.volume_off),
|
||||
onPressed: () => _setVolumeMute(!isMuted, entity.entityId)
|
||||
);
|
||||
} else {
|
||||
@ -322,11 +322,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:plus")),
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||
onPressed: () => _setVolumeUp(entity.entityId)
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:minus")),
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||
onPressed: () => _setVolumeDown(entity.entityId)
|
||||
)
|
||||
],
|
@ -1,11 +1,11 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class SelectEntity extends Entity {
|
||||
List<String> get listOptions => attributes["options"] != null
|
||||
? (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,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class SelectStateWidget extends StatefulWidget {
|
||||
|
@ -1,8 +1,4 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SunEntity extends Entity {
|
||||
SunEntity(Map rawData) : super(rawData);
|
||||
}
|
||||
part of '../../main.dart';
|
||||
|
||||
class SensorEntity extends Entity {
|
||||
|
||||
@ -12,6 +8,6 @@ class SensorEntity extends Entity {
|
||||
numericState: true
|
||||
);
|
||||
|
||||
SensorEntity(Map rawData) : super(rawData);
|
||||
SensorEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
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,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class SliderControlsWidget extends StatefulWidget {
|
||||
|
5
lib/entities/sun/sun_entity.class.dart
Normal file
@ -0,0 +1,5 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class SunEntity extends Entity {
|
||||
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
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,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class SwitchStateWidget extends StatefulWidget {
|
||||
|
||||
@ -71,13 +71,13 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
onPressed: () => _setNewState(false, entity),
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")),
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash-off")),
|
||||
color: newState == EntityState.on ? Colors.black : Colors.blue,
|
||||
iconSize: Sizes.iconSize,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _setNewState(true, entity),
|
||||
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")),
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash")),
|
||||
color: newState == EntityState.on ? Colors.blue : Colors.black,
|
||||
iconSize: Sizes.iconSize
|
||||
)
|
@ -1,7 +1,7 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class TextEntity extends Entity {
|
||||
TextEntity(Map rawData) : super(rawData);
|
||||
TextEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
int get valueMinLength => attributes["min"] ?? -1;
|
||||
int get valueMaxLength => attributes["max"] ?? -1;
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class TextInputStateWidget extends StatefulWidget {
|
||||
|
||||
@ -85,7 +85,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
TheLogger.warning( "Unsupported input mode for ${entity.entityId}");
|
||||
Logger.w( "Unsupported input mode for ${entity.entityId}");
|
||||
return SimpleEntityState();
|
||||
}
|
||||
}
|
45
lib/entities/timer/timer_entity.class.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();
|
||||
}
|
||||
}
|
65
lib/entities/timer/widgets/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,10 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ButtonEntity extends Entity {
|
||||
ButtonEntity(Map rawData) : super(rawData);
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return ButtonStateWidget();
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ClimateEntity extends Entity {
|
||||
|
||||
@override
|
||||
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||
chartType: EntityHistoryWidgetType.numericAttributes,
|
||||
numericState: false,
|
||||
numericAttributesToShow: ["current_temperature"]
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
bool get supportTargetTemperature => ((attributes["supported_features"] &
|
||||
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"] &
|
||||
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) ==
|
||||
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) ==
|
||||
ClimateEntity.SUPPORT_SWING_MODE);
|
||||
bool get supportAwayMode =>
|
||||
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
|
||||
ClimateEntity.SUPPORT_AWAY_MODE);
|
||||
bool get supportAuxHeat =>
|
||||
((attributes["supported_features"] & 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>()
|
||||
: null;
|
||||
List<String> get fanList => attributes["fan_list"] != null
|
||||
? (attributes["fan_list"] as List).cast<String>()
|
||||
: null;
|
||||
List<String> get swingList => attributes["swing_list"] != null
|
||||
? (attributes["swing_list"] as List).cast<String>()
|
||||
: null;
|
||||
double get temperature => _getDoubleAttributeValue('temperature');
|
||||
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
||||
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
||||
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
||||
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
|
||||
double get targetHumidity => _getDoubleAttributeValue('humidity');
|
||||
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||
String get operationMode => attributes['operation_mode'];
|
||||
String get fanMode => attributes['fan_mode'];
|
||||
String get swingMode => attributes['swing_mode'];
|
||||
bool get awayMode => attributes['away_mode'] == "on";
|
||||
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);
|
||||
if (supportTargetTemperature) {
|
||||
historyConfig.numericAttributesToShow.add("temperature");
|
||||
}
|
||||
if (supportTargetTemperatureHigh) {
|
||||
historyConfig.numericAttributesToShow.add("target_temp_high");
|
||||
}
|
||||
if (supportTargetTemperatureLow) {
|
||||
historyConfig.numericAttributesToShow.add("target_temp_low");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return ClimateStateWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
return ClimateControlWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
double _getDoubleAttributeValue(String attributeName) {
|
||||
var temp1 = attributes["$attributeName"];
|
||||
if (temp1 is int) {
|
||||
return temp1.toDouble();
|
||||
} else if (temp1 is double) {
|
||||
return temp1;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class LockEntity extends Entity {
|
||||
LockEntity(Map rawData) : super(rawData);
|
||||
|
||||
bool get isLocked => state == "locked";
|
||||
|
||||
@override
|
||||
Widget _buildStatePart(BuildContext context) {
|
||||
return LockStateWidget();
|
||||
}
|
||||
}
|
@ -2,13 +2,15 @@ part of 'main.dart';
|
||||
|
||||
class EntityCollection {
|
||||
|
||||
final homeAssistantWebHost;
|
||||
|
||||
Map<String, Entity> _allEntities;
|
||||
//Map<String, Entity> views;
|
||||
|
||||
bool get isEmpty => _allEntities.isEmpty;
|
||||
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
||||
|
||||
EntityCollection() {
|
||||
EntityCollection(this.homeAssistantWebHost) {
|
||||
_allEntities = {};
|
||||
//views = {};
|
||||
}
|
||||
@ -19,7 +21,7 @@ class EntityCollection {
|
||||
_allEntities.clear();
|
||||
//views.clear();
|
||||
|
||||
TheLogger.debug("Parsing ${rawData.length} Home Assistant entities");
|
||||
Logger.d("Parsing ${rawData.length} Home Assistant entities");
|
||||
rawData.forEach((rawEntityData) {
|
||||
addFromRaw(rawEntityData);
|
||||
});
|
||||
@ -33,67 +35,85 @@ class EntityCollection {
|
||||
});
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_allEntities.clear();
|
||||
}
|
||||
|
||||
Entity _createEntityInstance(rawEntityData) {
|
||||
switch (rawEntityData["entity_id"].split(".")[0]) {
|
||||
case 'sun': {
|
||||
return SunEntity(rawEntityData);
|
||||
return SunEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "media_player": {
|
||||
return MediaPlayerEntity(rawEntityData);
|
||||
return MediaPlayerEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case 'sensor': {
|
||||
return SensorEntity(rawEntityData);
|
||||
return SensorEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case 'lock': {
|
||||
return LockEntity(rawEntityData);
|
||||
return LockEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "automation":
|
||||
case "automation": {
|
||||
return AutomationEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
|
||||
case "input_boolean":
|
||||
case "switch": {
|
||||
return SwitchEntity(rawEntityData);
|
||||
return SwitchEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "light": {
|
||||
return LightEntity(rawEntityData);
|
||||
return LightEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "group": {
|
||||
return GroupEntity(rawEntityData);
|
||||
return GroupEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "script":
|
||||
case "scene": {
|
||||
return ButtonEntity(rawEntityData);
|
||||
return ButtonEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_datetime": {
|
||||
return DateTimeEntity(rawEntityData);
|
||||
return DateTimeEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_select": {
|
||||
return SelectEntity(rawEntityData);
|
||||
return SelectEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_number": {
|
||||
return SliderEntity(rawEntityData);
|
||||
return SliderEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "input_text": {
|
||||
return TextEntity(rawEntityData);
|
||||
return TextEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "climate": {
|
||||
return ClimateEntity(rawEntityData);
|
||||
return ClimateEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "cover": {
|
||||
return CoverEntity(rawEntityData);
|
||||
return CoverEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "fan": {
|
||||
return FanEntity(rawEntityData);
|
||||
return FanEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "camera": {
|
||||
return CameraEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "alarm_control_panel": {
|
||||
return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "timer": {
|
||||
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
default: {
|
||||
return Entity(rawEntityData);
|
||||
return Entity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateState(Map rawStateData) {
|
||||
bool updateState(Map rawStateData) {
|
||||
if (isExist(rawStateData["entity_id"])) {
|
||||
updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
|
||||
return false;
|
||||
} else {
|
||||
addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,14 +121,13 @@ class EntityCollection {
|
||||
_allEntities[entity.entityId] = entity;
|
||||
}
|
||||
|
||||
Entity addFromRaw(Map rawEntityData) {
|
||||
void addFromRaw(Map rawEntityData) {
|
||||
Entity entity = _createEntityInstance(rawEntityData);
|
||||
_allEntities[entity.entityId] = entity;
|
||||
return entity;
|
||||
}
|
||||
|
||||
void updateFromRaw(Map rawEntityData) {
|
||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
|
||||
get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
|
||||
Entity get(String entityId) {
|
||||
|
@ -9,7 +9,12 @@ class ButtonEntityContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
@ -19,11 +24,11 @@ class ButtonEntityContainer extends StatelessWidget {
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitHeight,
|
||||
child: EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
iconSize: Sizes.iconSize,
|
||||
)
|
||||
fit: BoxFit.fitHeight,
|
||||
child: EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
size: Sizes.iconSize,
|
||||
)
|
||||
),
|
||||
),
|
||||
_buildName()
|
||||
|
@ -14,40 +14,60 @@ class BadgeWidget extends StatelessWidget {
|
||||
{
|
||||
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
||||
? Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
|
||||
MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
|
||||
size: iconSize,
|
||||
)
|
||||
: Icon(
|
||||
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
|
||||
MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
|
||||
size: iconSize,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "sensor":
|
||||
case "camera":
|
||||
case "media_player":
|
||||
case "binary_sensor":
|
||||
{
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.state}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 17.0),
|
||||
),
|
||||
badgeIcon = EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
color: Colors.black
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
{
|
||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||
entityModel.entityWrapper, iconSize, Colors.black);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.state;
|
||||
badgeIcon = EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
color: Colors.black
|
||||
);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||
entityModel.entityWrapper, iconSize, Colors.black);
|
||||
double stateFontSize;
|
||||
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
|
||||
stateFontSize = 18.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
|
||||
stateFontSize = 15.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
|
||||
stateFontSize = 10.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||
stateFontSize = 8.0;
|
||||
}
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: stateFontSize),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Widget onBadgeText;
|
||||
|
70
lib/entity_widgets/common/camera_stream_view.dart
Normal file
@ -0,0 +1,70 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class CameraStreamView extends StatefulWidget {
|
||||
|
||||
CameraStreamView({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CameraStreamViewState createState() => _CameraStreamViewState();
|
||||
}
|
||||
|
||||
class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
CameraEntity _entity;
|
||||
bool started = false;
|
||||
String streamUrl = "";
|
||||
|
||||
launchStream() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebviewScaffold(
|
||||
url: "$streamUrl",
|
||||
withZoom: true,
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context)
|
||||
),
|
||||
title: new Text("${_entity.displayName}"),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!started) {
|
||||
_entity = EntityModel
|
||||
.of(context)
|
||||
.entityWrapper
|
||||
.entity;
|
||||
started = true;
|
||||
}
|
||||
streamUrl = '${Connection().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
.entityId}?token=${_entity.attributes['access_token']}';
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
||||
iconSize: 50.0,
|
||||
onPressed: () => launchStream(),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -7,24 +7,16 @@ class EntityAttributesList extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
List<Widget> attrs = [];
|
||||
if ((entityModel.entityWrapper.entity.attributesToShow == null) ||
|
||||
(entityModel.entityWrapper.entity.attributesToShow.contains("all"))) {
|
||||
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
|
||||
attrs.add(_buildSingleAttribute("$name", "$value"));
|
||||
});
|
||||
} else {
|
||||
entityModel.entityWrapper.entity.attributesToShow.forEach((String attr) {
|
||||
String attrValue = entityModel.entityWrapper.entity.getAttribute("$attr");
|
||||
if (attrValue != null) {
|
||||
attrs.add(
|
||||
_buildSingleAttribute("$attr", "$attrValue"));
|
||||
}
|
||||
});
|
||||
}
|
||||
return Column(
|
||||
children: attrs,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
|
||||
attrs.add(_buildSingleAttribute("$name", "${value ?? '-'}"));
|
||||
});
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
child: Column(
|
||||
children: attrs,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -46,7 +38,7 @@ class EntityAttributesList extends StatelessWidget {
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||
child: Text(
|
||||
"$value",
|
||||
"${value}",
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
|
41
lib/entity_widgets/common/flat_service_button.dart
Normal file
@ -0,0 +1,41 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class FlatServiceButton extends StatelessWidget {
|
||||
|
||||
final String serviceDomain;
|
||||
final String serviceName;
|
||||
final String entityId;
|
||||
final String text;
|
||||
final double fontSize;
|
||||
|
||||
FlatServiceButton({
|
||||
Key key,
|
||||
@required this.serviceDomain,
|
||||
@required this.serviceName,
|
||||
@required this.entityId,
|
||||
@required this.text,
|
||||
this.fontSize: Sizes.stateFontSize
|
||||
}) : super(key: key);
|
||||
|
||||
void _setNewState() {
|
||||
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: fontSize*2.5,
|
||||
child: FlatButton(
|
||||
onPressed: (() {
|
||||
_setNewState();
|
||||
}),
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: fontSize, color: Colors.blue),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ class LastUpdatedWidget extends StatelessWidget {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
Sizes.leftWidgetPadding, 0.0, 0.0, 0.0),
|
||||
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, 0.0),
|
||||
child: Text(
|
||||
'${entityModel.entityWrapper.entity.lastUpdated}',
|
||||
textAlign: TextAlign.left,
|
||||
|
101
lib/entity_widgets/common/light_color_picker.dart
Normal file
@ -0,0 +1,101 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightColorPicker extends StatefulWidget {
|
||||
|
||||
final HSVColor color;
|
||||
final onColorSelected;
|
||||
final double hueStep;
|
||||
final double saturationStep;
|
||||
final EdgeInsets padding;
|
||||
|
||||
LightColorPicker({this.color, this.onColorSelected, this.hueStep: 15.0, this.saturationStep: 0.2, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)});
|
||||
|
||||
@override
|
||||
LightColorPickerState createState() => new LightColorPickerState();
|
||||
}
|
||||
|
||||
class LightColorPickerState extends State<LightColorPicker> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> colorRows = [];
|
||||
Border border;
|
||||
bool isSomethingSelected = false;
|
||||
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
|
||||
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
|
||||
List<Widget> rowChildren = [];
|
||||
//Logger.d("$saturation");
|
||||
double roundedSaturation = double.parse(widget.color.saturation.toStringAsFixed(1));
|
||||
//Logger.d("Rounded saturation=$roundedSaturation");
|
||||
for (double hue = 0; hue <= (365 - widget.hueStep);
|
||||
hue += widget.hueStep) {
|
||||
bool isExactHue = widget.color.hue.round() == hue;
|
||||
bool isHueInRange = widget.color.hue.round() > hue && widget.color.hue.round() < (hue+widget.hueStep);
|
||||
bool isExactSaturation = roundedSaturation == saturation;
|
||||
bool isSaturationInRange = roundedSaturation > saturation && roundedSaturation < double.parse((saturation+widget.saturationStep).toStringAsFixed(1));
|
||||
if ((isExactHue || isHueInRange) && (isExactSaturation || isSaturationInRange)) {
|
||||
//Logger.d("$isExactHue $isHueInRange $isExactSaturation $isSaturationInRange (${saturation+widget.saturationStep})");
|
||||
border = Border.all(
|
||||
width: 2.0,
|
||||
color: Colors.white,
|
||||
);
|
||||
isSomethingSelected = true;
|
||||
} else {
|
||||
border = null;
|
||||
}
|
||||
HSVColor currentColor = HSVColor.fromAHSV(1.0, hue, double.parse(saturation.toStringAsFixed(2)), 1.0);
|
||||
rowChildren.add(
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
child: Container(
|
||||
height: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
color: currentColor.toColor(),
|
||||
border: border,
|
||||
),
|
||||
),
|
||||
onTap: () => widget.onColorSelected(currentColor),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
colorRows.add(
|
||||
Row(
|
||||
children: rowChildren,
|
||||
)
|
||||
);
|
||||
}
|
||||
colorRows.add(
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
child: Container(
|
||||
height: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: isSomethingSelected ? null : Border.all(
|
||||
width: 2.0,
|
||||
color: Colors.amber[200],
|
||||
)
|
||||
),
|
||||
),
|
||||
onTap: () => widget.onColorSelected(HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0)),
|
||||
)
|
||||
)
|
||||
);
|
||||
return Padding(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: colorRows,
|
||||
),
|
||||
padding: widget.padding,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
@ -7,8 +7,8 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
final String value;
|
||||
final double captionFontSize;
|
||||
final double valueFontSize;
|
||||
final double bottomPadding;
|
||||
final onChange;
|
||||
final EdgeInsets padding;
|
||||
|
||||
ModeSelectorWidget({
|
||||
Key key,
|
||||
@ -18,45 +18,47 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
@required this.onChange,
|
||||
this.captionFontSize,
|
||||
this.valueFontSize,
|
||||
this.bottomPadding
|
||||
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("$caption", style: TextStyle(
|
||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||
)),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: ButtonTheme(
|
||||
alignedDropdown: true,
|
||||
child: DropdownButton<String>(
|
||||
value: value,
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
style: TextStyle(
|
||||
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
||||
color: Colors.black,
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("$caption", style: TextStyle(
|
||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||
)),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: ButtonTheme(
|
||||
alignedDropdown: true,
|
||||
child: DropdownButton<String>(
|
||||
value: value,
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
style: TextStyle(
|
||||
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
||||
color: Colors.black,
|
||||
),
|
||||
hint: Text("Select ${caption.toLowerCase()}"),
|
||||
items: options.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => onChange(mode),
|
||||
),
|
||||
hint: Text("Select ${caption.toLowerCase()}"),
|
||||
items: options.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => onChange(mode),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(height: bottomPadding ?? Sizes.rowPadding,)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ class ModeSwitchWidget extends StatelessWidget {
|
||||
final double captionFontSize;
|
||||
final bool value;
|
||||
final bool expanded;
|
||||
final EdgeInsets padding;
|
||||
|
||||
ModeSwitchWidget({
|
||||
Key key,
|
||||
@ -14,19 +15,23 @@ class ModeSwitchWidget extends StatelessWidget {
|
||||
@required this.onChange,
|
||||
this.captionFontSize,
|
||||
this.value,
|
||||
this.expanded: true
|
||||
this.expanded: true,
|
||||
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
_buildCaption(),
|
||||
Switch(
|
||||
onChanged: (value) => onChange(value),
|
||||
value: value ?? false,
|
||||
)
|
||||
],
|
||||
return Padding(
|
||||
padding: this.padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
_buildCaption(),
|
||||
Switch(
|
||||
onChanged: (value) => onChange(value),
|
||||
value: value ?? false,
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -6,14 +6,26 @@ class SimpleEntityState extends StatelessWidget {
|
||||
final TextAlign textAlign;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final int maxLines;
|
||||
final String customValue;
|
||||
|
||||
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
String state = entityModel.entityWrapper.entity.state ?? "";
|
||||
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
|
||||
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(" ", " ");
|
||||
}
|
||||
@ -25,9 +37,7 @@ class SimpleEntityState extends StatelessWidget {
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
)
|
||||
style: textStyle
|
||||
)
|
||||
);
|
||||
if (expanded) {
|
@ -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,)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,29 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
||||
return Divider(
|
||||
color: Colors.black45,
|
||||
);
|
||||
}
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Divider(
|
||||
color: Colors.black45,
|
||||
),
|
||||
Text(
|
||||
"${entityModel.entityWrapper.entity.displayName}",
|
||||
style: TextStyle(color: Colors.blue),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onLongPress: () {
|
||||
if (entityModel.handleTap) {
|
||||
@ -30,7 +53,9 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 3,
|
||||
child: EntityName(),
|
||||
child: EntityName(
|
||||
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
||||
),
|
||||
),
|
||||
state
|
||||
],
|
||||
|
@ -2,6 +2,8 @@ part of '../main.dart';
|
||||
|
||||
class EntityColor {
|
||||
|
||||
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
|
||||
|
||||
static const badgeColors = {
|
||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||
@ -10,19 +12,29 @@ class EntityColor {
|
||||
static const _stateColors = {
|
||||
EntityState.on: Colors.amber,
|
||||
"auto": Colors.amber,
|
||||
EntityState.idle: Colors.amber,
|
||||
EntityState.active: Colors.amber,
|
||||
EntityState.playing: Colors.amber,
|
||||
"above_horizon": Colors.amber,
|
||||
EntityState.home: Colors.amber,
|
||||
EntityState.open: Colors.amber,
|
||||
EntityState.off: Color.fromRGBO(68, 115, 158, 1.0),
|
||||
EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0),
|
||||
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0),
|
||||
"default": Color.fromRGBO(68, 115, 158, 1.0),
|
||||
EntityState.off: defaultStateColor,
|
||||
EntityState.closed: defaultStateColor,
|
||||
"below_horizon": defaultStateColor,
|
||||
"default": defaultStateColor,
|
||||
EntityState.idle: defaultStateColor,
|
||||
"heat": Colors.redAccent,
|
||||
"cool": Colors.lightBlue,
|
||||
EntityState.unavailable: Colors.black26,
|
||||
EntityState.unknown: Colors.black26,
|
||||
EntityState.alarm_disarmed: Colors.green,
|
||||
EntityState.alarm_armed_away: Colors.redAccent,
|
||||
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
|
||||
EntityState.alarm_armed_home: Colors.redAccent,
|
||||
EntityState.alarm_armed_night: Colors.redAccent,
|
||||
EntityState.alarm_triggered: Colors.redAccent,
|
||||
EntityState.alarm_arming: Colors.amber,
|
||||
EntityState.alarm_disarming: Colors.amber,
|
||||
EntityState.alarm_pending: Colors.amber,
|
||||
};
|
||||
|
||||
static Color stateColor(String state) {
|
||||
|
@ -3,20 +3,71 @@ part of '../main.dart';
|
||||
class EntityIcon extends StatelessWidget {
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
final double iconSize;
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB(
|
||||
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key);
|
||||
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
|
||||
|
||||
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||
String domain = entityId.split(".")[0];
|
||||
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
|
||||
String iconNameByDeviceClass;
|
||||
if (deviceClass != null) {
|
||||
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
|
||||
}
|
||||
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
|
||||
if (iconName != null) {
|
||||
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildIcon(EntityWrapper data, Color color) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
if (data.entityPicture != null) {
|
||||
return Container(
|
||||
height: size+12,
|
||||
width: size+12,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit:BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
"${data.entityPicture}"
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
String iconName = data.icon;
|
||||
int iconCode = 0;
|
||||
if (iconName.length > 0) {
|
||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||
} else {
|
||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||
data.entity.deviceClass, data.entity.state); //
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
|
||||
child: Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: color,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||
child: buildIcon(
|
||||
entityWrapper,
|
||||
iconSize,
|
||||
EntityColor.stateColor(entityWrapper.entity.state)
|
||||
color ?? EntityColor.stateColor(entityWrapper.entity.state)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ class EntityName extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
TextStyle textStyle = TextStyle(fontSize: fontSize);
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
|
||||
}
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Text(
|
||||
@ -21,7 +25,7 @@ class EntityName extends StatelessWidget {
|
||||
overflow: textOverflow,
|
||||
softWrap: wordsWrap,
|
||||
maxLines: maxLines,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
style: textStyle,
|
||||
textAlign: textAlign,
|
||||
),
|
||||
);
|
||||
|
@ -22,6 +22,12 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
List<Widget> result = [];
|
||||
if (!nameInTheBottom) {
|
||||
if (showName) {
|
||||
@ -35,7 +41,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
result.add(
|
||||
EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
iconSize: iconSize,
|
||||
size: iconSize,
|
||||
)
|
||||
);
|
||||
if (!nameInTheBottom) {
|
||||
|
@ -94,11 +94,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
}
|
||||
|
||||
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
|
||||
TheLogger.debug(" parsing history...");
|
||||
Logger.d(" parsing history...");
|
||||
Map<String, List<EntityHistoryMoment>> numericDataLists = {};
|
||||
int colorIdCounter = 0;
|
||||
widget.config.numericAttributesToShow.forEach((String attrName) {
|
||||
TheLogger.debug(" parsing attribute $attrName");
|
||||
Logger.d(" parsing attribute $attrName");
|
||||
List<EntityHistoryMoment> data = [];
|
||||
DateTime now = DateTime.now();
|
||||
for (var i = 0; i < widget.rawHistory.length; i++) {
|
||||
@ -152,7 +152,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
}
|
||||
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
||||
numericDataLists.forEach((attrName, dataList) {
|
||||
TheLogger.debug(" adding ${dataList.length} data values");
|
||||
Logger.d(" adding ${dataList.length} data values");
|
||||
result.add(
|
||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||
id: "value",
|
||||
|
@ -32,6 +32,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
List _history;
|
||||
bool _needToUpdateHistory;
|
||||
DateTime _historyLastUpdated;
|
||||
bool _disposed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -39,37 +40,40 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
_needToUpdateHistory = true;
|
||||
}
|
||||
|
||||
void _loadHistory(HomeAssistant ha, String entityId) {
|
||||
void _loadHistory(String entityId) {
|
||||
DateTime now = DateTime.now();
|
||||
if (_historyLastUpdated != null) {
|
||||
TheLogger.debug("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
|
||||
Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
|
||||
}
|
||||
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
||||
_historyLastUpdated = now;
|
||||
ha.getHistory(entityId).then((history){
|
||||
setState(() {
|
||||
_history = history.isNotEmpty ? history[0] : [];
|
||||
_needToUpdateHistory = false;
|
||||
});
|
||||
Connection().getHistory(entityId).then((history){
|
||||
if (!_disposed) {
|
||||
setState(() {
|
||||
_history = history.isNotEmpty ? history[0] : [];
|
||||
_needToUpdateHistory = false;
|
||||
});
|
||||
}
|
||||
}).catchError((e) {
|
||||
TheLogger.error("Error loading $entityId history: $e");
|
||||
setState(() {
|
||||
_history = [];
|
||||
_needToUpdateHistory = false;
|
||||
});
|
||||
Logger.e("Error loading $entityId history: $e");
|
||||
if (!_disposed) {
|
||||
setState(() {
|
||||
_history = [];
|
||||
_needToUpdateHistory = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
final Entity entity = entityModel.entityWrapper.entity;
|
||||
if (!_needToUpdateHistory) {
|
||||
_needToUpdateHistory = true;
|
||||
} else {
|
||||
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
|
||||
_loadHistory(entity.entityId);
|
||||
}
|
||||
return _buildChart();
|
||||
}
|
||||
@ -122,7 +126,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
}
|
||||
|
||||
default: {
|
||||
TheLogger.debug(" Simple selected as default");
|
||||
Logger.d(" Simple selected as default");
|
||||
return SimpleStateHistoryChartWidget(
|
||||
rawHistory: _history,
|
||||
);
|
||||
@ -131,4 +135,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
19
lib/entity_widgets/missed_entity.dart
Normal file
@ -0,0 +1,19 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MissedEntityWidget extends StatelessWidget {
|
||||
MissedEntityWidget({
|
||||
Key key
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
return Container(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5.0),
|
||||
child: Text("Entity not available: ${entityModel.entityWrapper.entity.entityId}"),
|
||||
),
|
||||
color: Colors.amber[100],
|
||||
);
|
||||
}
|
||||
}
|
@ -15,26 +15,6 @@ class EntityModel extends InheritedWidget {
|
||||
return context.inheritFromWidgetOfExactType(EntityModel);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class HomeAssistantModel extends InheritedWidget {
|
||||
|
||||
const HomeAssistantModel({
|
||||
Key key,
|
||||
@required this.homeAssistant,
|
||||
@required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final HomeAssistant homeAssistant;
|
||||
|
||||
static HomeAssistantModel of(BuildContext context) {
|
||||
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||
return true;
|
||||
|
@ -1,27 +0,0 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class ButtonStateWidget extends StatelessWidget {
|
||||
|
||||
void _setNewState(Entity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "turn_on", entity.entityId, null));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
return SizedBox(
|
||||
height: 34.0,
|
||||
child: FlatButton(
|
||||
onPressed: (() {
|
||||
_setNewState(entityModel.entityWrapper.entity);
|
||||
}),
|
||||
child: Text(
|
||||
"EXECUTE",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class LockStateWidget extends StatelessWidget {
|
||||
|
||||
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;
|
||||
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),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -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,267 @@ 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_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",
|
||||
"os_name": Device().osName,
|
||||
"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(ShowPopupDialogEvent(
|
||||
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) {
|
||||
if (response == null || response.isEmpty) {
|
||||
Logger.d("No registration data in response. MobileApp integration was removed");
|
||||
_askToRegisterApp();
|
||||
} else {
|
||||
Logger.d("App registration works fine");
|
||||
if (showOkDialog) {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
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");
|
||||
_askToRegisterApp();
|
||||
} else {
|
||||
Logger.e("Error updating app registration: ${e.toString()}");
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "App integration is not working properly",
|
||||
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue.",
|
||||
positiveText: "Report to GitHub",
|
||||
negativeText: "Report to Discord",
|
||||
onPositive: () {
|
||||
HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
onNegative: () {
|
||||
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
||||
},
|
||||
));
|
||||
}
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
return _sendMessageRaw(message, true);
|
||||
}
|
||||
|
||||
void _askToRegisterApp() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
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();
|
||||
});
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
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,83 +303,131 @@ class HomeAssistant {
|
||||
List<HACard> _createLovelaceCards(List rawCards) {
|
||||
List<HACard> result = [];
|
||||
rawCards.forEach((rawCard){
|
||||
bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
HACard card = HACard(
|
||||
id: "card",
|
||||
name: isThereCardOptionsInside ? rawCard["card"]["title"] ?? rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
|
||||
type: isThereCardOptionsInside ? rawCard["card"]['type'] : rawCard['type'],
|
||||
columnsCount: isThereCardOptionsInside ? rawCard["card"]['columns'] ?? 4 : rawCard['columns'] ?? 4,
|
||||
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ?? true : rawCard['show_name'] ?? true,
|
||||
showState: isThereCardOptionsInside ? rawCard["card"]['show_state'] ?? true : rawCard['show_state'] ?? true,
|
||||
showEmpty: rawCard['show_empty'] ?? true,
|
||||
stateFilter: rawCard['state_filter'] ?? []
|
||||
);
|
||||
if (rawCard["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCard["cards"]);
|
||||
}
|
||||
rawCard["entities"]?.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (entities.isExist(rawEntity)) {
|
||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = entities.get(rawEntity["entity"]);
|
||||
card.entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
displayName: rawEntity["name"],
|
||||
try {
|
||||
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
var rawCardInfo = rawCard["card"] ?? rawCard;
|
||||
HACard card = HACard(
|
||||
id: "card",
|
||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||
type: rawCardInfo['type'],
|
||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||
showName: rawCardInfo['show_name'] ?? true,
|
||||
showState: rawCardInfo['show_state'] ?? true,
|
||||
showEmpty: rawCardInfo['show_empty'] ?? true,
|
||||
stateFilter: rawCardInfo['state_filter'] ?? [],
|
||||
states: rawCardInfo['states'],
|
||||
conditions: rawCard['conditions'] ?? [],
|
||||
content: rawCardInfo['content']
|
||||
);
|
||||
if (rawCardInfo["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||
}
|
||||
rawCardInfo["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 (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"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
name: rawEntity["name"],
|
||||
service: rawEntity["service"],
|
||||
actionName: rawEntity["action_name"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (rawEntity["type"] == "weblink") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.navigate,
|
||||
"service": rawEntity["url"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
card.entities.add(EntityWrapper(
|
||||
entity: Entity.weblink(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
url: rawEntity["url"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = entities.get(rawEntity["entity"]);
|
||||
card.entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
displayName: rawEntity["name"],
|
||||
icon: rawEntity["icon"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
if (rawCardInfo["entity"] != null) {
|
||||
var en = rawCardInfo["entity"];
|
||||
if (en is String) {
|
||||
if (entities.isExist(en)) {
|
||||
Entity e = entities.get(en);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: rawCardInfo["icon"],
|
||||
displayName: rawCardInfo["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
|
||||
}
|
||||
} else {
|
||||
if (entities.isExist(en["entity"])) {
|
||||
Entity e = entities.get(en["entity"]);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: en["icon"],
|
||||
displayName: en["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (rawCard["entity"] != null) {
|
||||
var en = rawCard["entity"];
|
||||
if (en is String) {
|
||||
if (entities.isExist(en)) {
|
||||
Entity e = entities.get(en);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (entities.isExist(en["entity"])) {
|
||||
Entity e = entities.get(en["entity"]);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: en["icon"],
|
||||
displayName: en["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
result.add(card);
|
||||
} catch (e) {
|
||||
Logger.e("There was an error parsing card: ${e.toString()}");
|
||||
}
|
||||
result.add(card);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void _parseEntities(response) async {
|
||||
if (response["success"] == false) {
|
||||
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
|
||||
return;
|
||||
}
|
||||
entities.parse(response["result"]);
|
||||
_statesCompleter.complete();
|
||||
}
|
||||
|
||||
void _createUI() {
|
||||
ui = HomeAssistantUI();
|
||||
if ((_useLovelace) && (_rawLovelaceData != null)) {
|
||||
TheLogger.debug("Creating Lovelace UI");
|
||||
if ((Connection().useLovelace) && (_rawLovelaceData != null)) {
|
||||
Logger.d("Creating Lovelace UI");
|
||||
_parseLovelace();
|
||||
} else {
|
||||
TheLogger.debug("Creating group-based UI");
|
||||
Logger.d("Creating group-based UI");
|
||||
int viewCounter = 0;
|
||||
if (!entities.hasDefaultView) {
|
||||
HAView view = HAView(
|
||||
@ -534,38 +457,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 = [];
|
||||
@ -604,4 +501,4 @@ class HAMessage {
|
||||
bool isExpired() {
|
||||
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
788
lib/main.dart
1011
lib/mdi.class.dart
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class EntityViewPage extends StatefulWidget {
|
||||
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
|
||||
@ -19,8 +19,8 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
TheLogger.debug("State change event handled by entity page: ${event.entityId}");
|
||||
if (event.entityId == widget.entityId) {
|
||||
Logger.d("State change event handled by entity page: ${event.entityId}");
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
@ -46,13 +46,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: new Text(_title),
|
||||
),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: HomeAssistantModel(
|
||||
homeAssistant: widget.homeAssistant,
|
||||
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
|
||||
)
|
||||
),
|
||||
body: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class LogViewPage extends StatefulWidget {
|
||||
LogViewPage({Key key, this.title}) : super(key: key);
|
||||
@ -19,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> {
|
||||
}
|
||||
|
||||
_loadLog() async {
|
||||
_logData = TheLogger.getLog();
|
||||
_logData = Logger.getLog();
|
||||
}
|
||||
|
||||
@override
|
40
lib/pages/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();
|
||||
}
|
||||
}
|
107
lib/pages/purchase.page.dart
Normal file
@ -0,0 +1,107 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class PurchasePage extends StatefulWidget {
|
||||
PurchasePage({Key key, this.title}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
_PurchasePageState createState() => new _PurchasePageState();
|
||||
}
|
||||
|
||||
class _PurchasePageState extends State<PurchasePage> {
|
||||
|
||||
bool _loaded = false;
|
||||
String _error = "";
|
||||
List<ProductDetails> _products;
|
||||
List<PurchaseDetails> _purchases;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
}
|
||||
|
||||
_loadProducts() async {
|
||||
final bool available = await InAppPurchaseConnection.instance.isAvailable();
|
||||
if (!available) {
|
||||
setState(() {
|
||||
_error = "Error connecting to store";
|
||||
});
|
||||
} else {
|
||||
const Set<String> _kIds = {'just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||
if (!response.notFoundIDs.isEmpty) {
|
||||
Logger.d("Products not found: ${response.notFoundIDs}");
|
||||
}
|
||||
_products = response.productDetails;
|
||||
_loadPreviousPurchases();
|
||||
}
|
||||
}
|
||||
|
||||
_loadPreviousPurchases() async {
|
||||
final QueryPurchaseDetailsResponse response = await InAppPurchaseConnection.instance.queryPastPurchases();
|
||||
if (response.error != null) {
|
||||
setState(() {
|
||||
_error = "Error loading previous purchases";
|
||||
});
|
||||
} else {
|
||||
_purchases = response.pastPurchases;
|
||||
for (PurchaseDetails purchase in _purchases) {
|
||||
Logger.d("Previous purchase: ${purchase.status}");
|
||||
}
|
||||
if (_products.isEmpty) {
|
||||
setState(() {
|
||||
_error = "No data found in store";
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildProducts() {
|
||||
List<Widget> productWidgets = [];
|
||||
for (ProductDetails product in _products) {
|
||||
productWidgets.add(
|
||||
ProductPurchase(
|
||||
product: product,
|
||||
onBuy: (product) => _buyProduct(product),
|
||||
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
children: productWidgets
|
||||
);
|
||||
}
|
||||
|
||||
void _buyProduct(ProductDetails product) {
|
||||
Logger.d("Starting purchase of ${product.id}");
|
||||
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
|
||||
InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (!_loaded) {
|
||||
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
||||
} else {
|
||||
body = _buildProducts();
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text(widget.title),
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class ConnectionSettingsPage extends StatefulWidget {
|
||||
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
||||
@ -14,45 +14,62 @@ 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;
|
||||
String _longLivedToken = "";
|
||||
String _newLongLivedToken = "";
|
||||
bool _useLovelace = true;
|
||||
bool _newUseLovelace = true;
|
||||
|
||||
String oauthUrl;
|
||||
bool useOAuth = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
|
||||
}
|
||||
|
||||
_loadSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final storage = new FlutterSecureStorage();
|
||||
|
||||
try {
|
||||
useOAuth = prefs.getBool("oauth-used") ?? true;
|
||||
} catch (e) {
|
||||
useOAuth = true;
|
||||
}
|
||||
|
||||
if (!useOAuth) {
|
||||
try {
|
||||
_longLivedToken = _newLongLivedToken =
|
||||
await storage.read(key: "hacl_llt");
|
||||
} catch (e) {
|
||||
_longLivedToken = _newLongLivedToken = "";
|
||||
await storage.delete(key: "hacl_llt");
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
(_newUseLovelace != _useLovelace) ||
|
||||
(_newLongLivedToken != _longLivedToken));
|
||||
|
||||
}
|
||||
|
||||
@ -60,13 +77,19 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||
}
|
||||
_newHassioDomain = _newHassioDomain.split("/")[0];
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final storage = new FlutterSecureStorage();
|
||||
if (_newLongLivedToken.isNotEmpty) {
|
||||
prefs.setBool("oauth-used", false);
|
||||
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
||||
} else if (!useOAuth) {
|
||||
await storage.delete(key: "hacl_llt");
|
||||
}
|
||||
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 +106,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);
|
||||
}
|
||||
}
|
||||
@ -154,45 +177,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
"Try ports 80 and 443 if default is not working and you don't know why.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
new Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
"Login with access token (HA >= 0.78.0)",
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: (_newAuthType == "access_token"),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_newAuthType = value ? "access_token" : "api_password";
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
new Text(
|
||||
"You should use access token for HA >= 0.84.1. Legacy password will not work there.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
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;
|
||||
}
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20.0),
|
||||
child: Text(
|
||||
@ -216,6 +200,33 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Authentication settings",
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontSize: 20.0
|
||||
),
|
||||
),
|
||||
Container(height: 10.0,),
|
||||
Text(
|
||||
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
new TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Long-lived token"
|
||||
),
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newLongLivedToken ?? '',
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newLongLivedToken != null ? _newLongLivedToken.length : 0)
|
||||
)
|
||||
),
|
||||
onChanged: (value) {
|
||||
_newLongLivedToken = value;
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
32
lib/pages/widgets/page_loading_error.dart
Normal file
@ -0,0 +1,32 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class PageLoadingError extends StatelessWidget {
|
||||
|
||||
final String errorText;
|
||||
|
||||
const PageLoadingError({Key key, this.errorText: "Error"}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||
child: Icon(
|
||||
Icons.error,
|
||||
color: Colors.redAccent,
|
||||
size: 48.0
|
||||
)
|
||||
),
|
||||
Text(this.errorText, style: TextStyle(color: Colors.black45))
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
23
lib/pages/widgets/page_loading_indicator.dart
Normal file
@ -0,0 +1,23 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class PageLoadingIndicator extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||
child: CircularProgressIndicator()
|
||||
),
|
||||
Text("Loading...", style: TextStyle(color: Colors.black45))
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
68
lib/pages/widgets/product_purchase.widget.dart
Normal file
@ -0,0 +1,68 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class ProductPurchase extends StatelessWidget {
|
||||
|
||||
final ProductDetails product;
|
||||
final onBuy;
|
||||
final purchased;
|
||||
|
||||
const ProductPurchase({Key key, @required this.product, @required this.onBuy, this.purchased}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String period = "/ ";
|
||||
Color priceColor;
|
||||
if (product.id.contains("year")) {
|
||||
period += "year";
|
||||
priceColor = Colors.amber;
|
||||
} else {
|
||||
period += "month";
|
||||
priceColor = Colors.deepOrangeAccent;
|
||||
}
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${product.title}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text(
|
||||
"${product.description}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 4,
|
||||
softWrap: true,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("${product.price} $period", style: TextStyle(color: priceColor)),
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: RaisedButton(
|
||||
child: Text(this.purchased ? "Bought" : "Buy", style: TextStyle(color: Colors.white)),
|
||||
color: Colors.blue,
|
||||
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -12,6 +12,9 @@ class HACard {
|
||||
bool showEmpty;
|
||||
int columnsCount;
|
||||
List stateFilter;
|
||||
List states;
|
||||
List conditions;
|
||||
String content;
|
||||
|
||||
HACard({
|
||||
this.name,
|
||||
@ -22,6 +25,9 @@ class HACard {
|
||||
this.showState: true,
|
||||
this.stateFilter: const [],
|
||||
this.showEmpty: true,
|
||||
this.content,
|
||||
this.states,
|
||||
this.conditions: const [],
|
||||
@required this.type
|
||||
});
|
||||
|
||||
|
57
lib/ui_class/panel_class.dart
Normal file
@ -0,0 +1,57 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Panel {
|
||||
|
||||
static const iconsByComponent = {
|
||||
"config": "mdi:settings",
|
||||
"history": "mdi:poll-box",
|
||||
"map": "mdi:tooltip-account",
|
||||
"logbook": "mdi:format-list-bulleted-type",
|
||||
"custom": "mdi:home-assistant"
|
||||
};
|
||||
|
||||
final String id;
|
||||
final String type;
|
||||
final String title;
|
||||
final String urlPath;
|
||||
final Map config;
|
||||
String icon;
|
||||
bool isHidden = true;
|
||||
|
||||
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
||||
if (icon == null || !icon.startsWith("mdi:")) {
|
||||
icon = Panel.iconsByComponent[type];
|
||||
}
|
||||
isHidden = (type != "iframe" && type != "config");
|
||||
}
|
||||
|
||||
void handleOpen(BuildContext context) {
|
||||
if (type == "iframe") {
|
||||
Logger.d("Launching custom tab with ${config["url"]}");
|
||||
HAUtils.launchURLInCustomTab(context: context, url: config["url"]);
|
||||
} else if (type == "config") {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PanelPage(title: "$title", panel: this),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
String url = "${Connection().httpWebHost}/$urlPath";
|
||||
Logger.d("Launching custom tab with $url");
|
||||
HAUtils.launchURLInCustomTab(context: context, url: url);
|
||||
}
|
||||
}
|
||||
|
||||
Widget getWidget() {
|
||||
switch (type) {
|
||||
case "config": {
|
||||
return ConfigPanelWidget();
|
||||
}
|
||||
|
||||
default: {
|
||||
return Text("Unsupported panel component: $type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Sizes {
|
||||
static const rightWidgetPadding = 14.0;
|
||||
static const leftWidgetPadding = 8.0;
|
||||
static const rightWidgetPadding = 10.0;
|
||||
static const leftWidgetPadding = 10.0;
|
||||
static const buttonPadding = 4.0;
|
||||
static const extendedWidgetHeight = 50.0;
|
||||
static const iconSize = 28.0;
|
||||
|
@ -4,12 +4,15 @@ class HomeAssistantUI {
|
||||
List<HAView> views;
|
||||
String title;
|
||||
|
||||
bool get isEmpty => views == null || views.isEmpty;
|
||||
|
||||
HomeAssistantUI() {
|
||||
views = [];
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, TabController tabController) {
|
||||
return TabBarView(
|
||||
controller: tabController,
|
||||
children: _buildViews(context)
|
||||
);
|
||||
}
|
||||
@ -24,4 +27,8 @@ class HomeAssistantUI {
|
||||
return result;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
views.clear();
|
||||
}
|
||||
|
||||
}
|