Compare commits
281 Commits
beta/0.7.6
...
foreground
Author | SHA1 | Date | |
---|---|---|---|
c844e21e76 | |||
24d42c9597 | |||
9078ad81e8 | |||
7cba6c8a10 | |||
c1f9c8c16d | |||
1d1d132b33 | |||
e258b3bc2c | |||
13508ee92f | |||
4fbf58e707 | |||
a3442f84ca | |||
6a6ab3b2cb | |||
d9fa553e2f | |||
cde5d9b912 | |||
3468446b5b | |||
326434273a | |||
470d3be946 | |||
d1032be6a6 | |||
cffac8e1f8 | |||
870bc25dd9 | |||
de713024f6 | |||
4d4add4581 | |||
1670c8e505 | |||
55eb1b5125 | |||
dbeaaaf91e | |||
8166d8ce6d | |||
35bcf0c1fa | |||
9c1d240962 | |||
a76652b552 | |||
a140f993d0 | |||
ded60a2867 | |||
b86602bcdb | |||
02ea45469f | |||
90105c3b09 | |||
3d828914cc | |||
8cd5776bc6 | |||
17ec73b176 | |||
e7cce01ca9 | |||
6c73f5d979 | |||
f59cb5afbf | |||
5629215229 | |||
45fb637d48 | |||
7c473eb1ca | |||
b40880c85a | |||
30329ea3ba | |||
ca10401bee | |||
814e0a8b00 | |||
b5fbe7b86f | |||
fc9b6f05c0 | |||
eadae4374b | |||
711cb04dcf | |||
1d39b7fc7d | |||
2fa640433a | |||
2445dc7869 | |||
e3e114fe94 | |||
7a1603b423 | |||
4b831821da | |||
1ec54953d7 | |||
61571600d1 | |||
07a097aa50 | |||
ce1cebaf64 | |||
faf6f73b2a | |||
db3b5d941e | |||
cc60dc2b21 | |||
8aa0e03187 | |||
4492a08d6b | |||
792c0d0c84 | |||
8221eceb78 | |||
12ba5598e4 | |||
536cbf9445 | |||
a87943da27 | |||
3fddc3b5a7 | |||
5bc0b0868a | |||
e9ad612fec | |||
c62e045dae | |||
725ec9291d | |||
96c8338890 | |||
0996fb94da | |||
5de2431a0f | |||
163338ea75 | |||
f28e5493dc | |||
01c0a08fa8 | |||
1c461d2449 | |||
915e8045a3 | |||
f10fc7eec1 | |||
320bc677e0 | |||
46ca1948e2 | |||
7a0ce93cfd | |||
3c0bd68b0a | |||
b4ad3061e4 | |||
d6b1fbec24 | |||
cacdd0d304 | |||
e3e1fa3499 | |||
58842d1ebb | |||
101569d6ee | |||
8a180c4c0e | |||
ba343fbd98 | |||
1d528df341 | |||
51ea0b0afa | |||
9dbb697e58 | |||
947558bb3d | |||
8ba4cc85d8 | |||
0f604a6ce6 | |||
7e48c6535f | |||
1d2a8b613b | |||
89e833eb33 | |||
b65a68e0c4 | |||
bfb24b9d11 | |||
0792cae2b1 | |||
a85fb3d03b | |||
ddb9a9d4e9 | |||
29ee360ec4 | |||
c0faaafd04 | |||
bc045344a5 | |||
7d746fd546 | |||
3ff55f181e | |||
187e12dd79 | |||
10daf2d952 | |||
31c6509d13 | |||
cb74108814 | |||
9efded2139 | |||
96b3e7c739 | |||
b029146bf3 | |||
d715aaf5e5 | |||
0dc12963f0 | |||
4da3b40d55 | |||
f7d05a57ad | |||
df01599fe0 | |||
2c3335ebf3 | |||
05c1427aa8 | |||
02bfaf7db6 | |||
f488c0810b | |||
8dbfb91234 | |||
aee99e3925 | |||
50d3280803 | |||
a90eb5c4db | |||
16c06a2d48 | |||
513bf85cae | |||
82d7aeba02 | |||
12f7cb86de | |||
b65c885467 | |||
2a828a1289 | |||
291f12ba97 | |||
6afbd37d71 | |||
0e8869878f | |||
7c2cfe3215 | |||
c376c0e952 | |||
da5f663396 | |||
0e92418a33 | |||
2eef7cfe5e | |||
de4e0bfb3a | |||
8bf2d31e72 | |||
2125c46143 | |||
5402eb84df | |||
ad5aa0898f | |||
040d40b614 | |||
8e58f22c56 | |||
c91695fbe5 | |||
c43741da49 | |||
f2563a0397 | |||
fba4017819 | |||
5f23e108a1 | |||
68d14bd13d | |||
022622522f | |||
89513ca4e5 | |||
a934ee2335 | |||
49aeea634f | |||
e18b9ebe14 | |||
08ee3f3d80 | |||
62d07bf8b9 | |||
ab398cbdc3 | |||
007d12719c | |||
524d195800 | |||
405de64249 | |||
f53554702e | |||
379e1a4a7e | |||
d6f7096055 | |||
37c721e4f6 | |||
d94235ef6d | |||
eb4184713f | |||
a0a0cb4612 | |||
f448a20784 | |||
36eff26862 | |||
5b2a1163b9 | |||
e627a8b963 | |||
4432124e8c | |||
b8ba3c59e9 | |||
c40a496b6b | |||
a7c3b46061 | |||
dfbaaeb06b | |||
f6ab20c6e8 | |||
7625099d74 | |||
32c8e76855 | |||
0aa2c974d5 | |||
9524c8587b | |||
c075db8b1a | |||
d0b7cc1929 | |||
d8df32f140 | |||
293b5e0242 | |||
2f517a3ad5 | |||
56d8e389db | |||
1377843350 | |||
8e31eaf8bb | |||
5ced01463f | |||
a3548455eb | |||
c40fceea4f | |||
6ad3938a91 | |||
bc642f81ad | |||
14ce608bbb | |||
c4c67747c5 | |||
5b3ceecb0e | |||
bf53e4b9df | |||
7e09d92fdf | |||
1ba9106d0b | |||
d727a29991 | |||
c5d617477f | |||
244a1984cc | |||
b00b745f27 | |||
959ff21b9b | |||
e6a7fd2dfe | |||
216276e5f3 | |||
3e6229cf3e | |||
fc4cb80b74 | |||
b907ff1e82 | |||
7536a52771 | |||
73a8c111d1 | |||
86a19eeec2 | |||
fba4459977 | |||
06f994a827 | |||
35d8607484 | |||
2f4c06e9b5 | |||
92e008a380 | |||
14c272af92 | |||
710de9f2b8 | |||
d9ad3b3083 | |||
b2686cb105 | |||
959e89de2b | |||
6e448d3458 | |||
6695756727 | |||
ed732e9b77 | |||
f495a6affc | |||
c8d7e1a95f | |||
e1ca2638e3 | |||
01226cb9eb | |||
8a80d0c5d1 | |||
f26f3e87c7 | |||
b750417415 | |||
2c35dd7c21 | |||
cff4a4feed | |||
62174b0651 | |||
d3ea4210c1 | |||
1c782bf64d | |||
bc96dab339 | |||
0f7179b944 | |||
1e3bfa8ff7 | |||
2bce86f905 | |||
0be00acc3a | |||
4e61adaeb1 | |||
49a8f08153 | |||
ce15658462 | |||
16d73ba7dd | |||
9f3e3c1917 | |||
f29e382a19 | |||
073562373a | |||
4298ebcd66 | |||
a121295bef | |||
9303e4c0a5 | |||
831fc98ab1 | |||
2003005e56 | |||
fda8fb7182 | |||
cf6039b279 | |||
41e552dce5 | |||
90043b5806 | |||
9eb74b5a8d | |||
9cc60a136b | |||
78eb1e779c | |||
8db2d8508e | |||
3f1ece26ec | |||
d1912a44c6 | |||
36a05eb390 | |||
4f39ea1ad8 | |||
a241cc1d61 |
6
.gitignore
vendored
@ -15,7 +15,9 @@ build/
|
||||
.settings/
|
||||
|
||||
flutter_export_environment.sh
|
||||
.flutter-plugins-dependencies
|
||||
|
||||
key.properties
|
||||
premium_features_manager.class.dart
|
||||
pubspec.lock
|
||||
.secrets.dart
|
||||
pubspec.lock
|
||||
google-services.json
|
||||
|
@ -4,9 +4,5 @@ ENV ANDROID_HOME=/workspace/android-sdk \
|
||||
FLUTTER_ROOT=/workspace/flutter \
|
||||
FLUTTER_HOME=/workspace/flutter
|
||||
|
||||
USER root
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install build-essential libkrb5-dev gcc make gradle openjdk-8-jdk && \
|
||||
apt-get clean && \
|
||||
apt-get -y autoremove
|
||||
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
|
||||
&& sdk install java 8.0.242.j9-adpt"
|
@ -3,12 +3,12 @@ image:
|
||||
|
||||
tasks:
|
||||
- before: |
|
||||
export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
||||
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
||||
mkdir -p /home/gitpod/.android
|
||||
touch /home/gitpod/.android/repositories.cfg
|
||||
init: |
|
||||
echo "Installing Flutter SDK..."
|
||||
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.9.1+hotfix.4-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
|
||||
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.12.13+hotfix.7-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
|
||||
echo "Installing Android SDK..."
|
||||
mkdir -p /workspace/android-sdk && cd /workspace/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip
|
||||
/workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
|
||||
@ -18,7 +18,6 @@ tasks:
|
||||
flutter doctor --android-licenses
|
||||
flutter pub get
|
||||
command: |
|
||||
flutter pub upgrade
|
||||
echo "Ready to go!"
|
||||
flutter doctor
|
||||
vscode:
|
||||
|
BIN
.gradle/6.0.1/fileChanges/last-build.bin
Normal file
BIN
.gradle/6.0.1/fileHashes/fileHashes.lock
Normal file
0
.gradle/6.0.1/gc.properties
Normal file
13
README.md
@ -1,15 +1,18 @@
|
||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||
# HA Client
|
||||
## Native Android client for Home Assistant
|
||||
### With notifications and Lovelace UI support
|
||||
|
||||
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
||||
Visit [ha-client.app](http://ha-client.app/) for more info.
|
||||
|
||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||
|
||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn)
|
||||
Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
||||
|
||||
#### Pre-release CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||
|
||||
#### Stable CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/latest_build)
|
||||
#### Beta CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
||||
#### Pre-release CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
||||
|
@ -78,10 +78,11 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-core:16.0.8'
|
||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||
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: 'io.fabric'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
@ -1,64 +0,0 @@
|
||||
{
|
||||
"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-uqmkibhf361828od1982o2jhl0n3m0ov.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.keyboardcrumbs.haclient",
|
||||
"certificate_hash": "bebe4d970fbebf0bff2c93244fdc7fcbcefb3470"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "441874387819-5q7vmimci4s2jl3v0ncugv1ocp4m48nb.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.keyboardcrumbs.haclient",
|
||||
"certificate_hash": "0ea12348468be44bc2aa5792ee7e8924c633da81"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "441874387819-joi8plo5345ebt8i1dug27u2aenv5tg7.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.keyboardcrumbs.haclient",
|
||||
"certificate_hash": "fcbc805d965ccf6a4d5417398d191edc9c9890b0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
@ -5,9 +5,13 @@
|
||||
android:required="false" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<!--
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
-->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
||||
|
||||
@ -17,11 +21,16 @@
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="ha_notify" />
|
||||
@ -33,13 +42,12 @@
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- 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).
|
||||
<meta-data
|
||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||
android:value="true" />-->
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background" />
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@ -48,16 +56,19 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="haclient"
|
||||
android:host="auth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name="rekab.app.background_locator.LocatorBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"/>
|
||||
<service android:name="rekab.app.background_locator.LocatorService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="true"/>
|
||||
<service android:name="rekab.app.background_locator.IsolateHolderService"
|
||||
android:permission="android.permission.FOREGROUND_SERVICE"
|
||||
android:exported="true"/>
|
||||
|
||||
<!--
|
||||
<service
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
@ -72,5 +83,6 @@
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
-->
|
||||
</application>
|
||||
</manifest>
|
||||
|
@ -1,20 +0,0 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import io.flutter.app.FlutterApplication;
|
||||
import io.flutter.plugin.common.PluginRegistry;
|
||||
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
import be.tramckrijte.workmanager.WorkmanagerPlugin;
|
||||
|
||||
public class Application extends FlutterApplication implements PluginRegistrantCallback {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
WorkmanagerPlugin.setPluginRegistrantCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWith(PluginRegistry registry) {
|
||||
GeneratedPluginRegistrant.registerWith(registry);
|
||||
}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.os.Bundle;
|
||||
import io.flutter.app.FlutterActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
import io.flutter.plugins.share.FlutterShareReceiverActivity;
|
||||
|
||||
public class MainActivity extends FlutterShareReceiverActivity {
|
||||
public class MainActivity extends FlutterActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
GeneratedPluginRegistrant.registerWith(this);
|
||||
}
|
||||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
}
|
||||
|
||||
}
|
||||
|
27
android/app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
|
||||
<path
|
||||
android:fillColor="#709ac1"
|
||||
android:fillAlpha="0"
|
||||
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.12"
|
||||
android:pathData="M 70.506 38.389 L 108 72.466 L 108 108 L 77 108 L 35.066 72.466 L 38.373 63.769 L 36.268 50.216 L 43.335 44.523 L 51.841 34.578 L 63.096 42.478 L 68.586 42.478 L 70.506 38.389 Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.12"
|
||||
android:pathData="M 28.979 53.708 L 47.736 67.31 L 38.373 58.563 L 36.268 51.52 L 28.979 53.708 Z" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M 77.131 54.24 L 72.878 54.24 L 72.878 72.415 L 56.339 72.415 L 56.339 64.85 L 62.931 58.511 L 64.609 58.784 C 67.349 58.784 69.57 56.649 69.57 54.013 C 69.57 51.378 67.349 49.242 64.609 49.242 C 61.868 49.242 59.647 51.378 59.647 54.013 L 59.883 55.626 L 56.339 59.079 L 56.339 46.63 C 57.898 45.812 58.938 44.244 58.938 42.427 C 58.938 39.792 56.717 37.656 53.976 37.656 C 51.236 37.656 49.015 39.792 49.015 42.427 C 49.015 44.244 50.054 45.812 51.614 46.63 L 51.614 59.079 L 48.07 55.626 L 48.306 54.013 C 48.306 51.378 46.084 49.242 43.344 49.242 C 40.604 49.242 38.383 51.378 38.383 54.013 C 38.383 56.648 40.604 58.784 43.344 58.784 L 45.022 58.511 L 51.614 64.85 L 51.614 72.415 L 35.075 72.415 L 35.075 54.24 L 30.94 54.24 C 29.948 54.24 28.979 54.24 28.979 53.763 C 29.003 53.263 29.995 52.309 31.011 51.332 L 51.614 31.522 C 52.393 30.772 53.197 30 53.976 30 C 54.756 30 55.559 30.772 56.339 31.522 L 65.79 40.609 L 65.79 38.338 L 70.515 38.338 L 70.515 45.153 L 77.084 51.469 C 78.029 52.377 78.997 53.309 79.021 53.786 C 79.021 54.24 78.076 54.24 77.131 54.24 Z M 43.344 51.969 C 43.908 51.969 44.449 52.184 44.848 52.567 C 45.247 52.951 45.471 53.471 45.471 54.013 C 45.471 54.555 45.247 55.076 44.848 55.459 C 44.449 55.842 43.908 56.058 43.344 56.058 C 42.78 56.058 42.239 55.842 41.841 55.459 C 41.442 55.076 41.218 54.555 41.218 54.013 C 41.218 53.471 41.442 52.951 41.841 52.567 C 42.239 52.184 42.78 51.969 43.344 51.969 Z M 64.609 51.969 C 65.79 51.969 66.735 52.877 66.735 54.013 C 66.735 55.149 65.79 56.058 64.609 56.058 C 64.045 56.058 63.504 55.842 63.105 55.459 C 62.706 55.076 62.482 54.555 62.482 54.013 C 62.482 53.471 62.706 52.951 63.105 52.567 C 63.504 52.184 64.045 51.969 64.609 51.969 Z M 53.976 40.382 C 55.158 40.382 56.103 41.291 56.103 42.427 C 56.103 43.563 55.158 44.472 53.976 44.472 C 52.795 44.472 51.85 43.563 51.85 42.427 C 51.85 41.291 52.795 40.382 53.976 40.382 Z" />
|
||||
</group>
|
||||
</vector>
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
<item android:drawable="@color/main_color" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/main_color"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/main_color"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.8 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 9.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 20 KiB |
@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="main_color">#709AC1</color>
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
@ -2,11 +2,15 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
classpath 'io.fabric.tools:gradle:1.26.1'
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +18,9 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,4 +2,5 @@ org.gradle.jvmargs=-Xmx2g
|
||||
org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=true
|
||||
android.enableR8=true
|
||||
|
1
android/settings_aar.gradle
Normal file
@ -0,0 +1 @@
|
||||
include ':app'
|
61
assets/html/cameraLiveView.html
Normal file
@ -0,0 +1,61 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
widows: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var messageChannel = '{{message_channel}}';
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<video id="screen" width="100%" controls></video>
|
||||
<script>
|
||||
if (Hls.isSupported()) {
|
||||
var video = document.getElementById('screen');
|
||||
var hls = new Hls();
|
||||
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||
if (data.fatal) {
|
||||
switch(data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
// try to recover network error
|
||||
console.log("fatal network error encountered, try to recover");
|
||||
hls.startLoad();
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.log("fatal media error encountered, try to recover");
|
||||
hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
// cannot recover
|
||||
hls.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
// bind them together
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
|
||||
console.log("video and hls.js are now bound together !");
|
||||
hls.loadSource("{{stream_url}}");
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
|
||||
console.log("manifest loaded, found " + data.levels.length + " quality level");
|
||||
video.play();
|
||||
video.onloadedmetadata = function() {
|
||||
window[messageChannel].postMessage(document.body.clientWidth / video.offsetHeight);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
28
assets/html/cameraView.html
Normal file
@ -0,0 +1,28 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
widows: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var messageChannel = '{{message_channel}}';
|
||||
window.onload = function() {
|
||||
var img = document.getElementById('screen');
|
||||
if (img) {
|
||||
window[messageChannel].postMessage(document.body.clientWidth / img.offsetHeight);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<img id="screen" src="{{stream_url}}">
|
||||
</body>
|
||||
</html>
|
@ -11,6 +11,27 @@ window.externalApp.getExternalAuth = function(options) {
|
||||
setTimeout(function(){
|
||||
console.log("Calling a callback");
|
||||
window[options.callback](true, responseData);
|
||||
}, 500);
|
||||
}, 900);
|
||||
}
|
||||
};
|
||||
};
|
||||
/*
|
||||
window.externalApp.externalBus = function(message) {
|
||||
console.log("External bus message: " + message);
|
||||
var messageObj = JSON.parse(message);
|
||||
if (messageObj.type == "config/get") {
|
||||
var responseData = {
|
||||
id: messageObj.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: {
|
||||
hasSettingsScreen: true
|
||||
}
|
||||
};
|
||||
setTimeout(function(){
|
||||
window.externalBus(responseData);
|
||||
}, 500);
|
||||
} else if (messageObj.type == "config_screen/show") {
|
||||
HAClient.postMessage('show-settings');
|
||||
}
|
||||
};
|
||||
*/
|
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 24 KiB |
62
lib/cards/alarm_panel_card.dart
Normal file
@ -0,0 +1,62 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class AlarmPanelCard extends StatelessWidget {
|
||||
final AlarmPanelCardData card;
|
||||
|
||||
const AlarmPanelCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeader(
|
||||
name: card.name ?? "",
|
||||
subtitle: Text("${card.entity.entity.displayState}",
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
EntityIcon(
|
||||
size: 50.0,
|
||||
),
|
||||
Container(
|
||||
width: 26.0,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
alignment: Alignment.centerRight,
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical")),
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: card.entity.entity.entityId))
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
));
|
||||
body.add(
|
||||
AlarmControlPanelControlsWidget(
|
||||
extended: true,
|
||||
states: card.states,
|
||||
)
|
||||
);
|
||||
return CardWrapper(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
handleTap: null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: body
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
196
lib/cards/badges.dart
Normal file
@ -0,0 +1,196 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Badges extends StatelessWidget {
|
||||
final BadgesData badges;
|
||||
|
||||
const Badges({Key key, this.badges}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow();
|
||||
|
||||
if (entitiesToShow.isNotEmpty) {
|
||||
if (ConnectionManager().scrollBadges) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(height: 112),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: entitiesToShow.map((entity) =>
|
||||
EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||
child: BadgeWidget(),
|
||||
),
|
||||
handleTap: true,
|
||||
)).toList()
|
||||
),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10.0,
|
||||
runSpacing: 5,
|
||||
children: entitiesToShow.map((entity) =>
|
||||
EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: BadgeWidget(),
|
||||
handleTap: true,
|
||||
)).toList(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
class BadgeWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
Widget badgeIcon;
|
||||
String onBadgeTextValue;
|
||||
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
||||
switch (entityModel.entityWrapper.entity.domain) {
|
||||
case "sun":
|
||||
{
|
||||
IconData iconData;
|
||||
if (entityModel.entityWrapper.entity.state == "below_horizon") {
|
||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc);
|
||||
} else {
|
||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8);
|
||||
}
|
||||
badgeIcon = Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Icon(
|
||||
iconData,
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "camera":
|
||||
case "media_player":
|
||||
case "binary_sensor":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
imagePadding: EdgeInsets.all(0.0),
|
||||
iconPadding: EdgeInsets.all(10),
|
||||
color: Theme.of(context).textTheme.body2.color
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
imagePadding: EdgeInsets.all(0.0),
|
||||
iconPadding: EdgeInsets.all(10),
|
||||
color: Theme.of(context).textTheme.body2.color
|
||||
);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||
badgeIcon = Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.body1
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Widget onBadgeText;
|
||||
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
||||
onBadgeText = Container(width: 0.0, height: 0.0);
|
||||
} else {
|
||||
onBadgeText = Container(
|
||||
constraints: BoxConstraints(maxWidth: 50),
|
||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||
child: Text("$onBadgeTextValue",
|
||||
style: Theme.of(context).textTheme.overline.copyWith(
|
||||
color: HAClientTheme().getOnBadgeTextColor()
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis
|
||||
),
|
||||
decoration: new BoxDecoration(
|
||||
color: iconColor,
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
)
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
overflow: Overflow.visible,
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).cardColor,
|
||||
// The border you want
|
||||
border: Border.all(
|
||||
width: 2.0,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 41,
|
||||
height: 41,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.center,
|
||||
child: badgeIcon,
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
bottom: -6,
|
||||
child: onBadgeText
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 45),
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.displayName}",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.caption.copyWith(
|
||||
fontSize: 10
|
||||
),
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
onTap: () => entityModel.entityWrapper.handleTap(),
|
||||
onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(),
|
||||
onLongPress: () => entityModel.entityWrapper.handleHold(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,64 +1,622 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class HACard {
|
||||
List<EntityWrapper> entities = [];
|
||||
List<HACard> childCards = [];
|
||||
EntityWrapper linkedEntityWrapper;
|
||||
String name;
|
||||
String id;
|
||||
String type;
|
||||
bool showName;
|
||||
bool showState;
|
||||
bool showEmpty;
|
||||
int columnsCount;
|
||||
List stateFilter;
|
||||
List states;
|
||||
List conditions;
|
||||
String content;
|
||||
String unit;
|
||||
int min;
|
||||
int max;
|
||||
Map severity;
|
||||
class CardData {
|
||||
|
||||
HACard({
|
||||
this.name,
|
||||
this.id,
|
||||
this.linkedEntityWrapper,
|
||||
this.columnsCount: 4,
|
||||
this.showName: true,
|
||||
this.showState: true,
|
||||
this.stateFilter: const [],
|
||||
this.showEmpty: true,
|
||||
this.content,
|
||||
this.states,
|
||||
this.conditions: const [],
|
||||
this.unit,
|
||||
this.min,
|
||||
this.max,
|
||||
this.severity,
|
||||
@required this.type
|
||||
}) {
|
||||
if (this.columnsCount <= 0) {
|
||||
this.columnsCount = 4;
|
||||
String type;
|
||||
List<EntityWrapper> entities = [];
|
||||
List conditions;
|
||||
bool showEmpty;
|
||||
List stateFilter;
|
||||
bool stateColor = true;
|
||||
|
||||
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
|
||||
|
||||
factory CardData.parse(rawData) {
|
||||
try {
|
||||
if (rawData['type'] == null) {
|
||||
rawData['type'] = CardType.ENTITIES;
|
||||
} else if (!(rawData['type'] is String)) {
|
||||
return CardData(null);
|
||||
}
|
||||
switch (rawData['type']) {
|
||||
case CardType.ENTITIES:
|
||||
case CardType.HISTORY_GRAPH:
|
||||
case CardType.MAP:
|
||||
case CardType.PICTURE_GLANCE:
|
||||
case CardType.SENSOR:
|
||||
case CardType.ENTITY:
|
||||
case CardType.WEATHER_FORECAST:
|
||||
case CardType.PLANT_STATUS:
|
||||
if (rawData['entity'] != null) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
return EntitiesCardData(rawData);
|
||||
break;
|
||||
case CardType.ALARM_PANEL:
|
||||
return AlarmPanelCardData(rawData);
|
||||
break;
|
||||
case CardType.ENTITY_BUTTON:
|
||||
case CardType.LIGHT:
|
||||
case CardType.BUTTON:
|
||||
case CardType.PICTURE_ENTITY:
|
||||
return ButtonCardData(rawData);
|
||||
break;
|
||||
case CardType.CONDITIONAL:
|
||||
return CardData.parse(rawData['card']);
|
||||
break;
|
||||
case CardType.ENTITY_FILTER:
|
||||
Map cardData = Map.from(rawData);
|
||||
cardData.remove('type');
|
||||
if (rawData.containsKey('card')) {
|
||||
cardData.addAll(rawData['card']);
|
||||
}
|
||||
cardData['type'] ??= CardType.ENTITIES;
|
||||
return CardData.parse(cardData);
|
||||
break;
|
||||
case CardType.GAUGE:
|
||||
return GaugeCardData(rawData);
|
||||
break;
|
||||
case CardType.GLANCE:
|
||||
case CardType.THERMOSTAT:
|
||||
if (rawData['entity'] != null) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
return GlanceCardData(rawData);
|
||||
break;
|
||||
case CardType.HORIZONTAL_STACK:
|
||||
return HorizontalStackCardData(rawData);
|
||||
break;
|
||||
case CardType.VERTICAL_STACK:
|
||||
return VerticalStackCardData(rawData);
|
||||
break;
|
||||
case CardType.MARKDOWN:
|
||||
return MarkdownCardData(rawData);
|
||||
break;
|
||||
case CardType.MEDIA_CONTROL:
|
||||
return MediaControlCardData(rawData);
|
||||
break;
|
||||
case CardType.BADGES:
|
||||
return BadgesData(rawData);
|
||||
break;
|
||||
default:
|
||||
return CardData(null);
|
||||
}
|
||||
} catch (error, stacktrace) {
|
||||
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
|
||||
return ErrorCardData(rawData);
|
||||
}
|
||||
}
|
||||
|
||||
CardData(rawData) {
|
||||
if (rawData != null && rawData is Map) {
|
||||
type = rawData['type'];
|
||||
conditions = rawData['conditions'] ?? [];
|
||||
showEmpty = rawData['show_empty'] ?? true;
|
||||
stateFilter = rawData['state_filter'] ?? [];
|
||||
} else {
|
||||
type = CardType.UNKNOWN;
|
||||
conditions = [];
|
||||
showEmpty = true;
|
||||
stateFilter = [];
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildCardWidget() {
|
||||
return UnsupportedCard(card: this);
|
||||
}
|
||||
|
||||
List<EntityWrapper> getEntitiesToShow() {
|
||||
return entities.where((entityWrapper) {
|
||||
if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) {
|
||||
if (entityWrapper.entity.isHidden) {
|
||||
return false;
|
||||
}
|
||||
if (stateFilter.isNotEmpty) {
|
||||
return stateFilter.contains(entityWrapper.entity.state);
|
||||
List currentStateFilter;
|
||||
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
|
||||
currentStateFilter = entityWrapper.stateFilter;
|
||||
} else {
|
||||
currentStateFilter = stateFilter;
|
||||
}
|
||||
return true;
|
||||
bool showByFilter = currentStateFilter.isEmpty;
|
||||
for (var allowedState in currentStateFilter) {
|
||||
if (allowedState is String && allowedState == entityWrapper.entity.state) {
|
||||
showByFilter = true;
|
||||
break;
|
||||
} else if (allowedState is Map) {
|
||||
try {
|
||||
var tmpVal = allowedState['attribute'] != null ? entityWrapper.entity.getAttribute(allowedState['attribute']) : entityWrapper.entity.state;
|
||||
var valToCompareWith = allowedState['value'];
|
||||
var valToCompare;
|
||||
if (valToCompareWith is! String && tmpVal is String) {
|
||||
valToCompare = double.tryParse(tmpVal);
|
||||
} else {
|
||||
valToCompare = tmpVal;
|
||||
}
|
||||
if (valToCompare != null) {
|
||||
bool result;
|
||||
switch (allowedState['operator']) {
|
||||
case '<=': { result = valToCompare <= valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '<': { result = valToCompare < valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '>=': { result = valToCompare >= valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '>': { result = valToCompare > valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '!=': { result = valToCompare != valToCompareWith;}
|
||||
break;
|
||||
|
||||
case 'regex': {
|
||||
RegExp regExp = RegExp(valToCompareWith.toString());
|
||||
result = regExp.hasMatch(valToCompare.toString());
|
||||
}
|
||||
break;
|
||||
|
||||
default: {
|
||||
result = valToCompare == valToCompareWith;
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
showByFilter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState: $e', stacktrace: stacktrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
return showByFilter;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return CardWidget(
|
||||
card: this,
|
||||
);
|
||||
}
|
||||
|
||||
class BadgesData extends CardData {
|
||||
|
||||
String title;
|
||||
String icon;
|
||||
bool showHeaderToggle;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return Badges(badges: this);
|
||||
}
|
||||
|
||||
BadgesData(rawData) : super(rawData) {
|
||||
if (rawData['badges'] is List) {
|
||||
rawData['badges'].forEach((dynamic rawBadge) {
|
||||
if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge)));
|
||||
} else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) {
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(rawBadge['entity']),
|
||||
overrideName: rawBadge["name"],
|
||||
overrideIcon: rawBadge["icon"],
|
||||
)
|
||||
);
|
||||
} else if (rawBadge is Map && rawBadge.containsKey('entities')) {
|
||||
_parseEntities(rawBadge);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
void _parseEntities(rawData) {
|
||||
var rawEntities = rawData['entities'] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(rawEntity),
|
||||
stateFilter: rawData['state_filter'] ?? [],
|
||||
));
|
||||
}
|
||||
} else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
overrideName: rawEntity["name"],
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntitiesCardData extends CardData {
|
||||
|
||||
String title;
|
||||
String icon;
|
||||
bool showHeaderToggle;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return EntitiesCard(card: this);
|
||||
}
|
||||
|
||||
EntitiesCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData['title'];
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
stateColor = rawData['state_color'] ?? false;
|
||||
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
||||
//Parsing entities
|
||||
var rawEntities = rawData['entities'] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (rawEntity["type"] == "divider") {
|
||||
entities.add(EntityWrapper(entity: Entity.divider()));
|
||||
} else if (rawEntity["type"] == "section") {
|
||||
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
|
||||
};
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: Entity.callService(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
service: rawEntity["service"],
|
||||
actionName: rawEntity["action_name"]
|
||||
),
|
||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (rawEntity["type"] == "weblink") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.navigate,
|
||||
"service": rawEntity["url"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
entities.add(EntityWrapper(
|
||||
entity: Entity.weblink(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
url: rawEntity["url"]
|
||||
),
|
||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||
overrideName: rawEntity["name"],
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AlarmPanelCardData extends CardData {
|
||||
|
||||
String name;
|
||||
List<dynamic> states;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return AlarmPanelCard(card: this);
|
||||
}
|
||||
|
||||
AlarmPanelCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
states = rawData['states'];
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
stateColor: true,
|
||||
overrideName: name
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ButtonCardData extends CardData {
|
||||
|
||||
String name;
|
||||
String icon;
|
||||
bool showName;
|
||||
bool showIcon;
|
||||
double iconHeightPx = 0;
|
||||
double iconHeightRem = 0;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return EntityButtonCard(card: this);
|
||||
}
|
||||
|
||||
ButtonCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
showName = rawData['show_name'] ?? true;
|
||||
showIcon = rawData['show_icon'] ?? true;
|
||||
stateColor = rawData['state_color'] ?? true;
|
||||
var rawHeight = rawData['icon_height'];
|
||||
if (rawHeight != null && rawHeight is String) {
|
||||
if (rawHeight.contains('px')) {
|
||||
iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0;
|
||||
} else if (rawHeight.contains('rem')) {
|
||||
iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;
|
||||
} else if (rawHeight.contains('em')) {
|
||||
iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0;
|
||||
}
|
||||
}
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
overrideName: name,
|
||||
overrideIcon: icon,
|
||||
stateColor: stateColor,
|
||||
uiAction: EntityUIAction(
|
||||
rawEntityData: rawData
|
||||
)
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
} else if (entitiId == null) {
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: Entity.ghost(
|
||||
name,
|
||||
icon,
|
||||
),
|
||||
stateColor: stateColor,
|
||||
uiAction: EntityUIAction(
|
||||
rawEntityData: rawData
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GaugeCardData extends CardData {
|
||||
|
||||
String name;
|
||||
String unit;
|
||||
double min;
|
||||
double max;
|
||||
Map severity;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return GaugeCard(card: this);
|
||||
}
|
||||
|
||||
GaugeCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
unit = rawData['unit'];
|
||||
if (rawData['min'] is int) {
|
||||
min = rawData['min'].toDouble();
|
||||
} else if (rawData['min'] is double) {
|
||||
min = rawData['min'];
|
||||
} else {
|
||||
min = 0;
|
||||
}
|
||||
if (rawData['max'] is int) {
|
||||
max = rawData['max'].toDouble();
|
||||
} else if (rawData['max'] is double) {
|
||||
max = rawData['max'];
|
||||
} else {
|
||||
max = 100;
|
||||
}
|
||||
severity = rawData['severity'];
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"] is List ? rawData["entity"][0] : rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
overrideName: name
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GlanceCardData extends CardData {
|
||||
|
||||
String title;
|
||||
bool showName;
|
||||
bool showIcon;
|
||||
bool showState;
|
||||
bool stateColor;
|
||||
int columnsCount;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return GlanceCard(card: this);
|
||||
}
|
||||
|
||||
GlanceCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData["title"];
|
||||
showName = rawData['show_name'] ?? true;
|
||||
showIcon = rawData['show_icon'] ?? true;
|
||||
showState = rawData['show_state'] ?? true;
|
||||
stateColor = rawData['state_color'] ?? true;
|
||||
columnsCount = rawData['columns'] ?? 4;
|
||||
//Parsing entities
|
||||
var rawEntities = rawData["entities"] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
stateColor: stateColor,
|
||||
overrideName: rawEntity["name"],
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class HorizontalStackCardData extends CardData {
|
||||
|
||||
List<CardData> childCards;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return HorizontalStackCard(card: this);
|
||||
}
|
||||
|
||||
HorizontalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
}).toList();
|
||||
} else {
|
||||
childCards = [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class VerticalStackCardData extends CardData {
|
||||
|
||||
List<CardData> childCards;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return VerticalStackCard(card: this);
|
||||
}
|
||||
|
||||
VerticalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
}).toList();
|
||||
} else {
|
||||
childCards = [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MarkdownCardData extends CardData {
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return MarkdownCard(card: this);
|
||||
}
|
||||
|
||||
MarkdownCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData['title'];
|
||||
content = rawData['content'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MediaControlCardData extends CardData {
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return MediaControlsCard(card: this);
|
||||
}
|
||||
|
||||
MediaControlCardData(rawData) : super(rawData) {
|
||||
var entitiId = rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ErrorCardData extends CardData {
|
||||
|
||||
String cardConfig;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return ErrorCard(card: this);
|
||||
}
|
||||
|
||||
ErrorCardData(rawData) : super(rawData) {
|
||||
cardConfig = '$rawData';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,363 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class CardWidget extends StatelessWidget {
|
||||
|
||||
final HACard card;
|
||||
|
||||
const CardWidget({
|
||||
Key key,
|
||||
this.card
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.linkedEntityWrapper!= null) {
|
||||
if (card.linkedEntityWrapper.entity.isHidden) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (card.conditions.isNotEmpty) {
|
||||
bool showCardByConditions = true;
|
||||
for (var condition in card.conditions) {
|
||||
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
||||
if (conditionEntity != null &&
|
||||
((condition['state'] != null && conditionEntity.state != condition['state']) ||
|
||||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
|
||||
) {
|
||||
showCardByConditions = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!showCardByConditions) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
switch (card.type) {
|
||||
|
||||
case CardType.ENTITIES: {
|
||||
return _buildEntitiesCard(context);
|
||||
}
|
||||
|
||||
case CardType.GLANCE: {
|
||||
return _buildGlanceCard(context);
|
||||
}
|
||||
|
||||
case CardType.MEDIA_CONTROL: {
|
||||
return _buildMediaControlsCard(context);
|
||||
}
|
||||
|
||||
case CardType.ENTITY_BUTTON: {
|
||||
return _buildEntityButtonCard(context);
|
||||
}
|
||||
|
||||
case CardType.GAUGE: {
|
||||
return _buildGaugeCard(context);
|
||||
}
|
||||
|
||||
/* case CardType.LIGHT: {
|
||||
return _buildLightCard(context);
|
||||
}*/
|
||||
|
||||
case CardType.MARKDOWN: {
|
||||
return _buildMarkdownCard(context);
|
||||
}
|
||||
|
||||
case CardType.ALARM_PANEL: {
|
||||
return _buildAlarmPanelCard(context);
|
||||
}
|
||||
|
||||
case CardType.HORIZONTAL_STACK: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
|
||||
children.add(
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: card.build(context),
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
case CardType.VERTICAL_STACK: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
children.add(
|
||||
card.build(context)
|
||||
);
|
||||
});
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
default: {
|
||||
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
|
||||
return _buildEntitiesCard(context);
|
||||
} else {
|
||||
return _buildUnsupportedCard(context);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildEntitiesCard(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeader(name: card.name));
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
body.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
handleTap: true,
|
||||
child: entity.entity.buildDefaultWidget(context)
|
||||
),
|
||||
));
|
||||
});
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMarkdownCard(BuildContext context) {
|
||||
if (card.content == null) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeader(name: card.name));
|
||||
body.add(MarkdownBody(data: card.content));
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeader(
|
||||
name: card.name ?? "",
|
||||
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||
style: TextStyle(
|
||||
color: Colors.grey
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
EntityIcon(
|
||||
size: 50.0,
|
||||
),
|
||||
Container(
|
||||
width: 26.0,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
alignment: Alignment.centerRight,
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical")),
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
));
|
||||
body.add(
|
||||
AlarmControlPanelControlsWidget(
|
||||
extended: true,
|
||||
states: card.states,
|
||||
)
|
||||
);
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
handleTap: null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: body
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGlanceCard(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> rows = [];
|
||||
rows.add(CardHeader(name: card.name));
|
||||
|
||||
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||
|
||||
rows.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
List<Widget> buttons = [];
|
||||
double buttonWidth = constraints.maxWidth / columnsCount;
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
buttons.add(
|
||||
SizedBox(
|
||||
width: buttonWidth,
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: GlanceCardEntityContainer(
|
||||
showName: card.showName,
|
||||
showState: card.showState,
|
||||
),
|
||||
handleTap: true
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
return Wrap(
|
||||
//spacing: 5.0,
|
||||
//alignment: WrapAlignment.spaceEvenly,
|
||||
runSpacing: Sizes.doubleRowPadding,
|
||||
children: buttons,
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: rows
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaControlsCard(BuildContext context) {
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
handleTap: null,
|
||||
child: MediaPlayerWidget()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntityButtonCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
||||
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: EntityButtonCardBody(
|
||||
showName: card.showName,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGaugeCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
|
||||
card.linkedEntityWrapper.unitOfMeasurement;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: GaugeCardBody(
|
||||
min: card.min,
|
||||
max: card.max,
|
||||
severity: card.severity,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLightCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: LightCardBody(
|
||||
min: card.min,
|
||||
max: card.max,
|
||||
severity: card.severity,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnsupportedCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeader(name: card.name ?? ""));
|
||||
List<Widget> result = [];
|
||||
if (card.linkedEntityWrapper != null) {
|
||||
result.addAll(<Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
handleTap: true,
|
||||
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
|
||||
),
|
||||
)
|
||||
]);
|
||||
} else {
|
||||
result.addAll(<Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Text("'${card.type}' card is not supported yet"),
|
||||
),
|
||||
]);
|
||||
}
|
||||
body.addAll(result);
|
||||
return Card(
|
||||
child: new Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: body
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
79
lib/cards/entities_card.dart
Normal file
@ -0,0 +1,79 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class EntitiesCard extends StatelessWidget {
|
||||
final EntitiesCardData card;
|
||||
|
||||
const EntitiesCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
Widget headerSwitch;
|
||||
if (card.showHeaderToggle) {
|
||||
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
|
||||
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
|
||||
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
|
||||
}).map((EntityWrapper en) {
|
||||
return en.entity.entityId;
|
||||
}).toList();
|
||||
headerSwitch = Switch(
|
||||
value: headerToggleVal,
|
||||
onChanged: (val) {
|
||||
if (entitiesToToggle.isNotEmpty) {
|
||||
ConnectionManager().callService(
|
||||
domain: "homeassistant",
|
||||
service: val ? "turn_on" : "turn_off",
|
||||
entityId: entitiesToToggle
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
body.add(
|
||||
CardHeader(
|
||||
name: card.title,
|
||||
trailing: headerSwitch,
|
||||
emptyPadding: Sizes.rowPadding,
|
||||
leading: card.icon != null ? Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconName(card.icon),
|
||||
size: Sizes.iconSize,
|
||||
color: Theme.of(context).textTheme.headline.color
|
||||
) : null,
|
||||
)
|
||||
);
|
||||
body.addAll(
|
||||
entitiesToShow.map((EntityWrapper entity) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
handleTap: true,
|
||||
child: entity.entity.buildDefaultWidget(context)
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
return CardWrapper(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: Sizes.rightWidgetPadding,
|
||||
left: Sizes.leftWidgetPadding,
|
||||
bottom: Sizes.rowPadding,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: body
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
96
lib/cards/entity_button_card.dart
Normal file
@ -0,0 +1,96 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class EntityButtonCard extends StatelessWidget {
|
||||
|
||||
final ButtonCardData card;
|
||||
|
||||
EntityButtonCard({
|
||||
Key key, this.card
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = card.entity;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
} else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
|
||||
double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize);
|
||||
|
||||
Widget buttonIcon;
|
||||
if (!card.showIcon) {
|
||||
buttonIcon = Container(height: Sizes.rowPadding, width: 10);
|
||||
} else if (iconSize > 0) {
|
||||
buttonIcon = SizedBox(
|
||||
height: iconSize,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: EntityIcon(
|
||||
//padding: EdgeInsets.only(top: 6),
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
} else {
|
||||
buttonIcon = AspectRatio(
|
||||
aspectRatio: 2,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
child: EntityIcon(
|
||||
//padding: EdgeInsets.only(top: 6),
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return CardWrapper(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buttonIcon,
|
||||
_buildName(context)
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName(BuildContext context) {
|
||||
if (card.showName) {
|
||||
return EntityName(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
textStyle: Theme.of(context).textTheme.subhead,
|
||||
wordsWrap: true,
|
||||
textAlign: TextAlign.center
|
||||
);
|
||||
}
|
||||
return Container(width: 0, height: 0);
|
||||
}
|
||||
}
|
38
lib/cards/error_card.dart
Normal file
@ -0,0 +1,38 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ErrorCard extends StatelessWidget {
|
||||
final ErrorCardData card;
|
||||
|
||||
const ErrorCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CardWrapper(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: card.cardConfig));
|
||||
},
|
||||
child: Text('Copy card config'),
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
|
||||
},
|
||||
child: Text('Report issue'),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
201
lib/cards/gauge_card.dart
Normal file
@ -0,0 +1,201 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class GaugeCard extends StatelessWidget {
|
||||
|
||||
final GaugeCardData card;
|
||||
|
||||
GaugeCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = card.entity;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
entityWrapper.overrideName = card.name ??
|
||||
entityWrapper.displayName;
|
||||
entityWrapper.unitOfMeasurementOverride = card.unit ??
|
||||
entityWrapper.unitOfMeasurement;
|
||||
double fixedValue;
|
||||
double value = entityWrapper.entity.doubleState;
|
||||
if (value > card.max) {
|
||||
fixedValue = card.max.toDouble();
|
||||
} else if (value < card.min) {
|
||||
fixedValue = card.min.toDouble();
|
||||
} else {
|
||||
fixedValue = value;
|
||||
}
|
||||
|
||||
List<GaugeRange> ranges;
|
||||
Color currentColor;
|
||||
if (card.severity != null && card.severity["green"] is int && card.severity["red"] is int && card.severity["yellow"] is int) {
|
||||
List<RangeContainer> rangesList = <RangeContainer>[
|
||||
RangeContainer(card.severity["green"], HAClientTheme().getGreenGaugeColor()),
|
||||
RangeContainer(card.severity["red"], HAClientTheme().getRedGaugeColor()),
|
||||
RangeContainer(card.severity["yellow"], HAClientTheme().getYellowGaugeColor())
|
||||
];
|
||||
rangesList.sort((current, next) {
|
||||
if (current.startFrom > next.startFrom) {
|
||||
return 1;
|
||||
}
|
||||
if (current.startFrom < next.startFrom) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (fixedValue < rangesList[1].startFrom) {
|
||||
currentColor = rangesList[0].color;
|
||||
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
|
||||
currentColor = rangesList[1].color;
|
||||
} else {
|
||||
currentColor = rangesList[2].color;
|
||||
}
|
||||
|
||||
ranges = [
|
||||
GaugeRange(
|
||||
startValue: rangesList[0].startFrom.toDouble(),
|
||||
endValue: rangesList[1].startFrom.toDouble(),
|
||||
color: rangesList[0].color.withOpacity(0.1),
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
endWidth: 0.3,
|
||||
startWidth: 0.3
|
||||
),
|
||||
GaugeRange(
|
||||
startValue: rangesList[1].startFrom.toDouble(),
|
||||
endValue: rangesList[2].startFrom.toDouble(),
|
||||
color: rangesList[1].color.withOpacity(0.1),
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
endWidth: 0.3,
|
||||
startWidth: 0.3
|
||||
),
|
||||
GaugeRange(
|
||||
startValue: rangesList[2].startFrom.toDouble(),
|
||||
endValue: card.max.toDouble(),
|
||||
color: rangesList[2].color.withOpacity(0.1),
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
endWidth: 0.3,
|
||||
startWidth: 0.3
|
||||
)
|
||||
];
|
||||
}
|
||||
if (ranges == null) {
|
||||
currentColor = Theme.of(context).primaryColorDark;
|
||||
ranges = <GaugeRange>[
|
||||
GaugeRange(
|
||||
startValue: card.min.toDouble(),
|
||||
endValue: card.max.toDouble(),
|
||||
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
endWidth: 0.3,
|
||||
startWidth: 0.3,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
return CardWrapper(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: EntityModel(
|
||||
entityWrapper: entityWrapper,
|
||||
child: InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.8,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: <Widget>[
|
||||
IgnorePointer(
|
||||
ignoring: true,
|
||||
child: SfRadialGauge(
|
||||
axes: <RadialAxis>[
|
||||
RadialAxis(
|
||||
maximum: card.max.toDouble(),
|
||||
minimum: card.min.toDouble(),
|
||||
showLabels: false,
|
||||
useRangeColorForAxis: true,
|
||||
showTicks: false,
|
||||
canScaleToFit: true,
|
||||
ranges: ranges,
|
||||
axisLineStyle: AxisLineStyle(
|
||||
thickness: 0.3,
|
||||
thicknessUnit: GaugeSizeUnit.factor,
|
||||
color: Colors.transparent
|
||||
),
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
pointers: <GaugePointer>[
|
||||
RangePointer(
|
||||
value: fixedValue,
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
width: 0.3,
|
||||
color: currentColor,
|
||||
enableAnimation: true,
|
||||
animationType: AnimationType.bounceOut,
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
flex: 8,
|
||||
fit: FlexFit.tight,
|
||||
child: Container()
|
||||
),
|
||||
Flexible(
|
||||
flex: 6,
|
||||
fit: FlexFit.tight,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SimpleEntityState(
|
||||
padding: EdgeInsets.all(0),
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
fit: FlexFit.tight,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: EntityName(
|
||||
padding: EdgeInsets.all(0),
|
||||
textStyle: Theme.of(context).textTheme.subhead
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RangeContainer {
|
||||
final int startFrom;
|
||||
Color color;
|
||||
|
||||
RangeContainer(this.startFrom, this.color);
|
||||
}
|
126
lib/cards/glance_card.dart
Normal file
@ -0,0 +1,126 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class GlanceCard extends StatelessWidget {
|
||||
final GlanceCardData card;
|
||||
|
||||
const GlanceCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
int length = entitiesToShow.length;
|
||||
int rowsCount;
|
||||
int columnsCount;
|
||||
if (length == 0) {
|
||||
columnsCount = 0;
|
||||
rowsCount = 0;
|
||||
} else {
|
||||
columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||
rowsCount = (length / columnsCount).round();
|
||||
}
|
||||
List<TableRow> rows = [];
|
||||
for (int i = 0; i < rowsCount; i++) {
|
||||
int start = i*columnsCount;
|
||||
int end = start + math.min(columnsCount, length - start);
|
||||
List<Widget> rowChildren = [];
|
||||
rowChildren.addAll(entitiesToShow.sublist(
|
||||
start, end
|
||||
).map(
|
||||
(EntityWrapper entity){
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: _buildEntityContainer(context, entity),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
).toList()
|
||||
);
|
||||
while (rowChildren.length < columnsCount) {
|
||||
rowChildren.add(
|
||||
Container()
|
||||
);
|
||||
}
|
||||
rows.add(
|
||||
TableRow(
|
||||
children: rowChildren
|
||||
)
|
||||
);
|
||||
}
|
||||
return CardWrapper(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CardHeader(
|
||||
name: card.title,
|
||||
emptyPadding: Sizes.rowPadding,
|
||||
),
|
||||
Table(
|
||||
children: rows
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) {
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||
return MissedEntityWidget();
|
||||
} else if (entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
List<Widget> result = [];
|
||||
if (card.showName) {
|
||||
result.add(_buildName(context));
|
||||
}
|
||||
result.add(
|
||||
EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: Sizes.iconSize,
|
||||
)
|
||||
);
|
||||
if (card.showState) {
|
||||
result.add(_buildState());
|
||||
}
|
||||
|
||||
return InkResponse(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: result,
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName(BuildContext context) {
|
||||
return EntityName(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
wordsWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
textStyle: Theme.of(context).textTheme.body1,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildState() {
|
||||
return SimpleEntityState(
|
||||
textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
30
lib/cards/horizontal_srack_card.dart
Normal file
@ -0,0 +1,30 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class HorizontalStackCard extends StatelessWidget {
|
||||
final HorizontalStackCardData card;
|
||||
|
||||
const HorizontalStackCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
children = card.childCards.map((childCard) => Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: childCard.buildCardWidget()
|
||||
)
|
||||
).toList();
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
|
||||
}
|
33
lib/cards/markdown_card.dart
Normal file
@ -0,0 +1,33 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MarkdownCard extends StatelessWidget {
|
||||
final MarkdownCardData card;
|
||||
|
||||
const MarkdownCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.content == null) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
} else if (card.content == '***') {
|
||||
return Container(height: Sizes.rowPadding, width: 0.0,);
|
||||
}
|
||||
return CardWrapper(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
CardHeader(name: card.title),
|
||||
MarkdownBody(
|
||||
data: card.content,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
35
lib/cards/media_control_card.dart
Normal file
@ -0,0 +1,35 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MediaControlsCard extends StatelessWidget {
|
||||
final MediaControlCardData card;
|
||||
|
||||
const MediaControlsCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
} else if (card.entity.entity.domain == null || card.entity.entity.domain != 'media_player') {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: ErrorEntityWidget(
|
||||
text: '${card.entity.entity?.entityId} is not a media_player',
|
||||
),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
return CardWrapper(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
handleTap: null,
|
||||
child: MediaPlayerWidget()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
12
lib/cards/unsupported_card.dart
Normal file
@ -0,0 +1,12 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class UnsupportedCard extends StatelessWidget {
|
||||
final CardData card;
|
||||
|
||||
const UnsupportedCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container();
|
||||
}
|
||||
}
|
23
lib/cards/vertical_stack_card.dart
Normal file
@ -0,0 +1,23 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class VerticalStackCard extends StatelessWidget {
|
||||
final VerticalStackCardData card;
|
||||
|
||||
const VerticalStackCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: card.childCards.map<Widget>(
|
||||
(childCard) => childCard.buildCardWidget()
|
||||
).toList(),
|
||||
);
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -4,9 +4,11 @@ class CardHeader extends StatelessWidget {
|
||||
|
||||
final String name;
|
||||
final Widget trailing;
|
||||
final Widget leading;
|
||||
final Widget subtitle;
|
||||
final double emptyPadding;
|
||||
|
||||
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||
const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -14,14 +16,15 @@ class CardHeader extends StatelessWidget {
|
||||
if ((name != null) && (name.trim().length > 0)) {
|
||||
result = new ListTile(
|
||||
trailing: trailing,
|
||||
leading: leading,
|
||||
subtitle: subtitle,
|
||||
title: Text("$name",
|
||||
textAlign: TextAlign.left,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
||||
style: Theme.of(context).textTheme.headline),
|
||||
);
|
||||
} else {
|
||||
result = new Container(width: 0.0, height: 0.0);
|
||||
result = new Container(width: 0.0, height: emptyPadding);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
21
lib/cards/widgets/card_wrapper.widget.dart
Normal file
@ -0,0 +1,21 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class CardWrapper extends StatelessWidget {
|
||||
|
||||
final Widget child;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: child
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class EntityButtonCardBody extends StatelessWidget {
|
||||
|
||||
final bool showName;
|
||||
|
||||
EntityButtonCardBody({
|
||||
Key key, this.showName: true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
size: constraints.maxWidth / 2.5,
|
||||
);
|
||||
}
|
||||
),
|
||||
_buildName()
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName() {
|
||||
if (showName) {
|
||||
return EntityName(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
wordsWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: Sizes.nameFontSize,
|
||||
);
|
||||
}
|
||||
return Container(width: 0, height: 0);
|
||||
}
|
||||
}
|
@ -1,153 +0,0 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class GaugeCardBody extends StatefulWidget {
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final Map severity;
|
||||
|
||||
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||
|
||||
@override
|
||||
_GaugeCardBodyState createState() => _GaugeCardBodyState();
|
||||
}
|
||||
|
||||
class _GaugeCardBodyState extends State<GaugeCardBody> {
|
||||
|
||||
List<charts.Series> seriesList;
|
||||
|
||||
List<charts.Series<GaugeSegment, String>> _createData(double value) {
|
||||
double fixedValue;
|
||||
if (value > widget.max) {
|
||||
fixedValue = widget.max.toDouble();
|
||||
} else if (value < widget.min) {
|
||||
fixedValue = widget.min.toDouble();
|
||||
} else {
|
||||
fixedValue = value;
|
||||
}
|
||||
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
|
||||
Color mainColor;
|
||||
if (widget.severity != null) {
|
||||
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
|
||||
mainColor = Colors.red;
|
||||
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
|
||||
mainColor = Colors.amber;
|
||||
} else {
|
||||
mainColor = Colors.green;
|
||||
}
|
||||
} else {
|
||||
mainColor = Colors.green;
|
||||
}
|
||||
final data = [
|
||||
GaugeSegment('Main', toShow, mainColor),
|
||||
GaugeSegment('Rest', 100 - toShow, Colors.black45),
|
||||
];
|
||||
|
||||
return [
|
||||
charts.Series<GaugeSegment, String>(
|
||||
id: 'Segments',
|
||||
domainFn: (GaugeSegment segment, _) => segment.segment,
|
||||
measureFn: (GaugeSegment segment, _) => segment.value,
|
||||
colorFn: (GaugeSegment segment, _) => segment.color,
|
||||
// Set a label accessor to control the text of the arc label.
|
||||
labelAccessorFn: (GaugeSegment segment, _) =>
|
||||
segment.segment == 'Main' ? '${segment.value}' : null,
|
||||
data: data,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
overflow: Overflow.clip,
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double verticalOffset;
|
||||
if(constraints.maxWidth > 150.0) {
|
||||
verticalOffset = 0.2;
|
||||
} else if (constraints.maxWidth > 100.0) {
|
||||
verticalOffset = 0.3;
|
||||
} else {
|
||||
verticalOffset = 0.3;
|
||||
}
|
||||
return FractionallySizedBox(
|
||||
heightFactor: 2,
|
||||
widthFactor: 1,
|
||||
alignment: FractionalOffset(0,verticalOffset),
|
||||
child: charts.PieChart(
|
||||
_createData(entityWrapper.entity.doubleState),
|
||||
animate: false,
|
||||
defaultRenderer: charts.ArcRendererConfig(
|
||||
arcRatio: 0.4,
|
||||
startAngle: pi,
|
||||
arcLength: pi,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||
child: SimpleEntityState(
|
||||
//textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
bold: true,
|
||||
textAlign: TextAlign.center,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
fontSize: fontSize,
|
||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: fontSize),
|
||||
child: EntityName(
|
||||
fontSize: fontSize,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
textAlign: TextAlign.center,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GaugeSegment {
|
||||
final String segment;
|
||||
final double value;
|
||||
final charts.Color color;
|
||||
|
||||
GaugeSegment(this.segment, this.value, Color color)
|
||||
: this.color = charts.Color(
|
||||
r: color.red, g: color.green, b: color.blue, a: color.alpha);
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class GlanceCardEntityContainer extends StatelessWidget {
|
||||
|
||||
final bool showName;
|
||||
final bool showState;
|
||||
final bool nameInTheBottom;
|
||||
final double iconSize;
|
||||
final double nameFontSize;
|
||||
final bool wordsWrapInName;
|
||||
|
||||
GlanceCardEntityContainer({
|
||||
Key key,
|
||||
@required this.showName,
|
||||
@required this.showState,
|
||||
this.nameInTheBottom: false,
|
||||
this.iconSize: Sizes.iconSize,
|
||||
this.nameFontSize: Sizes.smallFontSize,
|
||||
this.wordsWrapInName: false
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
List<Widget> result = [];
|
||||
if (!nameInTheBottom) {
|
||||
if (showName) {
|
||||
result.add(_buildName());
|
||||
}
|
||||
} else {
|
||||
if (showState) {
|
||||
result.add(_buildState());
|
||||
}
|
||||
}
|
||||
result.add(
|
||||
EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
)
|
||||
);
|
||||
if (!nameInTheBottom) {
|
||||
if (showState) {
|
||||
result.add(_buildState());
|
||||
}
|
||||
} else {
|
||||
result.add(_buildName());
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: InkResponse(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: result,
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName() {
|
||||
return EntityName(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
wordsWrap: wordsWrapInName,
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: nameFontSize,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildState() {
|
||||
return SimpleEntityState(
|
||||
textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightCardBody extends StatefulWidget {
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final Map severity;
|
||||
|
||||
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LightCardBodyState createState() => _LightCardBodyState();
|
||||
}
|
||||
|
||||
class _LightCardBodyState extends State<LightCardBody> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
LightEntity entity = entityWrapper.entity;
|
||||
Logger.d("Light brightness: ${entity.brightness}");
|
||||
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: Container(
|
||||
//color: Colors.redAccent,
|
||||
child: SingleCircularSlider(
|
||||
255,
|
||||
entity.brightness ?? 0,
|
||||
baseColor: Colors.white,
|
||||
handlerColor: Colors.blue[200],
|
||||
selectionColor: Colors.blue[100],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
overflow: Overflow.clip,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||
child: SimpleEntityState(
|
||||
//textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
bold: true,
|
||||
textAlign: TextAlign.center,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
fontSize: fontSize,
|
||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: fontSize),
|
||||
child: EntityName(
|
||||
fontSize: fontSize,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
textAlign: TextAlign.center,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -36,51 +36,6 @@ class EntityState {
|
||||
|
||||
}
|
||||
|
||||
class EntityUIAction {
|
||||
static const moreInfo = 'more-info';
|
||||
static const toggle = 'toggle';
|
||||
static const callService = 'call-service';
|
||||
static const navigate = 'navigate';
|
||||
static const none = 'none';
|
||||
|
||||
String tapAction = EntityUIAction.moreInfo;
|
||||
String tapNavigationPath;
|
||||
String tapService;
|
||||
Map<String, dynamic> tapServiceData;
|
||||
String holdAction = EntityUIAction.none;
|
||||
String holdNavigationPath;
|
||||
String holdService;
|
||||
Map<String, dynamic> holdServiceData;
|
||||
|
||||
EntityUIAction({rawEntityData}) {
|
||||
if (rawEntityData != null) {
|
||||
if (rawEntityData["tap_action"] != null) {
|
||||
if (rawEntityData["tap_action"] is String) {
|
||||
tapAction = rawEntityData["tap_action"];
|
||||
} else {
|
||||
tapAction =
|
||||
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
|
||||
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
|
||||
tapService = rawEntityData["tap_action"]["service"];
|
||||
tapServiceData = rawEntityData["tap_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
if (rawEntityData["hold_action"] != null) {
|
||||
if (rawEntityData["hold_action"] is String) {
|
||||
holdAction = rawEntityData["hold_action"];
|
||||
} else {
|
||||
holdAction =
|
||||
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
|
||||
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
|
||||
holdService = rawEntityData["hold_action"]["service"];
|
||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CardType {
|
||||
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||
static const VERTICAL_STACK = "vertical-stack";
|
||||
@ -98,10 +53,17 @@ class CardType {
|
||||
static const IFRAME = "iframe";
|
||||
static const GAUGE = "gauge";
|
||||
static const ENTITY_BUTTON = "entity-button";
|
||||
static const ENTITY = "entity";
|
||||
static const BUTTON = "button";
|
||||
static const CONDITIONAL = "conditional";
|
||||
static const ALARM_PANEL = "alarm-panel";
|
||||
static const MARKDOWN = "markdown";
|
||||
static const LIGHT = "light";
|
||||
static const ENTITY_FILTER = "entity-filter";
|
||||
static const UNKNOWN = "unknown";
|
||||
static const HISTORY_GRAPH = "history-graph";
|
||||
static const PICTURE_GLANCE = "picture-glance";
|
||||
static const BADGES = "badges";
|
||||
}
|
||||
|
||||
class Sizes {
|
||||
@ -111,10 +73,10 @@ class Sizes {
|
||||
static const extendedWidgetHeight = 50.0;
|
||||
static const iconSize = 28.0;
|
||||
static const largeIconSize = 46.0;
|
||||
static const stateFontSize = 15.0;
|
||||
static const nameFontSize = 15.0;
|
||||
static const smallFontSize = 14.0;
|
||||
static const largeFontSize = 24.0;
|
||||
//static const stateFontSize = 15.0;
|
||||
//static const nameFontSize = 15.0;
|
||||
//static const smallFontSize = 14.0;
|
||||
//static const largeFontSize = 24.0;
|
||||
static const inputWidth = 160.0;
|
||||
static const rowPadding = 10.0;
|
||||
static const doubleRowPadding = rowPadding*2;
|
||||
|
@ -248,7 +248,9 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
||||
FlatButton(
|
||||
child: Text(
|
||||
"TRIGGER",
|
||||
style: TextStyle(color: Colors.redAccent)
|
||||
style: Theme.of(context).textTheme.subhead.copyWith(
|
||||
color: Theme.of(context).errorColor
|
||||
)
|
||||
),
|
||||
onPressed: () => _askToTrigger(entity),
|
||||
)
|
||||
|
@ -1,145 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class BadgeWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
double iconSize = 26.0;
|
||||
Widget badgeIcon;
|
||||
String onBadgeTextValue;
|
||||
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
|
||||
EntityColor.badgeColors["default"];
|
||||
switch (entityModel.entityWrapper.entity.domain) {
|
||||
case "sun":
|
||||
{
|
||||
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
||||
? Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
|
||||
size: iconSize,
|
||||
)
|
||||
: Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
|
||||
size: iconSize,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "camera":
|
||||
case "media_player":
|
||||
case "binary_sensor":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
color: Colors.black
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
color: Colors.black
|
||||
);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
double stateFontSize;
|
||||
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
|
||||
stateFontSize = 18.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
|
||||
stateFontSize = 15.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
|
||||
stateFontSize = 10.0;
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||
stateFontSize = 8.0;
|
||||
}
|
||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: stateFontSize),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Widget onBadgeText;
|
||||
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
||||
onBadgeText = Container(width: 0.0, height: 0.0);
|
||||
} else {
|
||||
onBadgeText = Container(
|
||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||
child: Text("$onBadgeTextValue",
|
||||
style: TextStyle(fontSize: 12.0, color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade),
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
//shape: BoxShape.circle,
|
||||
color: iconColor,
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
));
|
||||
}
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
|
||||
width: 50.0,
|
||||
height: 50.0,
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
// The border you want
|
||||
border: new Border.all(
|
||||
width: 2.0,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
overflow: Overflow.visible,
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
width: 46.0,
|
||||
height: 46.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
child: badgeIcon,
|
||||
),
|
||||
Positioned(
|
||||
//width: 50.0,
|
||||
bottom: -9.0,
|
||||
left: -10.0,
|
||||
right: -10.0,
|
||||
child: Center(
|
||||
child: onBadgeText,
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 60.0,
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.displayName}",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () =>
|
||||
eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity)));
|
||||
}
|
||||
}
|
@ -3,12 +3,16 @@ part of '../../main.dart';
|
||||
class CameraEntity extends Entity {
|
||||
|
||||
static const SUPPORT_ON_OFF = 1;
|
||||
static const SUPPORT_STREAM = 2;
|
||||
|
||||
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportOnOff => ((supportedFeatures &
|
||||
CameraEntity.SUPPORT_ON_OFF) ==
|
||||
CameraEntity.SUPPORT_ON_OFF);
|
||||
bool get supportStream => ((supportedFeatures &
|
||||
CameraEntity.SUPPORT_STREAM) ==
|
||||
CameraEntity.SUPPORT_STREAM);
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
|
@ -2,7 +2,9 @@ part of '../../../main.dart';
|
||||
|
||||
class CameraStreamView extends StatefulWidget {
|
||||
|
||||
CameraStreamView({Key key}) : super(key: key);
|
||||
final bool withControls;
|
||||
|
||||
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CameraStreamViewState createState() => _CameraStreamViewState();
|
||||
@ -10,45 +12,175 @@ class CameraStreamView extends StatefulWidget {
|
||||
|
||||
class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
|
||||
CameraEntity _entity;
|
||||
String _streamUrl = "";
|
||||
bool _isLoaded = false;
|
||||
double _aspectRatio = 1.33;
|
||||
String _webViewHtml;
|
||||
String _jsMessageChannelName = 'unknown';
|
||||
Completer _loading;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
CameraEntity _entity;
|
||||
bool started = false;
|
||||
String streamUrl = "";
|
||||
Future _loadResources() {
|
||||
if (_loading != null && !_loading.isCompleted) {
|
||||
Logger.d("[Camera Player] Resources loading is not finished yet");
|
||||
return _loading.future;
|
||||
}
|
||||
Logger.d("[Camera Player] Loading resources");
|
||||
_loading = Completer();
|
||||
_entity = EntityModel
|
||||
.of(context)
|
||||
.entityWrapper
|
||||
.entity;
|
||||
if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
|
||||
HomeAssistant().getCameraStream(_entity.entityId)
|
||||
.then((data) {
|
||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
|
||||
_webViewHtml = Uri.dataFromString(
|
||||
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||
mimeType: 'text/html',
|
||||
encoding: Encoding.getByName('utf-8')
|
||||
).toString();
|
||||
_loading.complete();
|
||||
});
|
||||
})
|
||||
.catchError((e) {
|
||||
if (e == 'start_stream_failed') {
|
||||
Logger.e("[Camera Player] Home Assistant failed starting stream. Forcing MJPEG: $e");
|
||||
_loadMJPEG().then((_) {
|
||||
_loading.complete();
|
||||
});
|
||||
} else {
|
||||
_loading.completeError(e);
|
||||
Logger.e("[Camera Player] Error loading stream: $e");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_loadMJPEG().then((_) {
|
||||
_loading.complete();
|
||||
});
|
||||
}
|
||||
return _loading.future;
|
||||
}
|
||||
|
||||
launchStream() {
|
||||
Launcher.launchURLInCustomTab(
|
||||
context: context,
|
||||
url: streamUrl
|
||||
Future _loadMJPEG() async {
|
||||
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
.entityId}?token=${_entity.attributes['access_token']}';
|
||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||
var file = await rootBundle.loadString('assets/html/cameraView.html');
|
||||
_webViewHtml = Uri.dataFromString(
|
||||
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||
mimeType: 'text/html',
|
||||
encoding: Encoding.getByName('utf-8')
|
||||
).toString();
|
||||
}
|
||||
|
||||
Widget _buildScreen() {
|
||||
Widget screenWidget;
|
||||
if (!_isLoaded) {
|
||||
screenWidget = Center(
|
||||
child: EntityPicture(
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
screenWidget = WebView(
|
||||
initialUrl: _webViewHtml,
|
||||
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
|
||||
debuggingEnabled: Logger.isInDebugMode,
|
||||
gestureNavigationEnabled: false,
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
javascriptChannels: {
|
||||
JavascriptChannel(
|
||||
name: _jsMessageChannelName,
|
||||
onMessageReceived: ((message) {
|
||||
Logger.d('[Camera Player] Message from page: $message');
|
||||
setState((){
|
||||
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
||||
});
|
||||
})
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
return AspectRatio(
|
||||
aspectRatio: _aspectRatio,
|
||||
child: screenWidget
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControls() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
iconSize: 40,
|
||||
color: Theme.of(context).accentColor,
|
||||
onPressed: _isLoaded ? () {
|
||||
setState(() {
|
||||
_isLoaded = false;
|
||||
});
|
||||
} : null,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.fullscreen),
|
||||
iconSize: 40,
|
||||
color: Theme.of(context).accentColor,
|
||||
onPressed: _isLoaded ? () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (conext) => FullScreenPage(
|
||||
child: EntityModel(
|
||||
child: CameraStreamView(
|
||||
withControls: false
|
||||
),
|
||||
handleTap: false,
|
||||
entityWrapper: EntityWrapper(
|
||||
entity: _entity
|
||||
),
|
||||
),
|
||||
),
|
||||
fullscreenDialog: true
|
||||
)
|
||||
).then((_){
|
||||
eventBus.fire(ShowEntityPageEvent(entityId: _entity.entityId));
|
||||
});
|
||||
} : null,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!started) {
|
||||
_entity = EntityModel
|
||||
.of(context)
|
||||
.entityWrapper
|
||||
.entity;
|
||||
started = true;
|
||||
if (!_isLoaded && (_loading == null || _loading.isCompleted)) {
|
||||
_loadResources().then((_) => setState((){ _isLoaded = true; }));
|
||||
}
|
||||
streamUrl = '${ConnectionManager().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(),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
if (widget.withControls) {
|
||||
return Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
_buildScreen(),
|
||||
_buildControls()
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return _buildScreen();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -10,9 +10,8 @@ class ClimateControlWidget extends StatefulWidget {
|
||||
|
||||
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
|
||||
bool _showPending = false;
|
||||
bool _temperaturePending = false;
|
||||
bool _changedHere = false;
|
||||
Timer _resetTimer;
|
||||
Timer _tempThrottleTimer;
|
||||
Timer _targetTempThrottleTimer;
|
||||
double _tmpTemperature = 0.0;
|
||||
@ -27,9 +26,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
bool _tmpAuxHeat = false;
|
||||
|
||||
void _resetVars(ClimateEntity entity) {
|
||||
_tmpTemperature = entity.temperature;
|
||||
_tmpTargetHigh = entity.targetHigh;
|
||||
_tmpTargetLow = entity.targetLow;
|
||||
if (!_temperaturePending) {
|
||||
_tmpTemperature = entity.temperature;
|
||||
_tmpTargetHigh = entity.targetHigh;
|
||||
_tmpTargetLow = entity.targetLow;
|
||||
}
|
||||
_tmpHVACMode = entity.state;
|
||||
_tmpFanMode = entity.fanMode;
|
||||
_tmpSwingMode = entity.swingMode;
|
||||
@ -38,7 +39,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_tmpAuxHeat = entity.auxHeat;
|
||||
_tmpTargetHumidity = entity.targetHumidity;
|
||||
|
||||
_showPending = false;
|
||||
_changedHere = false;
|
||||
}
|
||||
|
||||
@ -73,46 +73,44 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
|
||||
void _setTemperature(ClimateEntity entity) {
|
||||
if (_tempThrottleTimer!=null) {
|
||||
_tempThrottleTimer.cancel();
|
||||
}
|
||||
_tempThrottleTimer?.cancel();
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = true;
|
||||
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
||||
});
|
||||
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = false;
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "set_temperature",
|
||||
entityId: entity.entityId,
|
||||
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _setTargetTemp(ClimateEntity entity) {
|
||||
if (_targetTempThrottleTimer!=null) {
|
||||
_targetTempThrottleTimer.cancel();
|
||||
}
|
||||
_targetTempThrottleTimer?.cancel();
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = true;
|
||||
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
||||
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
||||
});
|
||||
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = false;
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "set_temperature",
|
||||
entityId: entity.entityId,
|
||||
data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -127,7 +125,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
entityId: entity.entityId,
|
||||
data: {"humidity": "$_tmpTargetHumidity"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -141,7 +138,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
entityId: entity.entityId,
|
||||
data: {"hvac_mode": "$_tmpHVACMode"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -155,7 +151,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
entityId: entity.entityId,
|
||||
data: {"swing_mode": "$_tmpSwingMode"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -164,7 +159,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_tmpFanMode = value;
|
||||
_changedHere = true;
|
||||
ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"});
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -173,7 +167,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_tmpPresetMode = value;
|
||||
_changedHere = true;
|
||||
ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"});
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -191,17 +184,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_tmpAuxHeat = value;
|
||||
_changedHere = true;
|
||||
ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"});
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _resetStateTimer(ClimateEntity entity) {
|
||||
if (_resetTimer!=null) {
|
||||
_resetTimer.cancel();
|
||||
}
|
||||
_resetTimer = Timer(Duration(seconds: 3), () {
|
||||
setState(() {});
|
||||
_resetVars(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -210,10 +192,9 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
||||
if (_changedHere) {
|
||||
_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
|
||||
//_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
|
||||
_changedHere = false;
|
||||
} else {
|
||||
_resetTimer?.cancel();
|
||||
_resetVars(entity);
|
||||
}
|
||||
return Padding(
|
||||
@ -222,20 +203,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
//_buildOnOffControl(entity),
|
||||
_buildTemperatureControls(entity),
|
||||
_buildTargetTemperatureControls(entity),
|
||||
_buildHumidityControls(entity),
|
||||
_buildOperationControl(entity),
|
||||
_buildFanControl(entity),
|
||||
_buildSwingControl(entity),
|
||||
_buildPresetModeControl(entity),
|
||||
_buildAuxHeatControl(entity)
|
||||
_buildTemperatureControls(entity, context),
|
||||
_buildTargetTemperatureControls(entity, context),
|
||||
_buildHumidityControls(entity, context),
|
||||
_buildOperationControl(entity, context),
|
||||
_buildFanControl(entity, context),
|
||||
_buildSwingControl(entity, context),
|
||||
_buildPresetModeControl(entity, context),
|
||||
_buildAuxHeatControl(entity, context)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPresetModeControl(ClimateEntity entity) {
|
||||
Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) {
|
||||
if (entity.supportPresetMode) {
|
||||
return ModeSelectorWidget(
|
||||
options: entity.presetModes,
|
||||
@ -260,7 +241,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
}*/
|
||||
|
||||
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
||||
Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) {
|
||||
if (entity.supportAuxHeat ) {
|
||||
return ModeSwitchWidget(
|
||||
caption: "Aux heat",
|
||||
@ -272,7 +253,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOperationControl(ClimateEntity entity) {
|
||||
Widget _buildOperationControl(ClimateEntity entity, BuildContext context) {
|
||||
if (entity.hvacModes != null) {
|
||||
return ModeSelectorWidget(
|
||||
onChange: (mode) => _setHVACMode(entity, mode),
|
||||
@ -285,7 +266,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFanControl(ClimateEntity entity) {
|
||||
Widget _buildFanControl(ClimateEntity entity, BuildContext context) {
|
||||
if (entity.supportFanMode) {
|
||||
return ModeSelectorWidget(
|
||||
options: entity.fanModes,
|
||||
@ -298,7 +279,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSwingControl(ClimateEntity entity) {
|
||||
Widget _buildSwingControl(ClimateEntity entity, BuildContext context) {
|
||||
if (entity.supportSwingMode) {
|
||||
return ModeSelectorWidget(
|
||||
onChange: (mode) => _setSwingMode(entity, mode),
|
||||
@ -311,17 +292,15 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTemperatureControls(ClimateEntity entity) {
|
||||
Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) {
|
||||
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("Target temperature", style: TextStyle(
|
||||
fontSize: Sizes.stateFontSize
|
||||
)),
|
||||
Text("Target temperature", style: Theme.of(context).textTheme.body1),
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTemperature,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
active: _temperaturePending,
|
||||
onDec: () => _temperatureDown(entity),
|
||||
onInc: () => _temperatureUp(entity),
|
||||
)
|
||||
@ -332,13 +311,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
||||
Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) {
|
||||
List<Widget> controls = [];
|
||||
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
||||
controls.addAll(<Widget>[
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetLow,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
active: _temperaturePending,
|
||||
onDec: () => _targetLowDown(entity),
|
||||
onInc: () => _targetLowUp(entity),
|
||||
),
|
||||
@ -351,7 +330,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
controls.add(
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetHigh,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
active: _temperaturePending,
|
||||
onDec: () => _targetHighDown(entity),
|
||||
onInc: () => _targetHighUp(entity),
|
||||
)
|
||||
@ -361,9 +340,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("Target temperature range", style: TextStyle(
|
||||
fontSize: Sizes.stateFontSize
|
||||
)),
|
||||
Text("Target temperature range", style: Theme.of(context).textTheme.body1),
|
||||
Row(
|
||||
children: controls,
|
||||
)
|
||||
@ -374,16 +351,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHumidityControls(ClimateEntity entity) {
|
||||
List<Widget> result = [];
|
||||
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) {
|
||||
if (entity.supportTargetHumidity) {
|
||||
result.addAll(<Widget>[
|
||||
Text(
|
||||
"$_tmpTargetHumidity%",
|
||||
style: TextStyle(fontSize: Sizes.largeFontSize),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
child: Text("Target humidity", style: Theme.of(context).textTheme.body1),
|
||||
),
|
||||
UniversalSlider(
|
||||
leading: Text(
|
||||
"$_tmpTargetHumidity%",
|
||||
style: Theme.of(context).textTheme.display1,
|
||||
),
|
||||
value: _tmpTargetHumidity,
|
||||
max: entity.maxHumidity,
|
||||
min: entity.minHumidity,
|
||||
@ -395,24 +376,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}),
|
||||
onChangeEnd: (double v) => _setTargetHumidity(entity, v),
|
||||
),
|
||||
)
|
||||
]);
|
||||
}
|
||||
if (result.isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||
child: Text("Target humidity", style: TextStyle(
|
||||
fontSize: Sizes.stateFontSize
|
||||
)),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: result,
|
||||
),
|
||||
Container(
|
||||
height: Sizes.rowPadding,
|
||||
)
|
||||
@ -429,7 +392,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_resetTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -33,23 +33,16 @@ class ClimateStateWidget extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text("$displayState",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: Sizes.stateFontSize,
|
||||
)),
|
||||
style: Theme.of(context).textTheme.body2),
|
||||
Text(" $targetTemp",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
))
|
||||
style: Theme.of(context).textTheme.body1)
|
||||
],
|
||||
),
|
||||
entity.currentTemperature != null ?
|
||||
Text("Currently: ${entity.currentTemperature}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
color: Colors.black45)
|
||||
style: Theme.of(context).textTheme.subtitle
|
||||
) :
|
||||
Container(height: 0.0,)
|
||||
],
|
||||
|
@ -3,21 +3,17 @@ part of '../../../main.dart';
|
||||
class ModeSelectorWidget extends StatelessWidget {
|
||||
|
||||
final String caption;
|
||||
final List<String> options;
|
||||
final List options;
|
||||
final String value;
|
||||
final double captionFontSize;
|
||||
final double valueFontSize;
|
||||
final onChange;
|
||||
final EdgeInsets padding;
|
||||
|
||||
ModeSelectorWidget({
|
||||
Key key,
|
||||
@required this.caption,
|
||||
@required this.options,
|
||||
this.options: const [],
|
||||
this.value,
|
||||
@required this.onChange,
|
||||
this.captionFontSize,
|
||||
this.valueFontSize,
|
||||
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||
}) : super(key: key);
|
||||
|
||||
@ -28,9 +24,7 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("$caption", style: TextStyle(
|
||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||
)),
|
||||
Text("$caption", style: Theme.of(context).textTheme.body1),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
@ -40,15 +34,12 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
value: value,
|
||||
iconSize: 30.0,
|
||||
isExpanded: true,
|
||||
style: TextStyle(
|
||||
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: Theme.of(context).textTheme.title,
|
||||
hint: Text("Select ${caption.toLowerCase()}"),
|
||||
items: options.map((String value) {
|
||||
items: options.map((value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
value: '$value',
|
||||
child: Text('$value'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => onChange(mode),
|
||||
|
@ -4,7 +4,6 @@ class ModeSwitchWidget extends StatelessWidget {
|
||||
|
||||
final String caption;
|
||||
final onChange;
|
||||
final double captionFontSize;
|
||||
final bool value;
|
||||
final bool expanded;
|
||||
final EdgeInsets padding;
|
||||
@ -13,7 +12,6 @@ class ModeSwitchWidget extends StatelessWidget {
|
||||
Key key,
|
||||
@required this.caption,
|
||||
@required this.onChange,
|
||||
this.captionFontSize,
|
||||
this.value,
|
||||
this.expanded: true,
|
||||
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
|
||||
@ -25,7 +23,7 @@ class ModeSwitchWidget extends StatelessWidget {
|
||||
padding: this.padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
_buildCaption(),
|
||||
_buildCaption(context),
|
||||
Switch(
|
||||
onChanged: (value) => onChange(value),
|
||||
value: value ?? false,
|
||||
@ -35,12 +33,10 @@ class ModeSwitchWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCaption() {
|
||||
Widget _buildCaption(BuildContext context) {
|
||||
Widget captionWidget = Text(
|
||||
"$caption",
|
||||
style: TextStyle(
|
||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||
),
|
||||
style: Theme.of(context).textTheme.body1,
|
||||
);
|
||||
if (expanded) {
|
||||
return Expanded(
|
||||
|
@ -2,8 +2,7 @@ part of '../../../main.dart';
|
||||
|
||||
class TemperatureControlWidget extends StatelessWidget {
|
||||
final double value;
|
||||
final double fontSize;
|
||||
final Color fontColor;
|
||||
final bool active;
|
||||
final onInc;
|
||||
final onDec;
|
||||
|
||||
@ -12,8 +11,9 @@ class TemperatureControlWidget extends StatelessWidget {
|
||||
@required this.value,
|
||||
@required this.onInc,
|
||||
@required this.onDec,
|
||||
this.fontSize,
|
||||
this.fontColor})
|
||||
//this.fontSize,
|
||||
this.active: false
|
||||
})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@ -23,10 +23,7 @@ class TemperatureControlWidget extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"$value",
|
||||
style: TextStyle(
|
||||
fontSize: fontSize ?? 24.0,
|
||||
color: fontColor ?? Colors.black
|
||||
),
|
||||
style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1,
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
|
@ -62,13 +62,10 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||
child: Text("Position", style: TextStyle(
|
||||
fontSize: Sizes.stateFontSize
|
||||
)),
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
child: Text("Position"),
|
||||
),
|
||||
Slider(
|
||||
UniversalSlider(
|
||||
value: _tmpPosition,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
@ -80,8 +77,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double value) => _setNewPosition(entity, value),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,)
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
@ -98,7 +94,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
||||
}
|
||||
if (entity.supportSetTiltPosition) {
|
||||
controls.addAll(<Widget>[
|
||||
Slider(
|
||||
UniversalSlider(
|
||||
value: _tmpTiltPosition,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
@ -117,10 +113,8 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
||||
if (controls.isNotEmpty) {
|
||||
controls.insert(0, Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||
child: Text("Tilt position", style: TextStyle(
|
||||
fontSize: Sizes.stateFontSize
|
||||
)),
|
||||
0.0, Sizes.rowPadding, 0.0, 0),
|
||||
child: Text("Tilt position"),
|
||||
));
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -9,10 +9,8 @@ class DateTimeStateWidget extends StatelessWidget {
|
||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
||||
child: GestureDetector(
|
||||
child: Text("${entity.formattedState}",
|
||||
textAlign: TextAlign.right,
|
||||
style: new TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
)),
|
||||
textAlign: TextAlign.right
|
||||
),
|
||||
onTap: () => _handleStateTap(context, entity),
|
||||
));
|
||||
}
|
||||
|
@ -11,25 +11,23 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
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.divider) {
|
||||
return Divider();
|
||||
}
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Divider(
|
||||
color: Colors.black45,
|
||||
),
|
||||
Divider(),
|
||||
Text(
|
||||
"${entityModel.entityWrapper.entity.displayName}",
|
||||
style: TextStyle(color: Colors.blue),
|
||||
style: HAClientTheme().getLinkTextStyle(context).copyWith(
|
||||
decoration: TextDecoration.none
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
@ -38,7 +36,6 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
EntityIcon(),
|
||||
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 3,
|
||||
@ -61,6 +58,11 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
entityModel.entityWrapper.handleTap();
|
||||
}
|
||||
},
|
||||
onDoubleTap: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleDoubleTap();
|
||||
}
|
||||
},
|
||||
child: result,
|
||||
);
|
||||
} else {
|
||||
|
@ -1,13 +1,6 @@
|
||||
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;
|
||||
}
|
||||
enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink}
|
||||
|
||||
class Entity {
|
||||
|
||||
@ -76,8 +69,8 @@ class Entity {
|
||||
String entityPicture;
|
||||
String state;
|
||||
String displayState;
|
||||
DateTime _lastUpdated;
|
||||
int statelessType = 0;
|
||||
DateTime lastUpdatedTimestamp;
|
||||
StatelessEntityType statelessType = StatelessEntityType.none;
|
||||
|
||||
List<Entity> childEntities = [];
|
||||
String deviceClass;
|
||||
@ -86,7 +79,8 @@ class Entity {
|
||||
);
|
||||
|
||||
String get displayName =>
|
||||
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
|
||||
attributes["friendly_name"] ??
|
||||
(attributes["name"] ?? (entityId != null && entityId.contains('.')) ? entityId.split(".")[1].replaceAll("_", " ") : "");
|
||||
|
||||
bool get isView =>
|
||||
(domain == "group") &&
|
||||
@ -120,42 +114,47 @@ class Entity {
|
||||
}
|
||||
|
||||
Entity.missed(String entityId) {
|
||||
statelessType = StatelessEntityType.MISSED;
|
||||
statelessType = StatelessEntityType.missed;
|
||||
attributes = {"hidden": false};
|
||||
this.entityId = entityId;
|
||||
}
|
||||
|
||||
Entity.divider() {
|
||||
statelessType = StatelessEntityType.DIVIDER;
|
||||
statelessType = StatelessEntityType.divider;
|
||||
attributes = {"hidden": false};
|
||||
}
|
||||
|
||||
Entity.section(String label) {
|
||||
statelessType = StatelessEntityType.SECTION;
|
||||
statelessType = StatelessEntityType.section;
|
||||
attributes = {"hidden": false, "friendly_name": "$label"};
|
||||
}
|
||||
|
||||
Entity.ghost(String name, String icon) {
|
||||
statelessType = StatelessEntityType.ghost;
|
||||
attributes = {"icon": icon, "hidden": false, "friendly_name": name};
|
||||
}
|
||||
|
||||
Entity.callService({String icon, String name, String service, String actionName}) {
|
||||
statelessType = StatelessEntityType.CALL_SERVICE;
|
||||
statelessType = StatelessEntityType.callService;
|
||||
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??
|
||||
statelessType = StatelessEntityType.webLink;
|
||||
entityId = "custom.custom";
|
||||
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];
|
||||
domain = rawData["entity_id"] != null ? rawData["entity_id"].split(".")[0] : null;
|
||||
entityId = rawData["entity_id"];
|
||||
deviceClass = attributes["device_class"];
|
||||
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
|
||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||
lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]);
|
||||
entityPicture = _getEntityPictureUrl(webHost);
|
||||
}
|
||||
|
||||
@ -211,27 +210,19 @@ class Entity {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBadgeWidget(BuildContext context) {
|
||||
return EntityModel(
|
||||
entityWrapper: EntityWrapper(entity: this),
|
||||
child: BadgeWidget(),
|
||||
handleTap: true,
|
||||
);
|
||||
}
|
||||
|
||||
String getAttribute(String attributeName) {
|
||||
if (attributes != null) {
|
||||
return attributes["$attributeName"];
|
||||
return attributes["$attributeName"].toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _getLastUpdatedFormatted() {
|
||||
if (_lastUpdated == null) {
|
||||
if (lastUpdatedTimestamp == null) {
|
||||
return "-";
|
||||
} else {
|
||||
DateTime now = DateTime.now();
|
||||
Duration d = now.difference(_lastUpdated);
|
||||
Duration d = now.difference(lastUpdatedTimestamp);
|
||||
String text;
|
||||
int v;
|
||||
if (d.inDays == 0) {
|
||||
|
@ -1,77 +0,0 @@
|
||||
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)
|
||||
};
|
||||
|
||||
static const _stateColors = {
|
||||
EntityState.on: Colors.amber,
|
||||
"auto": Colors.amber,
|
||||
EntityState.active: Colors.amber,
|
||||
EntityState.playing: Colors.amber,
|
||||
EntityState.paused: Colors.amber,
|
||||
"above_horizon": Colors.amber,
|
||||
EntityState.home: Colors.amber,
|
||||
EntityState.open: Colors.amber,
|
||||
EntityState.cleaning: Colors.amber,
|
||||
EntityState.returning: Colors.amber,
|
||||
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) {
|
||||
return _stateColors[state] ?? _stateColors["default"];
|
||||
}
|
||||
|
||||
static charts.Color chartHistoryStateColor(String state, int id) {
|
||||
Color c = _stateColors[state];
|
||||
if (c != null) {
|
||||
return charts.Color(
|
||||
r: c.red,
|
||||
g: c.green,
|
||||
b: c.blue,
|
||||
a: c.alpha
|
||||
);
|
||||
} else {
|
||||
double r = id.toDouble() % 10;
|
||||
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||
}
|
||||
}
|
||||
|
||||
static Color historyStateColor(String state, int id) {
|
||||
Color c = _stateColors[state];
|
||||
if (c != null) {
|
||||
return c;
|
||||
} else {
|
||||
if (id > -1) {
|
||||
double r = id.toDouble() % 10;
|
||||
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
|
||||
} else {
|
||||
return _stateColors[EntityState.on];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -3,12 +3,17 @@ part of '../main.dart';
|
||||
class EntityIcon extends StatelessWidget {
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
final EdgeInsetsGeometry iconPadding;
|
||||
final EdgeInsetsGeometry imagePadding;
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
|
||||
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key);
|
||||
|
||||
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||
if (entityId == null) {
|
||||
return 0;
|
||||
}
|
||||
String domain = entityId.split(".")[0];
|
||||
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
|
||||
String iconNameByDeviceClass;
|
||||
@ -23,52 +28,103 @@ class EntityIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
Color iconColor;
|
||||
if (color != null) {
|
||||
iconColor = color;
|
||||
} else if (entityWrapper.stateColor) {
|
||||
iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context);
|
||||
} else {
|
||||
iconColor = HAClientTheme().getOffStateColor(context);
|
||||
}
|
||||
Widget iconWidget;
|
||||
bool isPicture = false;
|
||||
if (entityWrapper == null) {
|
||||
iconWidget = Container(
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
} else {
|
||||
if (entityWrapper.entityPicture != null) {
|
||||
iconWidget = Container(
|
||||
height: size+12,
|
||||
width: size+12,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit:BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
"${entityWrapper.entityPicture}"
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
isPicture = true;
|
||||
} else {
|
||||
String iconName = entityWrapper.icon;
|
||||
int iconCode = 0;
|
||||
if (iconName.length > 0) {
|
||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||
} else {
|
||||
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
|
||||
entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
|
||||
}
|
||||
if (entityWrapper.entity is LightEntity &&
|
||||
(entityWrapper.entity as LightEntity).supportColor &&
|
||||
(entityWrapper.entity as LightEntity).color != null &&
|
||||
(entityWrapper.entity as LightEntity).color.toColor() != Colors.white
|
||||
) {
|
||||
Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();
|
||||
iconWidget = Stack(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: iconColor,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: size / 3,
|
||||
height: size / 3,
|
||||
decoration: BoxDecoration(
|
||||
color: lightColor,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
spreadRadius: 0,
|
||||
blurRadius: 0,
|
||||
offset: Offset(0.3, 0.3)
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
iconWidget = Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: iconColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
EdgeInsetsGeometry computedPadding;
|
||||
if (isPicture && imagePadding != null) {
|
||||
computedPadding = imagePadding;
|
||||
} else if (!isPicture && iconPadding != null) {
|
||||
computedPadding = iconPadding;
|
||||
} else {
|
||||
computedPadding = padding;
|
||||
}
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: buildIcon(
|
||||
entityWrapper,
|
||||
color ?? EntityColor.stateColor(entityWrapper.entity.state)
|
||||
),
|
||||
padding: computedPadding,
|
||||
child: iconWidget,
|
||||
);
|
||||
}
|
||||
}
|
@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget {
|
||||
final bool handleTap;
|
||||
|
||||
static EntityModel of(BuildContext context) {
|
||||
return context.inheritFromWidgetOfExactType(EntityModel);
|
||||
return context.dependOnInheritedWidgetOfExactType<EntityModel>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||
return true;
|
||||
bool updateShouldNotify(EntityModel oldWidget) {
|
||||
return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp;
|
||||
}
|
||||
}
|
@ -5,18 +5,24 @@ class EntityName extends StatelessWidget {
|
||||
final EdgeInsetsGeometry padding;
|
||||
final TextOverflow textOverflow;
|
||||
final bool wordsWrap;
|
||||
final double fontSize;
|
||||
final TextAlign textAlign;
|
||||
final int maxLines;
|
||||
final TextStyle textStyle;
|
||||
|
||||
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
|
||||
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.textStyle, this.wordsWrap: true, this.textAlign: TextAlign.left}) : super(key: key);
|
||||
|
||||
@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);
|
||||
TextStyle tStyle;
|
||||
if (textStyle == null) {
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) {
|
||||
tStyle = HAClientTheme().getLinkTextStyle(context);
|
||||
} else {
|
||||
tStyle = Theme.of(context).textTheme.body1;
|
||||
}
|
||||
} else {
|
||||
tStyle = textStyle;
|
||||
}
|
||||
return Padding(
|
||||
padding: padding,
|
||||
@ -25,7 +31,7 @@ class EntityName extends StatelessWidget {
|
||||
overflow: textOverflow,
|
||||
softWrap: wordsWrap,
|
||||
maxLines: maxLines,
|
||||
style: textStyle,
|
||||
style: tStyle,
|
||||
textAlign: textAlign,
|
||||
),
|
||||
);
|
||||
|
@ -2,10 +2,9 @@ part of '../main.dart';
|
||||
|
||||
class EntityPageLayout extends StatelessWidget {
|
||||
|
||||
final bool showClose;
|
||||
final Entity entity;
|
||||
|
||||
EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key);
|
||||
EntityPageLayout({Key key, this.entity}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -14,38 +13,6 @@ class EntityPageLayout extends StatelessWidget {
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(0),
|
||||
children: <Widget>[
|
||||
showClose ?
|
||||
Container(
|
||||
color: Colors.blue[300],
|
||||
height: 40,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
entity.displayName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontSize: 22
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.all(0),
|
||||
icon: Icon(Icons.close),
|
||||
color: Colors.white,
|
||||
iconSize: 36.0,
|
||||
onPressed: () {
|
||||
eventBus.fire(ShowEntityPageEvent());
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
) :
|
||||
Container(height: 0, width: 0,),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
||||
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),
|
||||
|
71
lib/entities/entity_picture.widget.dart
Normal file
@ -0,0 +1,71 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class EntityPicture extends StatelessWidget {
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
final BoxFit fit;
|
||||
|
||||
const EntityPicture({Key key, this.padding: const EdgeInsets.all(0.0), this.fit: BoxFit.cover}) : 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, BuildContext context) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
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); //
|
||||
}
|
||||
Widget iconPicture = Container(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: Sizes.largeIconSize,
|
||||
color: HAClientTheme().getOffStateColor(context),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
if (data.entityPicture != null) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: data.entityPicture,
|
||||
fit: this.fit,
|
||||
errorWidget: (context, _, __) => iconPicture,
|
||||
placeholder: (context, _) => iconPicture,
|
||||
);
|
||||
}
|
||||
|
||||
return iconPicture;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: buildIcon(
|
||||
entityWrapper,
|
||||
context
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,30 +2,31 @@ part of '../main.dart';
|
||||
|
||||
class EntityWrapper {
|
||||
|
||||
String displayName;
|
||||
String icon;
|
||||
String unitOfMeasurement;
|
||||
String entityPicture;
|
||||
String overrideName;
|
||||
final String overrideIcon;
|
||||
final bool stateColor;
|
||||
EntityUIAction uiAction;
|
||||
Entity entity;
|
||||
String unitOfMeasurementOverride;
|
||||
final List stateFilter;
|
||||
|
||||
String get icon => overrideIcon ?? entity.icon;
|
||||
String get entityPicture => entity.entityPicture;
|
||||
String get displayName => overrideName ?? entity.displayName;
|
||||
String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement;
|
||||
|
||||
EntityWrapper({
|
||||
this.entity,
|
||||
String icon,
|
||||
String displayName,
|
||||
this.uiAction
|
||||
this.overrideIcon,
|
||||
this.overrideName,
|
||||
this.stateColor: true,
|
||||
this.uiAction,
|
||||
this.stateFilter
|
||||
}) {
|
||||
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 (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || entity.statelessType == StatelessEntityType.webLink) {
|
||||
if (uiAction == null) {
|
||||
uiAction = EntityUIAction();
|
||||
}
|
||||
unitOfMeasurement = entity.unitOfMeasurement;
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,16 +54,16 @@ class EntityWrapper {
|
||||
|
||||
case EntityUIAction.moreInfo: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entity: entity));
|
||||
new ShowEntityPageEvent(entityId: entity.entityId));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.tapService}');
|
||||
} else {
|
||||
Launcher.launchURL(uiAction.tapService);
|
||||
Launcher.launchURLInBrowser(uiAction.tapService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -93,16 +94,16 @@ class EntityWrapper {
|
||||
|
||||
case EntityUIAction.moreInfo: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entity: entity));
|
||||
new ShowEntityPageEvent(entityId: entity.entityId));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.holdService}');
|
||||
} else {
|
||||
Launcher.launchURL(uiAction.holdService);
|
||||
Launcher.launchURLInBrowser(uiAction.holdService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -113,4 +114,104 @@ class EntityWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
void handleDoubleTap() {
|
||||
switch (uiAction.doubleTapAction) {
|
||||
case EntityUIAction.toggle: {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.callService: {
|
||||
if (uiAction.doubleTapService != null) {
|
||||
ConnectionManager().callService(
|
||||
domain: uiAction.doubleTapService.split(".")[0],
|
||||
service: uiAction.doubleTapService.split(".")[1],
|
||||
data: uiAction.doubleTapServiceData
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.moreInfo: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entityId: entity.entityId));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.doubleTapService}');
|
||||
} else {
|
||||
Launcher.launchURLInBrowser(uiAction.doubleTapService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntityUIAction {
|
||||
static const moreInfo = 'more-info';
|
||||
static const toggle = 'toggle';
|
||||
static const callService = 'call-service';
|
||||
static const navigate = 'navigate';
|
||||
static const none = 'none';
|
||||
|
||||
String tapAction = EntityUIAction.moreInfo;
|
||||
String tapNavigationPath;
|
||||
String tapService;
|
||||
Map<String, dynamic> tapServiceData;
|
||||
String holdAction = EntityUIAction.moreInfo;
|
||||
String holdNavigationPath;
|
||||
String holdService;
|
||||
Map<String, dynamic> holdServiceData;
|
||||
String doubleTapAction = EntityUIAction.none;
|
||||
String doubleTapNavigationPath;
|
||||
String doubleTapService;
|
||||
Map<String, dynamic> doubleTapServiceData;
|
||||
|
||||
EntityUIAction({rawEntityData}) {
|
||||
if (rawEntityData != null) {
|
||||
if (rawEntityData["tap_action"] != null) {
|
||||
if (rawEntityData["tap_action"] is String) {
|
||||
tapAction = rawEntityData["tap_action"];
|
||||
} else {
|
||||
tapAction =
|
||||
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
|
||||
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
|
||||
tapService = rawEntityData["tap_action"]["service"];
|
||||
tapServiceData = rawEntityData["tap_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
if (rawEntityData["hold_action"] != null) {
|
||||
if (rawEntityData["hold_action"] is String) {
|
||||
holdAction = rawEntityData["hold_action"];
|
||||
} else {
|
||||
holdAction =
|
||||
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
|
||||
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
|
||||
holdService = rawEntityData["hold_action"]["service"];
|
||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
if (rawEntityData["double_tap_action"] != null) {
|
||||
if (rawEntityData["double_tap_action"] is String) {
|
||||
doubleTapAction = rawEntityData["double_tap_action"];
|
||||
} else {
|
||||
doubleTapAction =
|
||||
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
|
||||
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
|
||||
doubleTapService = rawEntityData["double_tap_action"]["service"];
|
||||
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
23
lib/entities/error_entity_widget.dart
Normal file
@ -0,0 +1,23 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ErrorEntityWidget extends StatelessWidget {
|
||||
|
||||
final String text;
|
||||
|
||||
ErrorEntityWidget({
|
||||
Key key, this.text
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
String errorText = text ?? "Entity error: ${entityModel.entityWrapper.entity?.entityId}";
|
||||
return Container(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5.0),
|
||||
child: Text(errorText),
|
||||
),
|
||||
color: Theme.of(context).errorColor,
|
||||
);
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ class FlatServiceButton extends StatelessWidget {
|
||||
final String serviceName;
|
||||
final String entityId;
|
||||
final String text;
|
||||
final double fontSize;
|
||||
|
||||
FlatServiceButton({
|
||||
Key key,
|
||||
@ -14,7 +13,6 @@ class FlatServiceButton extends StatelessWidget {
|
||||
@required this.serviceName,
|
||||
@required this.entityId,
|
||||
@required this.text,
|
||||
this.fontSize: Sizes.stateFontSize
|
||||
}) : super(key: key);
|
||||
|
||||
void _setNewState() {
|
||||
@ -24,7 +22,7 @@ class FlatServiceButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: fontSize*2.5,
|
||||
height: Theme.of(context).textTheme.subhead.fontSize*2.5,
|
||||
child: FlatButton(
|
||||
onPressed: (() {
|
||||
_setNewState();
|
||||
@ -32,8 +30,7 @@ class FlatServiceButton extends StatelessWidget {
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: fontSize, color: Colors.blue),
|
||||
style: HAClientTheme().getActionTextStyle(context),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
@ -183,7 +183,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
}
|
||||
return UniversalSlider(
|
||||
title: "Color temperature",
|
||||
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
|
||||
leading: Text("Cold", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.lightBlue)),
|
||||
value: val,
|
||||
onChangeEnd: (value) => _setColorTemp(entity, value),
|
||||
max: entity.maxMireds,
|
||||
@ -194,7 +194,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
_tmpColorTemp = value.round();
|
||||
});
|
||||
},
|
||||
closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
|
||||
closing: Text("Warm", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.amberAccent),),
|
||||
);
|
||||
} else {
|
||||
return Container(width: 0.0, height: 0.0);
|
||||
@ -224,7 +224,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
color: savedColor?.toColor() ?? Colors.transparent,
|
||||
color: savedColor?.toColor() ?? Theme.of(context).backgroundColor,
|
||||
child: Text('Paste color'),
|
||||
onPressed: savedColor == null ? null : () {
|
||||
_setColor(entity, savedColor);
|
||||
|
@ -28,8 +28,7 @@ class LockStateWidget extends StatelessWidget {
|
||||
onPressed: () => _unlock(entity),
|
||||
child: Text("UNLOCK",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
style: HAClientTheme().getActionTextStyle(context)
|
||||
),
|
||||
)
|
||||
),
|
||||
@ -39,8 +38,7 @@ class LockStateWidget extends StatelessWidget {
|
||||
onPressed: () => _lock(entity),
|
||||
child: Text("LOCK",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
style: HAClientTheme().getActionTextStyle(context),
|
||||
),
|
||||
)
|
||||
)
|
||||
@ -56,8 +54,7 @@ class LockStateWidget extends StatelessWidget {
|
||||
child: Text(
|
||||
entity.isLocked ? "UNLOCK" : "LOCK",
|
||||
textAlign: TextAlign.right,
|
||||
style:
|
||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||
style: HAClientTheme().getActionTextStyle(context),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
@ -33,7 +33,7 @@ class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
|
||||
return LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: Colors.black45,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(HAClientTheme().getOnStateColor(context)),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -13,12 +13,6 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
||||
double _currentPosition = 0;
|
||||
int _savedPosition = 0;
|
||||
|
||||
final TextStyle _seekTextStyle = TextStyle(
|
||||
fontSize: 20,
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.bold
|
||||
);
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
@ -53,8 +47,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
||||
buttons.add(
|
||||
RaisedButton(
|
||||
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
|
||||
color: Colors.orange,
|
||||
focusColor: Colors.white,
|
||||
color: Theme.of(context).accentColor,
|
||||
onPressed: () {
|
||||
ConnectionManager().callService(
|
||||
domain: "media_player",
|
||||
@ -79,16 +72,20 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
||||
children: <Widget>[
|
||||
Text("00:00"),
|
||||
Expanded(
|
||||
child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle),
|
||||
child: Text(
|
||||
"${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.title.copyWith(
|
||||
color: Colors.blue
|
||||
)
|
||||
),
|
||||
),
|
||||
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
|
||||
],
|
||||
),
|
||||
Container(height: 10,),
|
||||
Slider(
|
||||
UniversalSlider(
|
||||
min: 0,
|
||||
activeColor: Colors.amber,
|
||||
inactiveColor: Colors.black26,
|
||||
activeColor: Theme.of(context).accentColor,
|
||||
max: entity.durationSeconds.toDouble(),
|
||||
value: _currentPosition,
|
||||
onChangeStart: (val) {
|
||||
|
@ -12,14 +12,14 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
Stack(
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
children: <Widget>[
|
||||
_buildImage(entity),
|
||||
_buildImage(entity, context),
|
||||
Positioned(
|
||||
bottom: 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
child: Container(
|
||||
color: Colors.black45,
|
||||
child: _buildState(entity),
|
||||
child: _buildState(entity, context),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@ -35,12 +35,9 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildState(MediaPlayerEntity entity) {
|
||||
TextStyle style = TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 1.2
|
||||
Widget _buildState(MediaPlayerEntity entity, BuildContext context) {
|
||||
TextStyle style = Theme.of(context).textTheme.body1.copyWith(
|
||||
color: Colors.white
|
||||
);
|
||||
List<Widget> states = [];
|
||||
states.add(Text("${entity.displayName}", style: style));
|
||||
@ -71,7 +68,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImage(MediaPlayerEntity entity) {
|
||||
Widget _buildImage(MediaPlayerEntity entity, BuildContext context) {
|
||||
String state = entity.state;
|
||||
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||
return Container(
|
||||
@ -97,7 +94,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
||||
size: 150.0,
|
||||
color: EntityColor.stateColor("$state"),
|
||||
color: HAClientTheme().getColorByEntityState("$state", context),
|
||||
)
|
||||
],
|
||||
);
|
||||
@ -231,7 +228,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical")),
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: entity))
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: entity.entityId))
|
||||
)
|
||||
);
|
||||
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
|
||||
@ -356,13 +353,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
volumeStepWidget = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||
onPressed: () => _setVolumeUp(entity.entityId)
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||
onPressed: () => _setVolumeDown(entity.entityId)
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||
onPressed: () => _setVolumeUp(entity.entityId)
|
||||
)
|
||||
],
|
||||
);
|
||||
|
@ -7,10 +7,10 @@ class SimpleEntityState extends StatelessWidget {
|
||||
final EdgeInsetsGeometry padding;
|
||||
final int maxLines;
|
||||
final String customValue;
|
||||
final double fontSize;
|
||||
final bool bold;
|
||||
final TextStyle textStyle;
|
||||
//final bool bold;
|
||||
|
||||
const SimpleEntityState({Key key,this.bold: false, this.maxLines: 10, this.fontSize: Sizes.stateFontSize, 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);
|
||||
const SimpleEntityState({Key key,/*this.bold: false,*/ this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.textStyle, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -22,16 +22,19 @@ class SimpleEntityState extends StatelessWidget {
|
||||
} else {
|
||||
state = customValue;
|
||||
}
|
||||
TextStyle textStyle = TextStyle(
|
||||
fontSize: this.fontSize,
|
||||
fontWeight: FontWeight.normal
|
||||
);
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||
textStyle = textStyle.apply(color: Colors.blue);
|
||||
TextStyle tStyle;
|
||||
if (textStyle != null) {
|
||||
tStyle = textStyle;
|
||||
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) {
|
||||
tStyle = Theme.of(context).textTheme.subhead.copyWith(
|
||||
color: HAClientTheme().getLinkTextStyle(context).color
|
||||
);
|
||||
} else {
|
||||
tStyle = Theme.of(context).textTheme.body1;
|
||||
}
|
||||
if (this.bold) {
|
||||
/*if (this.bold) {
|
||||
textStyle = textStyle.apply(fontWeightDelta: 100);
|
||||
}
|
||||
}*/
|
||||
while (state.contains(" ")){
|
||||
state = state.replaceAll(" ", " ");
|
||||
}
|
||||
@ -43,7 +46,7 @@ class SimpleEntityState extends StatelessWidget {
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: textStyle
|
||||
style: tStyle
|
||||
)
|
||||
);
|
||||
if (expanded) {
|
||||
|
@ -40,7 +40,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
||||
} else {
|
||||
_changedHere = false;
|
||||
}
|
||||
Widget slider = Slider(
|
||||
Widget slider = UniversalSlider(
|
||||
min: entity.minValue * _multiplier,
|
||||
max: entity.maxValue * _multiplier,
|
||||
value: (_newValue <= entity.maxValue) &&
|
||||
@ -62,8 +62,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"$_newValue",
|
||||
style: TextStyle(
|
||||
fontSize: Sizes.largeFontSize,
|
||||
style: Theme.of(context).textTheme.display1.copyWith(
|
||||
color: Colors.blue
|
||||
),
|
||||
),
|
||||
|
@ -26,11 +26,11 @@ class TimerEntity extends Entity {
|
||||
seconds: int.tryParse(durationList[2]) ?? 0
|
||||
);
|
||||
} else {
|
||||
Logger.e("Strange $entityId duration format: $durationSource");
|
||||
Logger.e("Strange timer duration format: $durationSource");
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("Error parsing duration for $entityId: ${e.toString()}");
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e("Error parsing timer duration \'$durationSource\': $e", stacktrace: stacktrace);
|
||||
duration = Duration(seconds: 0);
|
||||
}
|
||||
} else {
|
||||
|
@ -27,11 +27,11 @@ class _TimerStateState extends State<TimerState> {
|
||||
try {
|
||||
int passed = DateTime
|
||||
.now()
|
||||
.difference(entity._lastUpdated)
|
||||
.difference(entity.lastUpdatedTimestamp)
|
||||
.inSeconds;
|
||||
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
||||
} catch (e) {
|
||||
Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}");
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e("Error calculating ${entity.entityId} remaining time: $e", stacktrace: stacktrace);
|
||||
remaining = Duration(seconds: 0);
|
||||
}
|
||||
});
|
||||
|
@ -1,58 +1,102 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class UniversalSlider extends StatelessWidget {
|
||||
class UniversalSlider extends StatefulWidget {
|
||||
|
||||
final onChanged;
|
||||
final onChangeEnd;
|
||||
final Function onChanged;
|
||||
final Function onChangeEnd;
|
||||
final Function onChangeStart;
|
||||
final Widget leading;
|
||||
final Widget closing;
|
||||
final String title;
|
||||
final double min;
|
||||
final Color activeColor;
|
||||
final double max;
|
||||
final double value;
|
||||
final int divisions;
|
||||
final EdgeInsets padding;
|
||||
|
||||
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);
|
||||
const UniversalSlider({Key key, this.onChanged, this.onChangeStart, this.activeColor, this.divisions, 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
|
||||
State<StatefulWidget> createState() {
|
||||
return UniversalSliderState();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class UniversalSliderState extends State<UniversalSlider> {
|
||||
|
||||
double _value;
|
||||
bool _changeStarted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_value = widget.value;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List <Widget> row = [];
|
||||
if (leading != null) {
|
||||
row.add(leading);
|
||||
List <Widget> col = [];
|
||||
if (widget.leading != null) {
|
||||
row.add(widget.leading);
|
||||
}
|
||||
row.add(
|
||||
Flexible(
|
||||
child: Slider(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
onChanged: (value) => onChanged(value),
|
||||
onChangeEnd: (value) => onChangeEnd(value),
|
||||
),
|
||||
)
|
||||
Flexible(
|
||||
child: Slider(
|
||||
value: _value ?? math.max(widget.max ?? 100, _value ?? 0),
|
||||
min: widget.min ?? 0,
|
||||
max: widget.max ?? 100,
|
||||
activeColor: widget.activeColor,
|
||||
onChangeStart: (value) {
|
||||
_changeStarted = true;
|
||||
widget.onChangeStart?.call(value);
|
||||
},
|
||||
divisions: widget.divisions,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
widget.onChanged?.call(value);
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
_changeStarted = false;
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
Timer(Duration(milliseconds: 500), () {
|
||||
if (!_changeStarted) {
|
||||
widget.onChangeEnd?.call(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
),
|
||||
)
|
||||
);
|
||||
if (closing != null) {
|
||||
row.add(closing);
|
||||
if (widget.closing != null) {
|
||||
row.add(widget.closing);
|
||||
}
|
||||
if (widget.title != null) {
|
||||
col.addAll(<Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text('${widget.title}'),
|
||||
]);
|
||||
}
|
||||
col.addAll(<Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: row,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,)
|
||||
]);
|
||||
return Padding(
|
||||
padding: padding,
|
||||
padding: widget.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,)
|
||||
],
|
||||
children: col,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -10,7 +10,7 @@ class VacuumControls extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
_buildStatusAndBattery(entity),
|
||||
_buildStatusAndBattery(entity, context),
|
||||
_buildCommands(entity),
|
||||
_buildFanSpeed(entity),
|
||||
_buildAdditionalInfo(entity)
|
||||
@ -19,12 +19,12 @@ class VacuumControls extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusAndBattery(VacuumEntity entity) {
|
||||
Widget _buildStatusAndBattery(VacuumEntity entity, BuildContext context) {
|
||||
List<Widget> result = [];
|
||||
if (entity.supportStatus) {
|
||||
result.addAll(
|
||||
<Widget>[
|
||||
Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),),
|
||||
Text("Status:"),
|
||||
Container(width: 6,),
|
||||
Expanded(
|
||||
//flex: 1,
|
||||
@ -33,10 +33,7 @@ class VacuumControls extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
style: Theme.of(context).textTheme.body2,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -48,7 +45,7 @@ class VacuumControls extends StatelessWidget {
|
||||
result.addAll(<Widget>[
|
||||
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
|
||||
Container(width: 6,),
|
||||
Text("$batteryLevel %", style: TextStyle(fontSize: Sizes.stateFontSize))
|
||||
Text("$batteryLevel %")
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -172,7 +169,7 @@ class VacuumControls extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("Vacuum cleaner commands:", style: TextStyle(fontSize: Sizes.stateFontSize)),
|
||||
Text("Vacuum cleaner commands:"),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
@ -27,10 +27,7 @@ class VacuumStateButton extends StatelessWidget {
|
||||
text: "RETURN TO DOCK"
|
||||
);
|
||||
} else {
|
||||
result = Text(entity.state.toUpperCase(), style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey
|
||||
));
|
||||
result = Text(entity.state.toUpperCase(), style: Theme.of(context).textTheme.subhead);
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
|
@ -152,39 +152,12 @@ class EntityCollection {
|
||||
return _allEntities[entityId] != null;
|
||||
}
|
||||
|
||||
List<Entity> getByDomains({List<String> domains, List<String> stateFiler}) {
|
||||
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
|
||||
return _allEntities.values.where((entity) {
|
||||
return domains.contains(entity.domain) &&
|
||||
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
||||
return
|
||||
(excludeDomains.isEmpty || !excludeDomains.contains(entity.domain)) &&
|
||||
(includeDomains.isEmpty || includeDomains.contains(entity.domain)) &&
|
||||
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<Entity> filterEntitiesForDefaultView() {
|
||||
List<Entity> result = [];
|
||||
List<Entity> groups = [];
|
||||
List<Entity> nonGroupEntities = [];
|
||||
_allEntities.forEach((id, entity){
|
||||
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
|
||||
groups.add(entity);
|
||||
}
|
||||
if (!entity.isGroup) {
|
||||
nonGroupEntities.add(entity);
|
||||
}
|
||||
});
|
||||
|
||||
nonGroupEntities.forEach((entity) {
|
||||
bool foundInGroup = false;
|
||||
groups.forEach((groupEntity) {
|
||||
if (groupEntity.childEntityIds.contains(entity.entityId)) {
|
||||
foundInGroup = true;
|
||||
}
|
||||
});
|
||||
if (!foundInGroup) {
|
||||
result.add(entity);
|
||||
}
|
||||
});
|
||||
result.insertAll(0, groups);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ part of 'main.dart';
|
||||
|
||||
class HomeAssistant {
|
||||
|
||||
static const DEFAULT_DASHBOARD = 'lovelace';
|
||||
|
||||
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||
|
||||
factory HomeAssistant() {
|
||||
@ -11,69 +13,73 @@ class HomeAssistant {
|
||||
EntityCollection entities;
|
||||
HomeAssistantUI ui;
|
||||
Map _instanceConfig = {};
|
||||
Map services;
|
||||
String _userName;
|
||||
bool childMode;
|
||||
String _lovelaceDashbordUrl;
|
||||
HSVColor savedColor;
|
||||
int savedPlayerPosition;
|
||||
String sendToPlayerId;
|
||||
String sendFromPlayerId;
|
||||
Map services;
|
||||
bool autoUi = false;
|
||||
|
||||
String fcmToken;
|
||||
|
||||
Map _rawLovelaceData;
|
||||
var _rawStates;
|
||||
var _rawUserInfo;
|
||||
var _rawPanels;
|
||||
|
||||
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
|
||||
|
||||
List<Panel> panels = [];
|
||||
|
||||
Duration fetchTimeout = Duration(seconds: 30);
|
||||
|
||||
String get locationName {
|
||||
if (ConnectionManager().useLovelace) {
|
||||
return ui?.title ?? "";
|
||||
if (!autoUi) {
|
||||
return ui?.title ?? "Home";
|
||||
} else {
|
||||
return _instanceConfig["location_name"] ?? "";
|
||||
return _instanceConfig["location_name"] ?? "Home";
|
||||
}
|
||||
}
|
||||
String get userName => _userName ?? locationName;
|
||||
String get userName => _userName ?? '';
|
||||
String get userAvatarText => userName.length > 0 ? userName[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._internal() {
|
||||
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||
ConnectionManager().onLovelaceUpdatedCallback = _handleLovelaceUpdate;
|
||||
DeviceInfoManager().loadDeviceInfo();
|
||||
}
|
||||
|
||||
Completer _fetchCompleter;
|
||||
|
||||
Future fetchData() {
|
||||
Future fetchData(bool uiOnly) {
|
||||
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||
Logger.w("Previous data fetch is not completed yet");
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
_fetchCompleter = Completer();
|
||||
List<Future> futures = [];
|
||||
futures.add(_getStates());
|
||||
if (ConnectionManager().useLovelace) {
|
||||
futures.add(_getLovelace());
|
||||
if (!uiOnly) {
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
futures.add(_getStates(null));
|
||||
futures.add(_getConfig(null));
|
||||
futures.add(_getUserInfo(null));
|
||||
futures.add(_getPanels(null));
|
||||
futures.add(_getServices(null));
|
||||
}
|
||||
if (!autoUi) {
|
||||
futures.add(_getLovelace(null));
|
||||
}
|
||||
futures.add(_getConfig());
|
||||
futures.add(_getServices());
|
||||
futures.add(_getUserInfo());
|
||||
futures.add(_getPanels());
|
||||
futures.add(ConnectionManager().sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "state_changed"},
|
||||
));
|
||||
Future.wait(futures).then((_) {
|
||||
if (isMobileAppEnabled) {
|
||||
if (!childMode) _createUI();
|
||||
if (isComponentEnabled('mobile_app')) {
|
||||
_createUI();
|
||||
_fetchCompleter.complete();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
||||
} else {
|
||||
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app-integration")]));
|
||||
_fetchCompleter.completeError(HACException("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
|
||||
}
|
||||
}).catchError((e) {
|
||||
_fetchCompleter.completeError(e);
|
||||
@ -81,6 +87,48 @@ class HomeAssistant {
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
|
||||
Future<void> fetchDataFromCache() async {
|
||||
Logger.d('Loading cached data');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
bool cached = prefs.getBool('cached');
|
||||
if (cached != null && cached) {
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
try {
|
||||
_getStates(prefs);
|
||||
if (!autoUi) {
|
||||
_getLovelace(prefs);
|
||||
}
|
||||
_getConfig(prefs);
|
||||
_getUserInfo(prefs);
|
||||
_getPanels(prefs);
|
||||
_getServices(prefs);
|
||||
if (isComponentEnabled('mobile_app')) {
|
||||
_createUI();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.d('Didnt get cached data: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void saveCache() async {
|
||||
Logger.d('Saving data to cache...');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
try {
|
||||
await prefs.setString('cached_states', json.encode(_rawStates));
|
||||
await prefs.setString('cached_lovelace', json.encode(_rawLovelaceData));
|
||||
await prefs.setString('cached_user', json.encode(_rawUserInfo));
|
||||
await prefs.setString('cached_config', json.encode(_instanceConfig));
|
||||
await prefs.setString('cached_panels', json.encode(_rawPanels));
|
||||
await prefs.setString('cached_services', json.encode(services));
|
||||
await prefs.setBool('cached', true);
|
||||
} catch (e, stacktrace) {
|
||||
await prefs.setBool('cached', false);
|
||||
Logger.e('Error saving cache: $e', stacktrace: stacktrace);
|
||||
}
|
||||
Logger.d('Done saving cache');
|
||||
}
|
||||
|
||||
Future logout() async {
|
||||
Logger.d("Logging out...");
|
||||
await ConnectionManager().logout().then((_) {
|
||||
@ -90,80 +138,202 @@ class HomeAssistant {
|
||||
});
|
||||
}
|
||||
|
||||
Future _getConfig() async {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
}).catchError((e) {
|
||||
throw HAError("Error getting config: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getStates() async {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||
(data) => entities.parse(data)
|
||||
).catchError((e) {
|
||||
throw HAError("Error getting states: $e");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getLovelace() {
|
||||
Completer completer = Completer();
|
||||
ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) {
|
||||
_rawLovelaceData = data;
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
if ("$e" == "config_not_found") {
|
||||
ConnectionManager().useLovelace = false;
|
||||
completer.complete();
|
||||
} else {
|
||||
completer.completeError(HAError("Error getting lovelace config: $e"));
|
||||
Future _getConfig(SharedPreferences sharedPrefs) async {
|
||||
_instanceConfig?.clear();
|
||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_config')) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_config'));
|
||||
_parseConfig(data);
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e('Error gettong config from cache: $e', stacktrace: stacktrace);
|
||||
}
|
||||
} else {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
|
||||
Logger.e('get_config error: $e');
|
||||
throw HACException("Error getting config: $e");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parseConfig(data) {
|
||||
_instanceConfig = data;
|
||||
}
|
||||
|
||||
Future _getStates(SharedPreferences sharedPrefs) async {
|
||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_states'));
|
||||
_parseStates(data ?? []);
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e('Error getting cached states: $e', stacktrace: stacktrace);
|
||||
}
|
||||
} else {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||
(data) => _parseStates(data)
|
||||
).catchError((e) {
|
||||
Logger.e('get_states error: $e');
|
||||
throw HACException("Error getting states: $e");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parseStates(data) {
|
||||
_rawStates = data;
|
||||
entities.parse(data);
|
||||
}
|
||||
|
||||
Future _getLovelace(SharedPreferences sharedPrefs) {
|
||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_lovelace')) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
|
||||
_rawLovelaceData = data;
|
||||
} catch (e) {
|
||||
autoUi = true;
|
||||
}
|
||||
return Future.value();
|
||||
} else {
|
||||
Completer completer = Completer();
|
||||
var additionalData;
|
||||
if (ConnectionManager().haVersion >= 107 && _lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
|
||||
additionalData = {
|
||||
'url_path': _lovelaceDashbordUrl
|
||||
};
|
||||
}
|
||||
ConnectionManager().sendSocketMessage(
|
||||
type: 'lovelace/config',
|
||||
additionalData: additionalData
|
||||
).then((data) {
|
||||
_rawLovelaceData = data;
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
if ("$e".contains("config_not_found")) {
|
||||
autoUi = true;
|
||||
_rawLovelaceData = null;
|
||||
completer.complete();
|
||||
} else {
|
||||
Logger.e('lovelace/config error: $e');
|
||||
completer.completeError(HACException("Error getting lovelace config: $e"));
|
||||
}
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
Future _getServices(SharedPreferences prefs) async {
|
||||
services?.clear();
|
||||
if (prefs != null && prefs.containsKey('cached_services')) {
|
||||
try {
|
||||
var data = json.decode(prefs.getString('cached_services'));
|
||||
_parseServices(data ?? {});
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e(e, stacktrace: stacktrace);
|
||||
}
|
||||
}
|
||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
|
||||
Logger.e('get_services error: $e');
|
||||
});
|
||||
}
|
||||
|
||||
void _parseServices(data) {
|
||||
services = data;
|
||||
}
|
||||
|
||||
Future _getUserInfo(SharedPreferences sharedPrefs) async {
|
||||
_userName = null;
|
||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_user'));
|
||||
_parseUserInfo(data ?? {});
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e('Error getting cached user info: $e', stacktrace: stacktrace);
|
||||
}
|
||||
return Future.value();
|
||||
} else {
|
||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
|
||||
Logger.e('auth/current_user error: $e');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parseUserInfo(data) {
|
||||
_rawUserInfo = data;
|
||||
_userName = data["name"];
|
||||
}
|
||||
|
||||
Future _getPanels(SharedPreferences sharedPrefs) async {
|
||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
||||
_parsePanels(data ?? {});
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e(e, stacktrace: stacktrace);
|
||||
panels.clear();
|
||||
}
|
||||
} else {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e, stacktrace) {
|
||||
Logger.e('get_panels error: $e', stacktrace: stacktrace);
|
||||
panels.clear();
|
||||
throw HACException('Can\'t get panels: $e');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parsePanels(data) {
|
||||
panels.clear();
|
||||
_rawPanels = data;
|
||||
List<Panel> dashboards = [];
|
||||
data.forEach((k,v) {
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
if (v['component_name'] != null && v['component_name'] == 'lovelace') {
|
||||
dashboards.add(
|
||||
Panel(
|
||||
id: k,
|
||||
componentName: v['component_name'],
|
||||
title: title,
|
||||
urlPath: v['url_path'],
|
||||
config: v['config'],
|
||||
icon: (v['icon'] == null || v['icon'] == 'hass:view-dashboard') ? 'mdi:view-dashboard' : v['icon']
|
||||
)
|
||||
);
|
||||
} else {
|
||||
panels.add(
|
||||
Panel(
|
||||
id: k,
|
||||
componentName: v['component_name'],
|
||||
title: title,
|
||||
urlPath: v['url_path'],
|
||||
config: v['config'],
|
||||
icon: v['icon']
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
panels.insertAll(0, dashboards);
|
||||
}
|
||||
|
||||
Future getCameraStream(String entityId) {
|
||||
Completer completer = Completer();
|
||||
|
||||
ConnectionManager().sendSocketMessage(type: "camera/stream", additionalData: {"entity_id": entityId}).then((data) {
|
||||
completer.complete(data);
|
||||
}).catchError((e) {
|
||||
completer.completeError(e);
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future _getUserInfo() async {
|
||||
_userName = null;
|
||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) {
|
||||
_userName = data["name"];
|
||||
childMode = _userName.startsWith("[child]");
|
||||
}).catchError((e) {
|
||||
Logger.w("Can't get user info: $e");
|
||||
});
|
||||
bool isComponentEnabled(String name) {
|
||||
return _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("$name");
|
||||
}
|
||||
|
||||
Future _getServices() async {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
|
||||
Logger.d("Got ${data.length} services");
|
||||
services = data;
|
||||
}).catchError((e) {
|
||||
Logger.w("Can't get services: $e");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getPanels() async {
|
||||
panels.clear();
|
||||
await ConnectionManager().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 _handleLovelaceUpdate() {
|
||||
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||
eventBus.fire(new LovelaceChangedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
void _handleEntityStateChange(Map eventData) {
|
||||
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||
if (_fetchCompleter.isCompleted) {
|
||||
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||
Map data = Map.from(eventData);
|
||||
eventBus.fire(new StateChangedEvent(
|
||||
entityId: data["entity_id"],
|
||||
@ -172,251 +342,14 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
void _parseLovelace() {
|
||||
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
||||
ui.title = _rawLovelaceData["title"];
|
||||
int viewCounter = 0;
|
||||
Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
|
||||
_rawLovelaceData["views"].forEach((rawView){
|
||||
Logger.d("----view id: ${rawView['id']}");
|
||||
HAView view = HAView(
|
||||
count: viewCounter,
|
||||
id: "${rawView['id']}",
|
||||
name: rawView['title'],
|
||||
iconName: rawView['icon'],
|
||||
panel: rawView['panel'] ?? false,
|
||||
);
|
||||
|
||||
if (rawView['badges'] != null && rawView['badges'] is List) {
|
||||
rawView['badges'].forEach((entity) {
|
||||
if (entity is String) {
|
||||
if (entities.isExist(entity)) {
|
||||
Entity e = entities.get(entity);
|
||||
view.badges.add(e);
|
||||
}
|
||||
} else {
|
||||
String eId = '${entity['entity']}';
|
||||
if (entities.isExist(eId)) {
|
||||
Entity e = entities.get(eId);
|
||||
view.badges.add(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
|
||||
ui.views.add(
|
||||
view
|
||||
);
|
||||
viewCounter += 1;
|
||||
});
|
||||
}
|
||||
|
||||
List<HACard> _createLovelaceCards(List rawCards) {
|
||||
List<HACard> result = [];
|
||||
rawCards.forEach((rawCard){
|
||||
try {
|
||||
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
var rawCardInfo = rawCard["card"] ?? rawCard;
|
||||
HACard card = HACard(
|
||||
id: "card",
|
||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
|
||||
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
|
||||
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
|
||||
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],
|
||||
states: rawCardInfo['states'],
|
||||
conditions: rawCard['conditions'] ?? [],
|
||||
content: rawCardInfo['content'],
|
||||
min: rawCardInfo['min'] ?? 0,
|
||||
max: rawCardInfo['max'] ?? 100,
|
||||
unit: rawCardInfo['unit'],
|
||||
severity: rawCardInfo['severity']
|
||||
);
|
||||
if (rawCardInfo["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||
}
|
||||
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
|
||||
rawEntities?.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"],
|
||||
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"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
|
||||
if (rawSingleEntity != null) {
|
||||
var en = rawSingleEntity;
|
||||
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"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
result.add(card);
|
||||
} catch (e) {
|
||||
Logger.e("There was an error parsing card: ${e.toString()}");
|
||||
}
|
||||
});
|
||||
return result;
|
||||
bool isServiceExist(String service) {
|
||||
return services != null &&
|
||||
services.isNotEmpty &&
|
||||
services.containsKey(service);
|
||||
}
|
||||
|
||||
void _createUI() {
|
||||
ui = HomeAssistantUI();
|
||||
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
||||
Logger.d("Creating Lovelace UI");
|
||||
_parseLovelace();
|
||||
} else {
|
||||
Logger.d("Creating group-based UI");
|
||||
int viewCounter = 0;
|
||||
if (!entities.hasDefaultView) {
|
||||
HAView view = HAView(
|
||||
count: viewCounter,
|
||||
id: "group.default_view",
|
||||
name: "Home",
|
||||
childEntities: entities.filterEntitiesForDefaultView()
|
||||
);
|
||||
ui.views.add(
|
||||
view
|
||||
);
|
||||
viewCounter += 1;
|
||||
}
|
||||
entities.viewEntities.forEach((viewEntity) {
|
||||
HAView view = HAView(
|
||||
count: viewCounter,
|
||||
id: viewEntity.entityId,
|
||||
name: viewEntity.displayName,
|
||||
childEntities: viewEntity.childEntities
|
||||
);
|
||||
view.linkedEntity = viewEntity;
|
||||
ui.views.add(
|
||||
view
|
||||
);
|
||||
viewCounter += 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildViews(BuildContext context, TabController tabController) {
|
||||
return ui.build(context, tabController);
|
||||
Logger.d("Creating Lovelace UI");
|
||||
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class SendMessageQueue {
|
||||
int _messageTimeout;
|
||||
List<HAMessage> _queue = [];
|
||||
|
||||
SendMessageQueue(this._messageTimeout);
|
||||
|
||||
void add(String message) {
|
||||
_queue.add(HAMessage(_messageTimeout, message));
|
||||
}
|
||||
|
||||
List<String> getActualMessages() {
|
||||
_queue.removeWhere((item) => item.isExpired());
|
||||
List<String> result = [];
|
||||
_queue.forEach((haMessage){
|
||||
result.add(haMessage.message);
|
||||
});
|
||||
this.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_queue.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class HAMessage {
|
||||
DateTime _timeStamp;
|
||||
int _messageTimeout;
|
||||
String message;
|
||||
|
||||
HAMessage(this._messageTimeout, this.message) {
|
||||
_timeStamp = DateTime.now();
|
||||
}
|
||||
|
||||
bool isExpired() {
|
||||
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
||||
}
|
||||
}*/
|
||||
|
226
lib/main.dart
@ -1,6 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -13,7 +16,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
import 'package:progress_indicators/progress_indicators.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
@ -21,18 +23,22 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/circular_slider/single_circular_slider.dart';
|
||||
import 'package:share/receive_share_state.dart';
|
||||
import 'package:share/share.dart';
|
||||
import 'plugins/dynamic_multi_column_layout.dart';
|
||||
import 'plugins/spoiler_card.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:workmanager/workmanager.dart' as workManager;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
//import 'package:workmanager/workmanager.dart' as workManager;
|
||||
//import 'package:geolocator/geolocator.dart';
|
||||
import 'package:background_locator/background_locator.dart';
|
||||
import 'package:background_locator/location_dto.dart';
|
||||
import 'package:background_locator/location_settings.dart';
|
||||
import 'package:battery/battery.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:syncfusion_flutter_core/core.dart';
|
||||
import 'package:syncfusion_flutter_gauges/gauges.dart';
|
||||
|
||||
import 'utils/logger.dart';
|
||||
import '.secrets.dart';
|
||||
|
||||
part 'const.dart';
|
||||
part 'utils/launcher.dart';
|
||||
@ -57,12 +63,10 @@ part 'entities/fan/fan_entity.class.dart';
|
||||
part 'entities/automation/automation_entity.class.dart';
|
||||
part 'entities/camera/camera_entity.class.dart';
|
||||
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
|
||||
part 'entities/badge.widget.dart';
|
||||
part 'entities/entity_model.widget.dart';
|
||||
part 'entities/default_entity_container.widget.dart';
|
||||
part 'entities/missed_entity.widget.dart';
|
||||
part 'cards/widgets/glance_card_entity_container.dart';
|
||||
part 'cards/widgets/entity_button_card_body.widget.dart';
|
||||
part 'cards/entity_button_card.dart';
|
||||
part 'pages/widgets/entity_attributes_list.dart';
|
||||
part 'entities/entity_icon.widget.dart';
|
||||
part 'entities/entity_name.widget.dart';
|
||||
@ -73,7 +77,6 @@ part 'entities/universal_slider.widget.dart';
|
||||
part 'entities/flat_service_button.widget.dart';
|
||||
part 'entities/light/widgets/light_color_picker.dart';
|
||||
part 'entities/camera/widgets/camera_stream_view.dart';
|
||||
part 'entities/entity_colors.class.dart';
|
||||
part 'plugins/history_chart/entity_history.dart';
|
||||
part 'plugins/history_chart/simple_state_history_chart.dart';
|
||||
part 'plugins/history_chart/numeric_state_history_chart.dart';
|
||||
@ -85,6 +88,7 @@ part 'entities/slider/widgets/slider_controls.dart';
|
||||
part 'entities/text/widgets/text_input_state.dart';
|
||||
part 'entities/select/widgets/select_state.dart';
|
||||
part 'entities/simple_state.widget.dart';
|
||||
part 'entities/entity_picture.widget.dart';
|
||||
part 'entities/timer/widgets/timer_state.dart';
|
||||
part 'entities/climate/widgets/climate_state.widget.dart';
|
||||
part 'entities/cover/widgets/cover_state.dart';
|
||||
@ -100,16 +104,21 @@ part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.d
|
||||
part 'entities/vacuum/vacuum_entity.class.dart';
|
||||
part 'entities/vacuum/widgets/vacuum_controls.dart';
|
||||
part 'entities/vacuum/widgets/vacuum_state_button.dart';
|
||||
part 'pages/settings.page.dart';
|
||||
part 'entities/error_entity_widget.dart';
|
||||
part 'pages/settings/connection_settings.part.dart';
|
||||
part 'pages/purchase.page.dart';
|
||||
part 'pages/widgets/product_purchase.widget.dart';
|
||||
part 'pages/widgets/page_loading_indicator.dart';
|
||||
part 'pages/widgets/bottom_info_bar.dart';
|
||||
part 'pages/widgets/page_loading_error.dart';
|
||||
part 'pages/panel.page.dart';
|
||||
part 'pages/main.page.dart';
|
||||
part 'pages/integration_settings.page.dart';
|
||||
part 'pages/main/main.page.dart';
|
||||
part 'pages/settings/integration_settings.part.dart';
|
||||
part 'pages/settings/app_settings.page.dart';
|
||||
part 'pages/settings/lookandfeel_settings.part.dart';
|
||||
part 'pages/zha_page.dart';
|
||||
part 'pages/quick_start.page.dart';
|
||||
part 'home_assistant.class.dart';
|
||||
part 'pages/log.page.dart';
|
||||
part 'pages/entity.page.dart';
|
||||
part 'utils/mdi.class.dart';
|
||||
part 'entity_collection.class.dart';
|
||||
@ -119,92 +128,213 @@ part 'managers/mobile_app_integration_manager.class.dart';
|
||||
part 'managers/connection_manager.class.dart';
|
||||
part 'managers/device_info_manager.class.dart';
|
||||
part 'managers/startup_user_messages_manager.class.dart';
|
||||
part 'managers/theme_manager.dart';
|
||||
part 'ui.dart';
|
||||
part 'view.class.dart';
|
||||
part 'cards/card.class.dart';
|
||||
part 'panels/panel_class.dart';
|
||||
part 'viewWidget.widget.dart';
|
||||
part 'cards/card_widget.dart';
|
||||
part 'cards/widgets/card_header.widget.dart';
|
||||
part 'panels/config_panel_widget.dart';
|
||||
part 'panels/widgets/link_to_web_config.dart';
|
||||
part 'types/ha_error.dart';
|
||||
part 'types/event_bus_events.dart';
|
||||
part 'cards/widgets/gauge_card_body.dart';
|
||||
part 'cards/widgets/light_card_body.dart';
|
||||
part 'cards/gauge_card.dart';
|
||||
part 'cards/widgets/card_wrapper.widget.dart';
|
||||
part 'cards/entities_card.dart';
|
||||
part 'cards/alarm_panel_card.dart';
|
||||
part 'cards/horizontal_srack_card.dart';
|
||||
part 'cards/markdown_card.dart';
|
||||
part 'cards/media_control_card.dart';
|
||||
part 'cards/unsupported_card.dart';
|
||||
part 'cards/error_card.dart';
|
||||
part 'cards/vertical_stack_card.dart';
|
||||
part 'cards/glance_card.dart';
|
||||
part 'pages/play_media.page.dart';
|
||||
part 'entities/entity_page_layout.widget.dart';
|
||||
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
|
||||
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
|
||||
part 'pages/whats_new.page.dart';
|
||||
part 'pages/fullscreen.page.dart';
|
||||
part 'popups.dart';
|
||||
part 'cards/badges.dart';
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
final SentryClient _sentry = SentryClient(dsn: "https://03ef364745cc4c23a60ddbc874c69925@sentry.io/1836118");
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = "HA Client";
|
||||
const appVersionNumber = "0.7.6";
|
||||
const appVersionAdd = "";
|
||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
||||
const String appName = 'HA Client';
|
||||
const appVersionNumber = '1.0.1';
|
||||
final String appVersionAdd = secrets['version_type'] ?? '';
|
||||
final String appVersion = '$appVersionNumber${appVersionAdd.isNotEmpty ? '-' : ''}$appVersionAdd';
|
||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.0.1.md';
|
||||
|
||||
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||
// Print the exception to the console.
|
||||
if (Logger.isInDebugMode) {
|
||||
Logger.e('Caught error: $error');
|
||||
Logger.e('Caught error: $error', skipCrashlytics: true);
|
||||
Logger.p(stackTrace);
|
||||
return;
|
||||
} else {
|
||||
Logger.e('Caught error: $error. Reporting to Senrty.');
|
||||
// Send the Exception and Stacktrace to Sentry in Production mode.
|
||||
_sentry.captureException(
|
||||
exception: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
Crashlytics.instance.recordError(error, stackTrace);
|
||||
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Crashlytics.instance.enableInDevMode = false;
|
||||
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
|
||||
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
Logger.e(" Caut Flutter runtime error: ${details.exception}");
|
||||
Logger.e("Caut Flutter runtime error: ${details.exception}", skipCrashlytics: true);
|
||||
if (Logger.isInDebugMode) {
|
||||
FlutterError.dumpErrorToConsole(details);
|
||||
} else {
|
||||
// In production mode, report to the application zone to report to
|
||||
// Sentry.
|
||||
Zone.current.handleUncaughtError(details.exception, details.stack);
|
||||
}
|
||||
Crashlytics.instance.recordFlutterError(details);
|
||||
};
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
|
||||
|
||||
runZoned(() {
|
||||
runApp(new HAClientApp());
|
||||
runApp(new HAClientApp(
|
||||
theme: theme,
|
||||
));
|
||||
}, onError: (error, stack) {
|
||||
_reportError(error, stack);
|
||||
});
|
||||
}
|
||||
|
||||
class HAClientApp extends StatelessWidget {
|
||||
class HAClientApp extends StatefulWidget {
|
||||
|
||||
final AppTheme theme;
|
||||
|
||||
const HAClientApp({Key key, this.theme: AppTheme.defaultTheme}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HAClientAppState createState() => new _HAClientAppState();
|
||||
|
||||
}
|
||||
|
||||
class _HAClientAppState extends State<HAClientApp> {
|
||||
StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription;
|
||||
StreamSubscription _themeChangeSubscription;
|
||||
AppTheme _currentTheme = AppTheme.defaultTheme;
|
||||
|
||||
ReceivePort port = ReceivePort();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
InAppPurchaseConnection.enablePendingPurchases();
|
||||
final Stream purchaseUpdates =
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
_purchaseUpdateSubscription = purchaseUpdates.listen((purchases) {
|
||||
_handlePurchaseUpdates(purchases);
|
||||
});
|
||||
_currentTheme = widget.theme;
|
||||
_themeChangeSubscription = eventBus.on<ChangeThemeEvent>().listen((event){
|
||||
setState(() {
|
||||
_currentTheme = event.theme;
|
||||
});
|
||||
});
|
||||
/*
|
||||
workManager.Workmanager.initialize(
|
||||
updateDeviceLocationIsolate,
|
||||
isInDebugMode: false
|
||||
);
|
||||
*/
|
||||
super.initState();
|
||||
IsolateNameServer.registerPortWithName(port.sendPort, LocationManager.isolateName);
|
||||
port.listen((dynamic data) {
|
||||
// do something with data
|
||||
});
|
||||
initPlatformState();
|
||||
}
|
||||
|
||||
void _handlePurchaseUpdates(purchase) {
|
||||
if (purchase is List<PurchaseDetails>) {
|
||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
title: "Thanks a lot!",
|
||||
body: "Thank you for supporting HA Client development!",
|
||||
positiveText: "Ok"
|
||||
)
|
||||
));
|
||||
} else {
|
||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||
}
|
||||
} else {
|
||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initPlatformState() async {
|
||||
await BackgroundLocator.initialize();
|
||||
}
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new MaterialApp(
|
||||
title: appName,
|
||||
theme: new ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
theme: HAClientTheme().getThemeData(_currentTheme),
|
||||
darkTheme: HAClientTheme().darkTheme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
initialRoute: "/",
|
||||
routes: {
|
||||
"/": (context) => MainPage(title: 'HA Client'),
|
||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||
"/integration-settings": (context) => IntegrationSettingsPage(title: "Integration settings"),
|
||||
"/app-settings": (context) => AppSettingsPage(),
|
||||
"/connection-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.connectionSettings),
|
||||
"/integration-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.integrationSettings),
|
||||
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
||||
"/play-media": (context) => PlayMediaPage(
|
||||
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
|
||||
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
|
||||
),
|
||||
"/log-view": (context) => LogViewPage(title: "Log"),
|
||||
"/whats-new": (context) => WhatsNewPage()
|
||||
"/webview": (context) => standaloneWebview.WebviewScaffold(
|
||||
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop()
|
||||
),
|
||||
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
|
||||
),
|
||||
),
|
||||
"/whats-new": (context) => WhatsNewPage(),
|
||||
"/quick-start": (context) => QuickStartPage(),
|
||||
"/haclient_zha": (context) => ZhaPage(),
|
||||
"/auth": (context) => new standaloneWebview.WebviewScaffold(
|
||||
url: "${ConnectionManager().oauthUrl}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.help),
|
||||
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "https://ha-client.app/help/connection")
|
||||
),
|
||||
title: new Text("Login"),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text("Long-lived token", style: Theme.of(context).textTheme.button.copyWith(
|
||||
decoration: TextDecoration.underline
|
||||
)),
|
||||
onPressed: () {
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
goBackFirst: true,
|
||||
popup: TokenLoginPopup()
|
||||
));
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_purchaseUpdateSubscription.cancel();
|
||||
_themeChangeSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -9,46 +9,37 @@ class AuthManager {
|
||||
}
|
||||
|
||||
AuthManager._internal();
|
||||
StreamSubscription deepLinksSubscription;
|
||||
|
||||
Future start({String oauthUrl}) {
|
||||
Completer completer = Completer();
|
||||
deepLinksSubscription?.cancel();
|
||||
deepLinksSubscription = getUriLinksStream().listen((Uri uri) {
|
||||
Logger.d("[LINKED AUTH] We got something private");
|
||||
_getTempToken(oauthUrl, uri.queryParameters["code"])
|
||||
.then((tempToken) => completer.complete(tempToken))
|
||||
.catchError((_){
|
||||
completer.completeError(HAError("Auth error"));
|
||||
});
|
||||
}, onError: (err) {
|
||||
Logger.e("[LINKED AUTH] Error handling linked auth: $e");
|
||||
completer.completeError(HAError("Auth error"));
|
||||
});
|
||||
Logger.d("Launching OAuth");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future _getTempToken(String oauthUrl,String authCode) {
|
||||
Completer completer = Completer();
|
||||
ConnectionManager().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')}"
|
||||
final flutterWebviewPlugin = new standaloneWebview.FlutterWebviewPlugin();
|
||||
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
||||
if (url.startsWith("https://ha-client.app/service/auth_callback.html")) {
|
||||
Logger.d("url=$url");
|
||||
String authCode = url.split("=")[1];
|
||||
Logger.d("authCode=$authCode");
|
||||
Logger.d("We have auth code. Getting temporary access token...");
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/auth/token",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
includeAuthHeader: false,
|
||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('https://ha-client.app')}"
|
||||
).then((response) {
|
||||
Logger.d("Got temp token");
|
||||
String tempToken = json.decode(response)['access_token'];
|
||||
Logger.d("Closing webview...");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.complete(tempToken);
|
||||
}).catchError((e) {
|
||||
//flutterWebviewPlugin.close();
|
||||
Logger.e("Error getting temp token: ${e.toString()}");
|
||||
Logger.e("Error getting temp token: $e");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.completeError(HAError("Error getting temp token"));
|
||||
});
|
||||
completer.completeError(HACException("Error getting temp token"));
|
||||
}).whenComplete(() => flutterWebviewPlugin.close());
|
||||
}
|
||||
});
|
||||
Logger.d("Launching OAuth");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -19,8 +19,11 @@ class ConnectionManager {
|
||||
String _tempToken;
|
||||
String oauthUrl;
|
||||
String webhookId;
|
||||
bool useLovelace = true;
|
||||
double haVersion;
|
||||
bool scrollBadges;
|
||||
String mobileAppDeviceName;
|
||||
bool settingsLoaded = false;
|
||||
int appIntegrationVersion;
|
||||
bool get isAuthenticated => _token != null;
|
||||
StreamSubscription _socketSubscription;
|
||||
Duration connectTimeout = Duration(seconds: 15);
|
||||
@ -28,6 +31,7 @@ class ConnectionManager {
|
||||
bool isConnected = false;
|
||||
|
||||
var onStateChangeCallback;
|
||||
var onLovelaceUpdatedCallback;
|
||||
|
||||
IOWebSocketChannel _socket;
|
||||
|
||||
@ -38,41 +42,47 @@ class ConnectionManager {
|
||||
Completer completer = Completer();
|
||||
bool stopInit = false;
|
||||
if (loadSettings) {
|
||||
Logger.e("Loading settings...");
|
||||
Logger.d("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');
|
||||
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
|
||||
mobileAppDeviceName = prefs.getString('app-integration-device-name');
|
||||
scrollBadges = prefs.getBool('scroll-badges') ?? true;
|
||||
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) ||
|
||||
Logger.d('$_domain$_port');
|
||||
if (_domain == null && _port == null && webhookId == null && mobileAppDeviceName == null) {
|
||||
completer.completeError(HACNotSetUpException());
|
||||
stopInit = true;
|
||||
} else if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
completer.completeError(HAError.checkConnectionSettings());
|
||||
completer.completeError(HACException.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
} else {
|
||||
final storage = new FlutterSecureStorage();
|
||||
try {
|
||||
_token = await storage.read(key: "hacl_llt");
|
||||
Logger.e("Long-lived token read successful");
|
||||
Logger.d("Long-lived token read successful");
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'http://ha-client.homemade.systems')}&redirect_uri=${Uri
|
||||
'https://ha-client.app')}&redirect_uri=${Uri
|
||||
.encodeComponent(
|
||||
'haclient://auth')}";
|
||||
'https://ha-client.app/service/auth_callback.html')}";
|
||||
settingsLoaded = true;
|
||||
} catch (e) {
|
||||
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||
Logger.e("Cannt read secure storage. Need to relogin.");
|
||||
} catch (e, stacktrace) {
|
||||
completer.completeError(HACException("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||
Logger.e("Error reading secure storage: $e", stacktrace: stacktrace);
|
||||
stopInit = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
completer.completeError(HAError.checkConnectionSettings());
|
||||
completer.completeError(HACException.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
}
|
||||
}
|
||||
@ -105,13 +115,13 @@ class ConnectionManager {
|
||||
_disconnect().then((_) {
|
||||
if (e is TimeoutException) {
|
||||
if (connecting != null && !connecting.isCompleted) {
|
||||
connecting.completeError(HAError("Connection timeout"));
|
||||
connecting.completeError(HACException("Connection timeout"));
|
||||
}
|
||||
completer?.completeError(HAError("Connection timeout"));
|
||||
} else if (e is HAError) {
|
||||
completer?.completeError(HACException("Connection timeout"));
|
||||
} else if (e is HACException) {
|
||||
completer?.completeError(e);
|
||||
} else {
|
||||
completer?.completeError(HAError("${e.toString()}"));
|
||||
completer?.completeError(HACException("${e.toString()}"));
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -147,17 +157,32 @@ class ConnectionManager {
|
||||
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();
|
||||
String v = data["ha_version"];
|
||||
if (v != null && v.isNotEmpty) {
|
||||
haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
|
||||
}
|
||||
Logger.d("Home assistant version: $v ($haVersion)");
|
||||
Crashlytics.instance.setString('ha_version', v);
|
||||
Logger.d("[Connection] Subscribing to events");
|
||||
sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "lovelace_updated"},
|
||||
);
|
||||
sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "state_changed"},
|
||||
).whenComplete((){
|
||||
_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["auth"]?.completeError(HACException("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||
_messageResolver.remove("auth");
|
||||
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
if (!connecting.isCompleted) connecting.completeError(HACException("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
} else {
|
||||
_handleMessage(data);
|
||||
}
|
||||
@ -167,7 +192,7 @@ class ConnectionManager {
|
||||
onError: (e) => _handleSocketError(e, connecting)
|
||||
);
|
||||
} catch(exeption) {
|
||||
connecting.completeError(HAError("${exeption.toString()}"));
|
||||
connecting.completeError(HACException("${exeption.toString()}"));
|
||||
}
|
||||
});
|
||||
return connecting.future;
|
||||
@ -200,18 +225,19 @@ class ConnectionManager {
|
||||
//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"]["code"]}");
|
||||
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}", skipCrashlytics: true);
|
||||
_messageResolver["${data["id"]}"]?.completeError("${data["error"]["code"]}: ${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");
|
||||
if (data["event"] != null) {
|
||||
if (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"]["event_type"] == "lovelace_updated") {
|
||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: $data");
|
||||
onLovelaceUpdatedCallback();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.d("[Received unhandled] <== ${data.toString()}");
|
||||
@ -223,20 +249,20 @@ class ConnectionManager {
|
||||
_disconnect().then((_) {
|
||||
if (!connectionCompleter.isCompleted) {
|
||||
isConnected = false;
|
||||
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||
connectionCompleter.completeError(HACException("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||
}
|
||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||
eventBus.fire(ShowErrorEvent(HACException("Unable to connect to Home Assistant")));
|
||||
});
|
||||
}
|
||||
|
||||
void _handleSocketError(e, Completer connectionCompleter) {
|
||||
Logger.e("Socket stream Error: $e");
|
||||
Logger.e("Socket stream Error: $e", skipCrashlytics: true);
|
||||
_disconnect().then((_) {
|
||||
if (!connectionCompleter.isCompleted) {
|
||||
isConnected = false;
|
||||
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||
connectionCompleter.completeError(HACException("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||
}
|
||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||
eventBus.fire(ShowErrorEvent(HACException("Unable to connect to Home Assistant")));
|
||||
});
|
||||
}
|
||||
|
||||
@ -269,7 +295,7 @@ class ConnectionManager {
|
||||
});
|
||||
}).catchError((e) => completer.completeError(e));
|
||||
} else {
|
||||
completer.completeError(HAError("General login error"));
|
||||
completer.completeError(HACException("General login error"));
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
@ -304,7 +330,7 @@ class ConnectionManager {
|
||||
throw e;
|
||||
});
|
||||
}).catchError((e) {
|
||||
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
completer.completeError(HACException("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
@ -331,7 +357,7 @@ class ConnectionManager {
|
||||
_socket.sink.add(rawMessage);
|
||||
}).catchError((e) {
|
||||
if (!_completer.isCompleted) {
|
||||
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||
_completer.completeError(HACException("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -345,7 +371,7 @@ class ConnectionManager {
|
||||
_currentMessageId += 1;
|
||||
}
|
||||
|
||||
Future callService({@required String domain, @required String service, String entityId, Map data}) {
|
||||
Future callService({@required String domain, @required String service, entityId, Map data}) {
|
||||
eventBus.fire(NotifyServiceCallEvent(domain, service, entityId));
|
||||
Logger.d("Service call: $domain.$service, $entityId, $data");
|
||||
Completer completer = Completer();
|
||||
@ -360,12 +386,12 @@ class ConnectionManager {
|
||||
sendHTTPPost(
|
||||
endPoint: "/api/services/$domain/$service",
|
||||
data: json.encode(serviceData)
|
||||
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
|
||||
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HACException(e.toString())));
|
||||
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
|
||||
else
|
||||
sendHTTPPost(
|
||||
endPoint: "/api/services/$domain/$service"
|
||||
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
|
||||
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HACException(e.toString())));
|
||||
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
|
||||
return completer.future;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ class LocationManager {
|
||||
defaultUpdateIntervalMinutes);
|
||||
_isRunning = prefs.getBool("location-enabled") ?? false;
|
||||
if (_isRunning) {
|
||||
await _startLocationService();
|
||||
//await _startLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,20 @@ class LocationManager {
|
||||
}
|
||||
|
||||
_startLocationService() async {
|
||||
Logger.d('Starting location tracking');
|
||||
BackgroundLocator.registerLocationUpdate(
|
||||
locationCallback,
|
||||
//optional
|
||||
androidNotificationCallback: locationNotificationCallback,
|
||||
settings: LocationSettings(
|
||||
notificationTitle: "HA Client location tracking",
|
||||
notificationMsg: "HA Client is updating your device location",
|
||||
wakeLockTime: 20,
|
||||
autoStop: false,
|
||||
interval: 10
|
||||
),
|
||||
);
|
||||
/*
|
||||
String webhookId = ConnectionManager().webhookId;
|
||||
String httpWebHost = ConnectionManager().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
@ -95,105 +109,220 @@ class LocationManager {
|
||||
backoffPolicy: workManager.BackoffPolicy.linear,
|
||||
backoffPolicyDelay: interval,
|
||||
constraints: workManager.Constraints(
|
||||
networkType: workManager.NetworkType.connected
|
||||
)
|
||||
networkType: workManager.NetworkType.connected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
_stopLocationService() async {
|
||||
Logger.d('Stopping location tracking');
|
||||
IsolateNameServer.removePortNameMapping(isolateName);
|
||||
BackgroundLocator.unRegisterLocationUpdate();
|
||||
/*Logger.d("Canceling previous schedule if any...");
|
||||
await workManager.Workmanager.cancelAll();*/
|
||||
}
|
||||
|
||||
static const String isolateName = "HAClientLocatorIsolate";
|
||||
|
||||
static void locationCallback(LocationDto locationDto) async {
|
||||
print('[Background location] Got location: $locationDto');
|
||||
sendLocationData(locationDto);
|
||||
final SendPort send = IsolateNameServer.lookupPortByName(isolateName);
|
||||
send?.send(locationDto);
|
||||
}
|
||||
|
||||
static Future<void> sendLocationData(LocationDto location) async {
|
||||
print('[Background location] Loading settings...');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String domain = prefs.getString('hassio-domain');
|
||||
String port = prefs.getString('hassio-port');
|
||||
String webhookId = prefs.getString('app-webhook-id');
|
||||
String httpWebHost =
|
||||
"${prefs.getString('hassio-res-protocol')}://$domain:$port";
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map<String, String> headers = {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [],
|
||||
"gps_accuracy": 0,
|
||||
"battery": 100
|
||||
}
|
||||
};
|
||||
try {
|
||||
if (location.longitude != null && location.latitude != null) {
|
||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||
data["data"]["gps_accuracy"] = location.accuracy;
|
||||
print('[Background location] Sending...');
|
||||
try {
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: json.encode(data)
|
||||
);
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
print('[Background location] Success!');
|
||||
} else {
|
||||
print('[Background location] Error sending data: ${response.statusCode}');
|
||||
}
|
||||
} catch(e) {
|
||||
print('[Background location] Error sending data: $e');
|
||||
}
|
||||
} else {
|
||||
print('[Background location] Error. Location is null');
|
||||
}
|
||||
} catch (e) {
|
||||
print('[Background location] Error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_stopLocationService() async {
|
||||
Logger.d("Canceling previous schedule if any...");
|
||||
await workManager.Workmanager.cancelByTag(backgroundTaskTag);
|
||||
|
||||
static void locationNotificationCallback() {
|
||||
print('[Background location] User clicked on the notification');
|
||||
}
|
||||
|
||||
updateDeviceLocation() async {
|
||||
Logger.d("[Foreground location] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
String webhookId = ConnectionManager().webhookId;
|
||||
String httpWebHost = ConnectionManager().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Logger.d("[Foreground location] Getting battery level...");
|
||||
int batteryLevel = await battery.batteryLevel;
|
||||
Logger.d("[Foreground location] Getting device location...");
|
||||
Position position = await geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
locationPermissionLevel: GeolocationPermission.locationAlways
|
||||
);
|
||||
if (position != null) {
|
||||
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [position.latitude, position.longitude],
|
||||
"gps_accuracy": position.accuracy,
|
||||
"battery": batteryLevel ?? 100
|
||||
/*
|
||||
try {
|
||||
Logger.d("[Foreground location] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
String webhookId = ConnectionManager().webhookId;
|
||||
String httpWebHost = ConnectionManager().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Logger.d("[Foreground location] Getting battery level...");
|
||||
int batteryLevel = await battery.batteryLevel;
|
||||
Logger.d("[Foreground location] Getting device location...");
|
||||
Position position = await geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
locationPermissionLevel: GeolocationPermission.locationAlways
|
||||
);
|
||||
if (position != null) {
|
||||
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [position.latitude, position.longitude],
|
||||
"gps_accuracy": position.accuracy,
|
||||
"battery": batteryLevel ?? 100
|
||||
}
|
||||
};
|
||||
Logger.d("[Foreground location] Sending data home...");
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode(data)
|
||||
);
|
||||
if (response.statusCode >= 300) {
|
||||
Logger.e('Foreground location update error: ${response.body}');
|
||||
}
|
||||
};
|
||||
Logger.d("[Foreground location] Sending data home...");
|
||||
var response = await http.post(
|
||||
url,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode(data)
|
||||
);
|
||||
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
|
||||
} else {
|
||||
Logger.d("[Foreground location] No location. Aborting.");
|
||||
}
|
||||
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
|
||||
} else {
|
||||
Logger.d("[Foreground location] No location. Aborting.");
|
||||
}
|
||||
}
|
||||
} catch (e, stack) {
|
||||
Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
void updateDeviceLocationIsolate() {
|
||||
workManager.Workmanager.executeTask((backgroundTask, data) {
|
||||
print("[Background $backgroundTask] Started");
|
||||
final SentryClient sentryBackgroundClient = SentryClient(dsn: "https://5c868e5ef26947e2b61b189e391ec31b@sentry.io/1836366");
|
||||
workManager.Workmanager.executeTask((backgroundTask, data) async {
|
||||
//print("[Background $backgroundTask] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
int batteryLevel = 100;
|
||||
String webhookId = data["webhookId"];
|
||||
String httpWebHost = data["httpWebHost"];
|
||||
//String logData = '==> ${DateTime.now()} [Background $backgroundTask]:';
|
||||
//print("[Background $backgroundTask] Getting path for log file...");
|
||||
//final logFileDirectory = await getExternalStorageDirectory();
|
||||
//print("[Background $backgroundTask] Opening log file...");
|
||||
//File logFile = File('${logFileDirectory.path}/ha-client-background-log.txt');
|
||||
//print("[Background $backgroundTask] Log file path: ${logFile.path}");
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
print("[Background $backgroundTask] hour=$battery");
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map<String, String> headers = {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [],
|
||||
"gps_accuracy": 0,
|
||||
"battery": batteryLevel
|
||||
}
|
||||
};
|
||||
print("[Background $backgroundTask] Getting battery level...");
|
||||
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){
|
||||
print("[Background $backgroundTask] Getting device location...");
|
||||
geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways).then((location) {
|
||||
if (location != null) {
|
||||
print("[Background $backgroundTask] Got location: ${location.latitude} ${location.longitude}");
|
||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||
data["data"]["gps_accuracy"] = location.accuracy;
|
||||
print("[Background $backgroundTask] Sending data home.");
|
||||
http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: json.encode(data)
|
||||
);
|
||||
} else {
|
||||
throw "Can't get device location. Location is null";
|
||||
}
|
||||
}).catchError((e) {
|
||||
sentryBackgroundClient.captureException(
|
||||
exception: "${e.toString()}"
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map<String, String> headers = {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [],
|
||||
"gps_accuracy": 0,
|
||||
"battery": 100
|
||||
}
|
||||
};
|
||||
//print("[Background $backgroundTask] Getting battery level...");
|
||||
int batteryLevel;
|
||||
try {
|
||||
batteryLevel = await battery.batteryLevel;
|
||||
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
|
||||
} catch(e) {
|
||||
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
|
||||
batteryLevel = 0;
|
||||
//logData += 'Battery: error, $e';
|
||||
}
|
||||
if (batteryLevel != null) {
|
||||
data["data"]["battery"] = batteryLevel;
|
||||
//logData += 'Battery: success, $batteryLevel';
|
||||
}/* else {
|
||||
logData += 'Battery: error, level is null';
|
||||
}*/
|
||||
Position location;
|
||||
try {
|
||||
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
|
||||
if (location != null && location.latitude != null) {
|
||||
//logData += ' || Location: success, ${location.latitude} ${location.longitude} (${location.timestamp})';
|
||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||
data["data"]["gps_accuracy"] = location.accuracy;
|
||||
try {
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: json.encode(data)
|
||||
);
|
||||
print("[Background $backgroundTask] Error getting current location: ${e.toString()}");
|
||||
});
|
||||
});
|
||||
/*if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
logData += ' || Post: success, ${response.statusCode}';
|
||||
} else {
|
||||
logData += ' || Post: error, ${response.statusCode}';
|
||||
}*/
|
||||
} catch(e) {
|
||||
//logData += ' || Post: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += ' || Location: error, location is null';
|
||||
}*/
|
||||
} catch (e) {
|
||||
//print("[Background $backgroundTask] Location error: $e");
|
||||
//logData += ' || Location: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += 'Not configured';
|
||||
}*/
|
||||
//print("[Background $backgroundTask] Writing log data...");
|
||||
/*try {
|
||||
var fileMode;
|
||||
if (logFile.existsSync() && logFile.lengthSync() < 5000000) {
|
||||
fileMode = FileMode.append;
|
||||
} else {
|
||||
fileMode = FileMode.write;
|
||||
}
|
||||
await logFile.writeAsString('$logData\n', mode: fileMode);
|
||||
} catch (e) {
|
||||
print("[Background $backgroundTask] Error writing log: $e");
|
||||
}
|
||||
return Future.value(true);
|
||||
print("[Background $backgroundTask] Finished.");*/
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
@ -2,9 +2,11 @@ part of '../main.dart';
|
||||
|
||||
class MobileAppIntegrationManager {
|
||||
|
||||
static const INTEGRATION_VERSION = 3;
|
||||
|
||||
static final _appRegistrationData = {
|
||||
"app_version": "$appVersion",
|
||||
"device_name": "",
|
||||
"app_version": "$appVersion",
|
||||
"manufacturer": DeviceInfoManager().manufacturer,
|
||||
"model": DeviceInfoManager().model,
|
||||
"os_version": DeviceInfoManager().osVersion,
|
||||
@ -14,12 +16,19 @@ class MobileAppIntegrationManager {
|
||||
}
|
||||
};
|
||||
|
||||
static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
|
||||
static String getDefaultDeviceName() {
|
||||
if (HomeAssistant().userName.isEmpty) {
|
||||
return '${DeviceInfoManager().model}';
|
||||
}
|
||||
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
|
||||
}
|
||||
|
||||
static Future checkAppRegistration() {
|
||||
Completer completer = Completer();
|
||||
_appRegistrationData["device_name"] = "${HomeAssistant().userName}'s ${DeviceInfoManager().model}";
|
||||
_appRegistrationData["device_name"] = ConnectionManager().mobileAppDeviceName ?? getDefaultDeviceName();
|
||||
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
|
||||
if (ConnectionManager().webhookId == null || forceRegister) {
|
||||
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
||||
if (ConnectionManager().webhookId == null) {
|
||||
Logger.d("Mobile app was not registered yet. Registering...");
|
||||
var registrationData = Map.from(_appRegistrationData);
|
||||
registrationData.addAll({
|
||||
"app_id": "ha_client",
|
||||
@ -27,6 +36,11 @@ class MobileAppIntegrationManager {
|
||||
"os_name": DeviceInfoManager().osName,
|
||||
"supports_encryption": false,
|
||||
});
|
||||
if (ConnectionManager().haVersion >= 104) {
|
||||
registrationData.addAll({
|
||||
"device_id": "${DeviceInfoManager().unicDeviceId}"
|
||||
});
|
||||
}
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/api/mobile_app/registrations",
|
||||
includeAuthHeader: true,
|
||||
@ -34,24 +48,33 @@ class MobileAppIntegrationManager {
|
||||
).then((response) {
|
||||
Logger.d("Processing registration responce...");
|
||||
var responseObject = json.decode(response);
|
||||
ConnectionManager().webhookId = responseObject["webhook_id"];
|
||||
ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
||||
ConnectionManager().webhookId = responseObject["webhook_id"];
|
||||
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
|
||||
|
||||
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: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "restart");
|
||||
},
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
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: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "restart");
|
||||
},
|
||||
)
|
||||
));
|
||||
});
|
||||
}).catchError((e) {
|
||||
completer.complete();
|
||||
Logger.e("Error registering the app: ${e.toString()}");
|
||||
if (e is http.Response) {
|
||||
Logger.e("Error registering the app: ${e.statusCode}: ${e.body}");
|
||||
} else {
|
||||
Logger.e("Error registering the app: ${e?.toString()}");
|
||||
}
|
||||
_showError();
|
||||
});
|
||||
return completer.future;
|
||||
} else {
|
||||
@ -65,39 +88,34 @@ class MobileAppIntegrationManager {
|
||||
includeAuthHeader: false,
|
||||
data: json.encode(updateData)
|
||||
).then((response) {
|
||||
if (response == null || response.isEmpty) {
|
||||
Logger.d("No registration data in response. MobileApp integration was removed");
|
||||
var registrationData;
|
||||
try {
|
||||
registrationData = json.decode(response);
|
||||
} catch (e) {
|
||||
registrationData = null;
|
||||
}
|
||||
if (registrationData == null || registrationData.isEmpty) {
|
||||
Logger.w("No registration data in response. MobileApp integration was removed or broken");
|
||||
_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"
|
||||
));
|
||||
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
|
||||
Logger.d('App registration needs to be updated');
|
||||
_askToRemoveAndRegisterApp();
|
||||
} else {
|
||||
Logger.d('App registration works fine');
|
||||
}
|
||||
}
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
if (e is http.Response && e.statusCode == 410) {
|
||||
Logger.e("MobileApp integration was removed");
|
||||
Logger.w("MobileApp integration was removed");
|
||||
_askToRegisterApp();
|
||||
} else if (e is http.Response) {
|
||||
Logger.w("Error updating app registration: ${e.statusCode}: ${e.body}");
|
||||
_showError();
|
||||
} 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: () {
|
||||
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
onNegative: () {
|
||||
Launcher.launchURL("https://discord.gg/AUzEvwn");
|
||||
},
|
||||
));
|
||||
Logger.w("Error updating app registration: ${e?.toString()}");
|
||||
_showError();
|
||||
}
|
||||
completer.complete();
|
||||
});
|
||||
@ -105,19 +123,43 @@ class MobileAppIntegrationManager {
|
||||
}
|
||||
}
|
||||
|
||||
static void _showError() {
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
title: "App integration is not working properly",
|
||||
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue. You can try to remove Mobile App integration from Home Assistant and restart server to fix this issue.",
|
||||
positiveText: "Report to GitHub",
|
||||
negativeText: "Report to Discord",
|
||||
onPositive: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
onNegative: () {
|
||||
Launcher.launchURLInBrowser("https://discord.gg/u9vq7QE");
|
||||
},
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
static void _askToRemoveAndRegisterApp() {
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
title: "Mobile app integration needs to be updated",
|
||||
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
|
||||
positiveText: "Ok",
|
||||
negativeText: "Report an issue",
|
||||
onNegative: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
static 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");
|
||||
ConnectionManager().webhookId = null;
|
||||
checkAppRegistration();
|
||||
});
|
||||
},
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: RegisterAppPopup(
|
||||
title: "Mobile App integration is missing",
|
||||
body: "Looks like mobile app integration was removed from your Home Assistant or it needs to be updated.",
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -9,18 +9,18 @@ class StartupUserMessagesManager {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
StartupUserMessagesManager._internal() {}
|
||||
StartupUserMessagesManager._internal();
|
||||
|
||||
bool _supportAppDevelopmentMessageShown;
|
||||
bool _whatsNewMessageShown;
|
||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||
static final _whatsNewMessageKey = "user-message-shown-whats-new-706";
|
||||
static final _whatsNewMessageKey = "user-msg-whats-new-url";
|
||||
|
||||
void checkMessagesToShow() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.reload();
|
||||
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
|
||||
_whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false;
|
||||
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
|
||||
if (!_whatsNewMessageShown) {
|
||||
_showWhatsNewMessage();
|
||||
} else if (!_supportAppDevelopmentMessageShown) {
|
||||
@ -29,7 +29,8 @@ class StartupUserMessagesManager {
|
||||
}
|
||||
|
||||
void _showSupportAppDevelopmentMessage() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
title: "Hi!",
|
||||
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
|
||||
positiveText: "Show options",
|
||||
@ -45,14 +46,15 @@ class StartupUserMessagesManager {
|
||||
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
||||
});
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
void _showWhatsNewMessage() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_whatsNewMessageKey, true);
|
||||
prefs.setString(_whatsNewMessageKey, whatsNewUrl);
|
||||
eventBus.fire(ShowPageEvent(path: "/whats-new"));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
272
lib/managers/theme_manager.dart
Normal file
@ -0,0 +1,272 @@
|
||||
part of '../main.dart';
|
||||
|
||||
enum AppTheme {darkTheme, defaultTheme, haTheme}
|
||||
|
||||
class HAClientTheme {
|
||||
|
||||
static const TextTheme textTheme = TextTheme(
|
||||
display1: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
|
||||
display2: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
|
||||
headline: TextStyle(fontSize: 24, fontWeight: FontWeight.normal),
|
||||
title: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
|
||||
subhead: TextStyle(fontSize: 16, fontWeight: FontWeight.normal),
|
||||
body1: TextStyle(fontSize: 15, fontWeight: FontWeight.normal),
|
||||
body2: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
subtitle: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
caption: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
|
||||
overline: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.normal,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
button: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
);
|
||||
|
||||
static const offEntityStates = [
|
||||
EntityState.off,
|
||||
EntityState.closed,
|
||||
"below_horizon",
|
||||
"default",
|
||||
EntityState.idle,
|
||||
EntityState.alarm_disarmed,
|
||||
];
|
||||
|
||||
static const onEntityStates = [
|
||||
EntityState.on,
|
||||
"auto",
|
||||
EntityState.active,
|
||||
EntityState.playing,
|
||||
EntityState.paused,
|
||||
"above_horizon",
|
||||
EntityState.home,
|
||||
EntityState.open,
|
||||
EntityState.cleaning,
|
||||
EntityState.returning,
|
||||
"cool",
|
||||
EntityState.alarm_arming,
|
||||
EntityState.alarm_disarming,
|
||||
EntityState.alarm_pending,
|
||||
];
|
||||
|
||||
static const disabledEntityStates = [
|
||||
EntityState.unavailable,
|
||||
EntityState.unknown,
|
||||
];
|
||||
|
||||
static const alarmEntityStates = [
|
||||
EntityState.alarm_armed_away,
|
||||
EntityState.alarm_armed_custom_bypass,
|
||||
EntityState.alarm_armed_home,
|
||||
EntityState.alarm_armed_night,
|
||||
EntityState.alarm_triggered,
|
||||
"heat",
|
||||
];
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
static final HAClientTheme _instance = HAClientTheme
|
||||
._internal();
|
||||
|
||||
factory HAClientTheme() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
HAClientTheme._internal();
|
||||
|
||||
ThemeData getThemeData(AppTheme theme) {
|
||||
switch (theme) {
|
||||
case AppTheme.darkTheme:
|
||||
return darkTheme;
|
||||
break;
|
||||
case AppTheme.defaultTheme:
|
||||
return defaultTheme;
|
||||
break;
|
||||
case AppTheme.haTheme:
|
||||
return homeAssistantTheme;
|
||||
break;
|
||||
default:
|
||||
return defaultTheme;
|
||||
}
|
||||
}
|
||||
|
||||
final ThemeData defaultTheme = ThemeData.from(
|
||||
colorScheme: ColorScheme(
|
||||
primary: Color.fromRGBO(112, 154, 193, 1),
|
||||
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
||||
secondary: Color.fromRGBO(253, 216, 53, 1),
|
||||
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
||||
background: Color.fromRGBO(250, 250, 250, 1),
|
||||
surface: Colors.white,
|
||||
error: Colors.red,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.black87,
|
||||
onBackground: Colors.black87,
|
||||
onSurface: Colors.black87,
|
||||
onError: Colors.white,
|
||||
brightness: Brightness.light
|
||||
),
|
||||
textTheme: ThemeData.light().textTheme.copyWith(
|
||||
display1: textTheme.display1.copyWith(color: Colors.black54),
|
||||
display2: textTheme.display2.copyWith(color: Colors.redAccent),
|
||||
headline: textTheme.headline.copyWith(color: Colors.black87),
|
||||
title: textTheme.title.copyWith(color: Colors.black87),
|
||||
subhead: textTheme.subhead.copyWith(color: Colors.black54),
|
||||
body1: textTheme.body1.copyWith(color: Colors.black87),
|
||||
body2: textTheme.body2.copyWith(color: Colors.black87),
|
||||
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
|
||||
caption: textTheme.caption.copyWith(color: Colors.black45),
|
||||
overline: textTheme.overline.copyWith(color: Colors.black26),
|
||||
button: textTheme.button.copyWith(color: Colors.white),
|
||||
)
|
||||
);
|
||||
|
||||
final ThemeData homeAssistantTheme = ThemeData.from(
|
||||
colorScheme: ColorScheme(
|
||||
primary: Color.fromRGBO(2, 165, 238, 1),
|
||||
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
||||
secondary: Color.fromRGBO(253, 216, 53, 1),
|
||||
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
||||
background: Color.fromRGBO(250, 250, 250, 1),
|
||||
surface: Colors.white,
|
||||
error: Colors.red,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.black87,
|
||||
onBackground: Colors.black87,
|
||||
onSurface: Colors.black87,
|
||||
onError: Colors.white,
|
||||
brightness: Brightness.light
|
||||
),
|
||||
textTheme: ThemeData.light().textTheme.copyWith(
|
||||
display1: textTheme.display1.copyWith(color: Colors.black54),
|
||||
display2: textTheme.display2.copyWith(color: Colors.redAccent),
|
||||
headline: textTheme.headline.copyWith(color: Colors.black87),
|
||||
title: textTheme.title.copyWith(color: Colors.black87),
|
||||
subhead: textTheme.subhead.copyWith(color: Colors.black54),
|
||||
body1: textTheme.body1.copyWith(color: Colors.black87),
|
||||
body2: textTheme.body2.copyWith(color: Colors.black87),
|
||||
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
|
||||
caption: textTheme.caption.copyWith(color: Colors.black45),
|
||||
overline: textTheme.overline.copyWith(color: Colors.black26),
|
||||
button: textTheme.button.copyWith(color: Colors.white),
|
||||
)
|
||||
);
|
||||
|
||||
final ThemeData darkTheme = ThemeData.from(
|
||||
colorScheme: ColorScheme(
|
||||
primary: Color.fromRGBO(112, 154, 193, 1),
|
||||
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
||||
secondary: Color.fromRGBO(253, 216, 53, 1),
|
||||
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
||||
background: Color.fromRGBO(47, 49, 54, 1),
|
||||
surface: Color.fromRGBO(54, 57, 63, 1),
|
||||
error: Color.fromRGBO(183, 109, 109, 1),
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.black87,
|
||||
onBackground: Color.fromRGBO(220, 221, 222, 1),
|
||||
onSurface: Colors.white,
|
||||
onError: Colors.white,
|
||||
brightness: Brightness.dark
|
||||
),
|
||||
textTheme: textTheme
|
||||
);
|
||||
|
||||
Color getOnStateColor(BuildContext context) {
|
||||
return Theme.of(context).colorScheme.secondary;
|
||||
}
|
||||
|
||||
Color getOffStateColor(BuildContext context) {
|
||||
return Theme.of(context).colorScheme.primaryVariant;
|
||||
}
|
||||
|
||||
Color getDisabledStateColor(BuildContext context) {
|
||||
return Theme.of(context).disabledColor;
|
||||
}
|
||||
|
||||
Color getAlertStateColor(BuildContext context) {
|
||||
return Theme.of(context).colorScheme.error;
|
||||
}
|
||||
|
||||
Color getColorByEntityState(String state, BuildContext context) {
|
||||
if (onEntityStates.contains(state)) {
|
||||
return getOnStateColor(context);
|
||||
} else if (disabledEntityStates.contains(state)) {
|
||||
return getDisabledStateColor(context);
|
||||
} else if (alarmEntityStates.contains(state)) {
|
||||
return getAlertStateColor(context);
|
||||
} else {
|
||||
return getOffStateColor(context);
|
||||
}
|
||||
}
|
||||
|
||||
Color getGreenGaugeColor() {
|
||||
return Colors.green;
|
||||
}
|
||||
|
||||
Color getYellowGaugeColor() {
|
||||
return Colors.yellow;
|
||||
}
|
||||
|
||||
Color getRedGaugeColor() {
|
||||
return Colors.red;
|
||||
}
|
||||
|
||||
TextStyle getLinkTextStyle(BuildContext context) {
|
||||
ThemeData theme = Theme.of(context);
|
||||
return theme.textTheme.body1.copyWith(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle getActionTextStyle(BuildContext context) {
|
||||
ThemeData theme = Theme.of(context);
|
||||
return theme.textTheme.subhead.copyWith(
|
||||
color: Colors.blue
|
||||
);
|
||||
}
|
||||
|
||||
Color getBadgeColor(String entityDomain) {
|
||||
return badgeColors[entityDomain] ??
|
||||
badgeColors["default"];
|
||||
}
|
||||
|
||||
Color getOnBadgeTextColor() {
|
||||
return Colors.white;
|
||||
}
|
||||
|
||||
charts.Color chartHistoryStateColor(String state, int id, BuildContext context) {
|
||||
Color c = getColorByEntityState(state, context);
|
||||
if (c != null) {
|
||||
return charts.Color(
|
||||
r: c.red,
|
||||
g: c.green,
|
||||
b: c.blue,
|
||||
a: c.alpha
|
||||
);
|
||||
} else {
|
||||
double r = id.toDouble() % 10;
|
||||
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||
}
|
||||
}
|
||||
|
||||
Color historyStateColor(String state, int id, BuildContext context) {
|
||||
Color c = getColorByEntityState(state, context);
|
||||
if (c != null) {
|
||||
return c;
|
||||
} else {
|
||||
if (id > -1) {
|
||||
double r = id.toDouble() % 10;
|
||||
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
|
||||
} else {
|
||||
return getOnStateColor(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|