Compare commits
110 Commits
beta/0.7.5
...
beta/0.8.1
Author | SHA1 | Date | |
---|---|---|---|
e627a8b963 | |||
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 | |||
8b4df98cb9 | |||
7d30c2f9d5 | |||
44acabadfe | |||
6f3a2bb78d | |||
dd5f8b155d | |||
cd81fc72fd | |||
890da650dc | |||
9897b6a44b | |||
7969f54d3b | |||
7c18454de3 | |||
dcf5efddd1 | |||
a6541134e0 | |||
90504047b4 | |||
ca1eec6602 | |||
edc01d14b7 |
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,35 +7,16 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
**HA Client version:** [Main menu -> About HA Client]
|
||||||
Please provide as much information as possible.
|
|
||||||
-->
|
|
||||||
**HA Client version:** <!-- Main app menu => About HA Client -->
|
|
||||||
|
|
||||||
**Home Assistant version:** <!-- 0.94.1 for example -->
|
**Home Assistant version:**
|
||||||
|
|
||||||
**Device name:** <!-- Pixel 2 for example -->
|
**Device name:**
|
||||||
|
|
||||||
**Android version:** <!-- 8.1 for example -->
|
**Android version:**
|
||||||
|
|
||||||
**Connection type:** <!-- For example "Local IP" or "Remote UI" or "Own domain"-->
|
|
||||||
|
|
||||||
**Login type:** <!-- For example "HA Login" or "Manual token"-->
|
|
||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
<!--
|
[Replace with description]
|
||||||
Describe your issue here
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
<!--
|
[Replace with screenshots]
|
||||||
Please provide screenshots if it is a UI issue. Also you can attach screenshot from Home Assistant web UI as an expected result
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Logs**
|
|
||||||
<!--
|
|
||||||
Right after issue reproduced go to app menu and tap "Log". Copy log with a "Copy" button in the upper-right corner and post it below
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
[Replace this text with your logs]
|
|
||||||
```
|
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -15,7 +15,8 @@ build/
|
|||||||
.settings/
|
.settings/
|
||||||
|
|
||||||
flutter_export_environment.sh
|
flutter_export_environment.sh
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
premium_features_manager.class.dart
|
premium_features_manager.class.dart
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
|
@ -4,9 +4,5 @@ ENV ANDROID_HOME=/workspace/android-sdk \
|
|||||||
FLUTTER_ROOT=/workspace/flutter \
|
FLUTTER_ROOT=/workspace/flutter \
|
||||||
FLUTTER_HOME=/workspace/flutter
|
FLUTTER_HOME=/workspace/flutter
|
||||||
|
|
||||||
USER root
|
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
|
||||||
|
&& sdk install java 8.0.242.j9-adpt"
|
||||||
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
|
|
@ -8,7 +8,7 @@ tasks:
|
|||||||
touch /home/gitpod/.android/repositories.cfg
|
touch /home/gitpod/.android/repositories.cfg
|
||||||
init: |
|
init: |
|
||||||
echo "Installing Flutter SDK..."
|
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..."
|
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
|
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"
|
/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 doctor --android-licenses
|
||||||
flutter pub get
|
flutter pub get
|
||||||
command: |
|
command: |
|
||||||
flutter pub upgrade
|
|
||||||
echo "Ready to go!"
|
echo "Ready to go!"
|
||||||
flutter doctor
|
flutter doctor
|
||||||
vscode:
|
vscode:
|
||||||
|
BIN
.gradle/6.0.1/fileChanges/last-build.bin
Normal file
BIN
.gradle/6.0.1/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/6.0.1/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/6.0.1/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
0
.gradle/6.0.1/gc.properties
Normal file
0
.gradle/6.0.1/gc.properties
Normal file
@ -1,13 +1,14 @@
|
|||||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
|
||||||
# HA Client
|
# HA Client
|
||||||
## Native Android client for Home Assistant
|
## Native Android client for Home Assistant
|
||||||
### With notifications and Lovelace UI support
|
### 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)
|
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 [Spectrum.chat](https://spectrum.chat/ha-client) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||||
|
|
||||||
#### Pre-release CI build
|
#### Pre-release CI build
|
||||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
||||||
|
@ -78,10 +78,11 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.firebase:firebase-core:16.0.8'
|
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply plugin: 'io.fabric'
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
@ -17,11 +17,14 @@
|
|||||||
additional functionality it is fine to subclass or reimplement
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<application
|
<application
|
||||||
android:name=".Application"
|
|
||||||
android:label="HA Client"
|
android:label="HA Client"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
android:value="ha_notify" />
|
android:value="ha_notify" />
|
||||||
@ -33,13 +36,12 @@
|
|||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
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
|
<meta-data
|
||||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||||
android:value="true" />-->
|
android:resource="@drawable/launch_background" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme" />
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@ -48,14 +50,6 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
@ -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;
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import androidx.annotation.NonNull;
|
||||||
import io.flutter.app.FlutterActivity;
|
import io.flutter.embedding.android.FlutterActivity;
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
import io.flutter.plugins.share.FlutterShareReceiverActivity;
|
|
||||||
|
|
||||||
public class MainActivity extends FlutterShareReceiverActivity {
|
public class MainActivity extends FlutterActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||||
super.onCreate(savedInstanceState);
|
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||||
GeneratedPluginRegistrant.registerWith(this);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,7 @@
|
|||||||
Flutter draws its first frame -->
|
Flutter draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
</style>
|
</style>
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -2,11 +2,15 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://maven.fabric.io/public'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
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 {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://maven.fabric.io/public'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,4 +2,5 @@ org.gradle.jvmargs=-Xmx2g
|
|||||||
org.gradle.daemon=true
|
org.gradle.daemon=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.enableR8=true
|
||||||
|
1
android/settings_aar.gradle
Normal file
1
android/settings_aar.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
include ':app'
|
28
assets/html/cameraView.html
Normal file
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>
|
@ -13,4 +13,23 @@ window.externalApp.getExternalAuth = function(options) {
|
|||||||
window[options.callback](true, responseData);
|
window[options.callback](true, responseData);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
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');
|
||||||
|
}
|
||||||
};
|
};
|
@ -10,6 +10,7 @@ class HACard {
|
|||||||
bool showName;
|
bool showName;
|
||||||
bool showState;
|
bool showState;
|
||||||
bool showEmpty;
|
bool showEmpty;
|
||||||
|
bool showHeaderToggle;
|
||||||
int columnsCount;
|
int columnsCount;
|
||||||
List stateFilter;
|
List stateFilter;
|
||||||
List states;
|
List states;
|
||||||
@ -26,6 +27,7 @@ class HACard {
|
|||||||
this.linkedEntityWrapper,
|
this.linkedEntityWrapper,
|
||||||
this.columnsCount: 4,
|
this.columnsCount: 4,
|
||||||
this.showName: true,
|
this.showName: true,
|
||||||
|
this.showHeaderToggle: true,
|
||||||
this.showState: true,
|
this.showState: true,
|
||||||
this.stateFilter: const [],
|
this.stateFilter: const [],
|
||||||
this.showEmpty: true,
|
this.showEmpty: true,
|
||||||
@ -45,13 +47,70 @@ class HACard {
|
|||||||
|
|
||||||
List<EntityWrapper> getEntitiesToShow() {
|
List<EntityWrapper> getEntitiesToShow() {
|
||||||
return entities.where((entityWrapper) {
|
return entities.where((entityWrapper) {
|
||||||
if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) {
|
if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (stateFilter.isNotEmpty) {
|
List currentStateFilter;
|
||||||
return stateFilter.contains(entityWrapper.entity.state);
|
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) {
|
||||||
|
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState');
|
||||||
|
Logger.e('$e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return showByFilter;
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +132,33 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeader(name: card.name));
|
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.name,
|
||||||
|
trailing: headerSwitch
|
||||||
|
)
|
||||||
|
);
|
||||||
entitiesToShow.forEach((EntityWrapper entity) {
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
body.add(
|
body.add(
|
||||||
Padding(
|
Padding(
|
||||||
@ -280,21 +306,23 @@ class CardWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEntityButtonCard(BuildContext context) {
|
Widget _buildEntityButtonCard(BuildContext context) {
|
||||||
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
card.linkedEntityWrapper.overrideName = card.name?.toUpperCase() ??
|
||||||
card.linkedEntityWrapper.displayName.toUpperCase();
|
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||||
return Card(
|
return Card(
|
||||||
child: EntityModel(
|
child: EntityModel(
|
||||||
entityWrapper: card.linkedEntityWrapper,
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
child: EntityButtonCardBody(),
|
child: EntityButtonCardBody(
|
||||||
|
showName: card.showName,
|
||||||
|
),
|
||||||
handleTap: true
|
handleTap: true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGaugeCard(BuildContext context) {
|
Widget _buildGaugeCard(BuildContext context) {
|
||||||
card.linkedEntityWrapper.displayName = card.name ??
|
card.linkedEntityWrapper.overrideName = card.name ??
|
||||||
card.linkedEntityWrapper.displayName;
|
card.linkedEntityWrapper.displayName;
|
||||||
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
|
card.linkedEntityWrapper.unitOfMeasurementOverride = card.unit ??
|
||||||
card.linkedEntityWrapper.unitOfMeasurement;
|
card.linkedEntityWrapper.unitOfMeasurement;
|
||||||
return Card(
|
return Card(
|
||||||
child: EntityModel(
|
child: EntityModel(
|
||||||
@ -310,7 +338,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLightCard(BuildContext context) {
|
Widget _buildLightCard(BuildContext context) {
|
||||||
card.linkedEntityWrapper.displayName = card.name ??
|
card.linkedEntityWrapper.overrideName = card.name ??
|
||||||
card.linkedEntityWrapper.displayName;
|
card.linkedEntityWrapper.displayName;
|
||||||
return Card(
|
return Card(
|
||||||
child: EntityModel(
|
child: EntityModel(
|
||||||
@ -327,7 +355,11 @@ class CardWidget extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildUnsupportedCard(BuildContext context) {
|
Widget _buildUnsupportedCard(BuildContext context) {
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeader(name: card.name ?? ""));
|
body.add(
|
||||||
|
CardHeader(
|
||||||
|
name: card.name ?? ""
|
||||||
|
)
|
||||||
|
);
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (card.linkedEntityWrapper != null) {
|
if (card.linkedEntityWrapper != null) {
|
||||||
result.addAll(<Widget>[
|
result.addAll(<Widget>[
|
||||||
|
@ -2,8 +2,10 @@ part of '../../main.dart';
|
|||||||
|
|
||||||
class EntityButtonCardBody extends StatelessWidget {
|
class EntityButtonCardBody extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool showName;
|
||||||
|
|
||||||
EntityButtonCardBody({
|
EntityButtonCardBody({
|
||||||
Key key,
|
Key key, this.showName: true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -19,6 +21,7 @@ class EntityButtonCardBody extends StatelessWidget {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
child: FractionallySizedBox(
|
child: FractionallySizedBox(
|
||||||
widthFactor: 1,
|
widthFactor: 1,
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -39,13 +42,16 @@ class EntityButtonCardBody extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildName() {
|
Widget _buildName() {
|
||||||
return EntityName(
|
if (showName) {
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
return EntityName(
|
||||||
textOverflow: TextOverflow.ellipsis,
|
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||||
maxLines: 3,
|
textOverflow: TextOverflow.ellipsis,
|
||||||
wordsWrap: true,
|
maxLines: 3,
|
||||||
textAlign: TextAlign.center,
|
wordsWrap: true,
|
||||||
fontSize: Sizes.nameFontSize,
|
textAlign: TextAlign.center,
|
||||||
);
|
fontSize: Sizes.nameFontSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(width: 0, height: 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -64,6 +64,7 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 1.5,
|
aspectRatio: 1.5,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
@ -60,6 +60,7 @@ class GlanceCardEntityContainer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ class _LightCardBodyState extends State<LightCardBody> {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 1.5,
|
aspectRatio: 1.5,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
@ -51,6 +51,10 @@ class EntityUIAction {
|
|||||||
String holdNavigationPath;
|
String holdNavigationPath;
|
||||||
String holdService;
|
String holdService;
|
||||||
Map<String, dynamic> holdServiceData;
|
Map<String, dynamic> holdServiceData;
|
||||||
|
String doubleTapAction = EntityUIAction.none;
|
||||||
|
String doubleTapNavigationPath;
|
||||||
|
String doubleTapService;
|
||||||
|
Map<String, dynamic> doubleTapServiceData;
|
||||||
|
|
||||||
EntityUIAction({rawEntityData}) {
|
EntityUIAction({rawEntityData}) {
|
||||||
if (rawEntityData != null) {
|
if (rawEntityData != null) {
|
||||||
@ -76,6 +80,17 @@ class EntityUIAction {
|
|||||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
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"];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,12 +3,16 @@ part of '../../main.dart';
|
|||||||
class CameraEntity extends Entity {
|
class CameraEntity extends Entity {
|
||||||
|
|
||||||
static const SUPPORT_ON_OFF = 1;
|
static const SUPPORT_ON_OFF = 1;
|
||||||
|
static const SUPPORT_STREAM = 2;
|
||||||
|
|
||||||
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get supportOnOff => ((supportedFeatures &
|
bool get supportOnOff => ((supportedFeatures &
|
||||||
CameraEntity.SUPPORT_ON_OFF) ==
|
CameraEntity.SUPPORT_ON_OFF) ==
|
||||||
CameraEntity.SUPPORT_ON_OFF);
|
CameraEntity.SUPPORT_ON_OFF);
|
||||||
|
bool get supportStream => ((supportedFeatures &
|
||||||
|
CameraEntity.SUPPORT_STREAM) ==
|
||||||
|
CameraEntity.SUPPORT_STREAM);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
@ -2,7 +2,9 @@ part of '../../../main.dart';
|
|||||||
|
|
||||||
class CameraStreamView extends StatefulWidget {
|
class CameraStreamView extends StatefulWidget {
|
||||||
|
|
||||||
CameraStreamView({Key key}) : super(key: key);
|
final bool withControls;
|
||||||
|
|
||||||
|
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_CameraStreamViewState createState() => _CameraStreamViewState();
|
_CameraStreamViewState createState() => _CameraStreamViewState();
|
||||||
@ -10,49 +12,235 @@ class CameraStreamView extends StatefulWidget {
|
|||||||
|
|
||||||
class _CameraStreamViewState extends State<CameraStreamView> {
|
class _CameraStreamViewState extends State<CameraStreamView> {
|
||||||
|
|
||||||
|
CameraEntity _entity;
|
||||||
|
String _streamUrl = "";
|
||||||
|
VideoPlayerController _videoPlayerController;
|
||||||
|
Timer _monitorTimer;
|
||||||
|
bool _isLoaded = false;
|
||||||
|
double _aspectRatio = 1.33;
|
||||||
|
String _webViewHtml;
|
||||||
|
String _jsMessageChannelName = 'unknown';
|
||||||
|
Completer _loading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
CameraEntity _entity;
|
Future _loadResources() {
|
||||||
bool started = false;
|
if (_loading != null && !_loading.isCompleted) {
|
||||||
String streamUrl = "";
|
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().getCameraStream(_entity.entityId)
|
||||||
|
.then((data) {
|
||||||
|
if (_videoPlayerController != null) {
|
||||||
|
_videoPlayerController.dispose().then((_) => createPlayer(data));
|
||||||
|
} else {
|
||||||
|
createPlayer(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catchError((e) {
|
||||||
|
_loading.completeError(e);
|
||||||
|
Logger.e("[Camera Player] $e");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||||
|
.entityId}?token=${_entity.attributes['access_token']}';
|
||||||
|
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||||
|
rootBundle.loadString('assets/html/cameraView.html').then((file) {
|
||||||
|
_webViewHtml = Uri.dataFromString(
|
||||||
|
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||||
|
mimeType: 'text/html',
|
||||||
|
encoding: Encoding.getByName('utf-8')
|
||||||
|
).toString();
|
||||||
|
_loading.complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return _loading.future;
|
||||||
|
}
|
||||||
|
|
||||||
launchStream() {
|
void createPlayer(data) {
|
||||||
Launcher.launchURLInCustomTab(
|
_videoPlayerController = VideoPlayerController.network("${ConnectionManager().httpWebHost}${data["url"]}");
|
||||||
context: context,
|
_videoPlayerController.initialize().then((_) {
|
||||||
url: streamUrl
|
setState((){
|
||||||
|
_aspectRatio = _videoPlayerController.value.aspectRatio;
|
||||||
|
});
|
||||||
|
_loading.complete();
|
||||||
|
autoPlay();
|
||||||
|
startMonitor();
|
||||||
|
}).catchError((e) {
|
||||||
|
_loading.completeError(e);
|
||||||
|
Logger.e("[Camera Player] Error player init. Retrying");
|
||||||
|
_loadResources();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void autoPlay() {
|
||||||
|
if (!_videoPlayerController.value.isPlaying) {
|
||||||
|
_videoPlayerController.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void startMonitor() {
|
||||||
|
_monitorTimer?.cancel();
|
||||||
|
_monitorTimer = Timer.periodic(Duration(milliseconds: 500), (timer) {
|
||||||
|
if (_videoPlayerController.value.hasError) {
|
||||||
|
timer.cancel();
|
||||||
|
setState(() {
|
||||||
|
_isLoaded = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScreen() {
|
||||||
|
Widget screenWidget;
|
||||||
|
if (!_isLoaded) {
|
||||||
|
screenWidget = Center(
|
||||||
|
child: EntityPicture(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (_entity.supportStream) {
|
||||||
|
if (_videoPlayerController.value.initialized) {
|
||||||
|
screenWidget = VideoPlayer(_videoPlayerController);
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
setState((){
|
||||||
|
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AspectRatio(
|
||||||
|
aspectRatio: _aspectRatio,
|
||||||
|
child: screenWidget
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildControls() {
|
||||||
|
Widget playControl;
|
||||||
|
if (_entity.supportStream) {
|
||||||
|
playControl = Center(
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon((_videoPlayerController != null && _videoPlayerController.value.isPlaying) ? Icons.pause_circle_outline : Icons.play_circle_outline),
|
||||||
|
iconSize: 60,
|
||||||
|
color: Colors.amberAccent,
|
||||||
|
onPressed: (_videoPlayerController == null || _videoPlayerController.value.hasError || !_isLoaded) ? null :
|
||||||
|
() {
|
||||||
|
setState(() {
|
||||||
|
if (_videoPlayerController != null && _videoPlayerController.value.isPlaying) {
|
||||||
|
_videoPlayerController.pause();
|
||||||
|
} else {
|
||||||
|
_videoPlayerController.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
playControl = Container();
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
iconSize: 40,
|
||||||
|
color: Colors.amberAccent,
|
||||||
|
onPressed: _isLoaded ? () {
|
||||||
|
setState(() {
|
||||||
|
_isLoaded = false;
|
||||||
|
});
|
||||||
|
} : null,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: playControl,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.fullscreen),
|
||||||
|
iconSize: 40,
|
||||||
|
color: Colors.amberAccent,
|
||||||
|
onPressed: _isLoaded ? () {
|
||||||
|
_videoPlayerController?.pause();
|
||||||
|
eventBus.fire(ShowEntityPageEvent());
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (conext) => FullScreenPage(
|
||||||
|
child: EntityModel(
|
||||||
|
child: CameraStreamView(
|
||||||
|
withControls: false
|
||||||
|
),
|
||||||
|
handleTap: false,
|
||||||
|
entityWrapper: EntityWrapper(
|
||||||
|
entity: _entity
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
fullscreenDialog: true
|
||||||
|
)
|
||||||
|
).then((_) {
|
||||||
|
eventBus.fire(ShowEntityPageEvent(entity: _entity));
|
||||||
|
});
|
||||||
|
} : null,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!started) {
|
if (!_isLoaded && (_loading == null || _loading.isCompleted)) {
|
||||||
_entity = EntityModel
|
_loadResources().then((_) => setState((){ _isLoaded = true; }));
|
||||||
.of(context)
|
|
||||||
.entityWrapper
|
|
||||||
.entity;
|
|
||||||
started = true;
|
|
||||||
}
|
}
|
||||||
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
if (widget.withControls) {
|
||||||
.entityId}?token=${_entity.attributes['access_token']}';
|
return Card(
|
||||||
return Column(
|
child: Column(
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
Container(
|
children: <Widget>[
|
||||||
padding: const EdgeInsets.all(20.0),
|
_buildScreen(),
|
||||||
child: IconButton(
|
_buildControls()
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
],
|
||||||
iconSize: 50.0,
|
),
|
||||||
onPressed: () => launchStream(),
|
);
|
||||||
)
|
} else {
|
||||||
)
|
return _buildScreen();
|
||||||
],
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_monitorTimer?.cancel();
|
||||||
|
_videoPlayerController?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,9 +10,8 @@ class ClimateControlWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||||
|
|
||||||
bool _showPending = false;
|
bool _temperaturePending = false;
|
||||||
bool _changedHere = false;
|
bool _changedHere = false;
|
||||||
Timer _resetTimer;
|
|
||||||
Timer _tempThrottleTimer;
|
Timer _tempThrottleTimer;
|
||||||
Timer _targetTempThrottleTimer;
|
Timer _targetTempThrottleTimer;
|
||||||
double _tmpTemperature = 0.0;
|
double _tmpTemperature = 0.0;
|
||||||
@ -27,9 +26,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
bool _tmpAuxHeat = false;
|
bool _tmpAuxHeat = false;
|
||||||
|
|
||||||
void _resetVars(ClimateEntity entity) {
|
void _resetVars(ClimateEntity entity) {
|
||||||
_tmpTemperature = entity.temperature;
|
if (!_temperaturePending) {
|
||||||
_tmpTargetHigh = entity.targetHigh;
|
_tmpTemperature = entity.temperature;
|
||||||
_tmpTargetLow = entity.targetLow;
|
_tmpTargetHigh = entity.targetHigh;
|
||||||
|
_tmpTargetLow = entity.targetLow;
|
||||||
|
}
|
||||||
_tmpHVACMode = entity.state;
|
_tmpHVACMode = entity.state;
|
||||||
_tmpFanMode = entity.fanMode;
|
_tmpFanMode = entity.fanMode;
|
||||||
_tmpSwingMode = entity.swingMode;
|
_tmpSwingMode = entity.swingMode;
|
||||||
@ -38,7 +39,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
_tmpAuxHeat = entity.auxHeat;
|
_tmpAuxHeat = entity.auxHeat;
|
||||||
_tmpTargetHumidity = entity.targetHumidity;
|
_tmpTargetHumidity = entity.targetHumidity;
|
||||||
|
|
||||||
_showPending = false;
|
|
||||||
_changedHere = false;
|
_changedHere = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,46 +73,44 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _setTemperature(ClimateEntity entity) {
|
void _setTemperature(ClimateEntity entity) {
|
||||||
if (_tempThrottleTimer!=null) {
|
_tempThrottleTimer?.cancel();
|
||||||
_tempThrottleTimer.cancel();
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
|
_temperaturePending = true;
|
||||||
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
||||||
});
|
});
|
||||||
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
|
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
|
_temperaturePending = false;
|
||||||
ConnectionManager().callService(
|
ConnectionManager().callService(
|
||||||
domain: entity.domain,
|
domain: entity.domain,
|
||||||
service: "set_temperature",
|
service: "set_temperature",
|
||||||
entityId: entity.entityId,
|
entityId: entity.entityId,
|
||||||
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
|
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
|
||||||
);
|
);
|
||||||
_resetStateTimer(entity);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setTargetTemp(ClimateEntity entity) {
|
void _setTargetTemp(ClimateEntity entity) {
|
||||||
if (_targetTempThrottleTimer!=null) {
|
_targetTempThrottleTimer?.cancel();
|
||||||
_targetTempThrottleTimer.cancel();
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
|
_temperaturePending = true;
|
||||||
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
||||||
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
||||||
});
|
});
|
||||||
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
|
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
|
_temperaturePending = false;
|
||||||
ConnectionManager().callService(
|
ConnectionManager().callService(
|
||||||
domain: entity.domain,
|
domain: entity.domain,
|
||||||
service: "set_temperature",
|
service: "set_temperature",
|
||||||
entityId: entity.entityId,
|
entityId: entity.entityId,
|
||||||
data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
|
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,
|
entityId: entity.entityId,
|
||||||
data: {"humidity": "$_tmpTargetHumidity"}
|
data: {"humidity": "$_tmpTargetHumidity"}
|
||||||
);
|
);
|
||||||
_resetStateTimer(entity);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +138,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
entityId: entity.entityId,
|
entityId: entity.entityId,
|
||||||
data: {"hvac_mode": "$_tmpHVACMode"}
|
data: {"hvac_mode": "$_tmpHVACMode"}
|
||||||
);
|
);
|
||||||
_resetStateTimer(entity);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +151,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
entityId: entity.entityId,
|
entityId: entity.entityId,
|
||||||
data: {"swing_mode": "$_tmpSwingMode"}
|
data: {"swing_mode": "$_tmpSwingMode"}
|
||||||
);
|
);
|
||||||
_resetStateTimer(entity);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +159,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
_tmpFanMode = value;
|
_tmpFanMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"});
|
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;
|
_tmpPresetMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"});
|
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;
|
_tmpAuxHeat = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"});
|
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,11 +191,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final entityModel = EntityModel.of(context);
|
final entityModel = EntityModel.of(context);
|
||||||
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
Logger.d("[Climate widget build] changed here = $_changedHere");
|
||||||
if (_changedHere) {
|
if (_changedHere) {
|
||||||
_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
|
//_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
|
||||||
_changedHere = false;
|
_changedHere = false;
|
||||||
} else {
|
} else {
|
||||||
_resetTimer?.cancel();
|
|
||||||
_resetVars(entity);
|
_resetVars(entity);
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -321,7 +303,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
)),
|
)),
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTemperature,
|
value: _tmpTemperature,
|
||||||
fontColor: _showPending ? Colors.red : Colors.black,
|
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
||||||
onDec: () => _temperatureDown(entity),
|
onDec: () => _temperatureDown(entity),
|
||||||
onInc: () => _temperatureUp(entity),
|
onInc: () => _temperatureUp(entity),
|
||||||
)
|
)
|
||||||
@ -338,7 +320,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
controls.addAll(<Widget>[
|
controls.addAll(<Widget>[
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetLow,
|
value: _tmpTargetLow,
|
||||||
fontColor: _showPending ? Colors.red : Colors.black,
|
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
||||||
onDec: () => _targetLowDown(entity),
|
onDec: () => _targetLowDown(entity),
|
||||||
onInc: () => _targetLowUp(entity),
|
onInc: () => _targetLowUp(entity),
|
||||||
),
|
),
|
||||||
@ -351,7 +333,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
controls.add(
|
controls.add(
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetHigh,
|
value: _tmpTargetHigh,
|
||||||
fontColor: _showPending ? Colors.red : Colors.black,
|
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
||||||
onDec: () => _targetHighDown(entity),
|
onDec: () => _targetHighDown(entity),
|
||||||
onInc: () => _targetHighUp(entity),
|
onInc: () => _targetHighUp(entity),
|
||||||
)
|
)
|
||||||
@ -429,7 +411,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_resetTimer?.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ part of '../../../main.dart';
|
|||||||
class ModeSelectorWidget extends StatelessWidget {
|
class ModeSelectorWidget extends StatelessWidget {
|
||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
final List<String> options;
|
final List options;
|
||||||
final String value;
|
final String value;
|
||||||
final double captionFontSize;
|
final double captionFontSize;
|
||||||
final double valueFontSize;
|
final double valueFontSize;
|
||||||
@ -45,10 +45,10 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
hint: Text("Select ${caption.toLowerCase()}"),
|
hint: Text("Select ${caption.toLowerCase()}"),
|
||||||
items: options.map((String value) {
|
items: options.map((value) {
|
||||||
return new DropdownMenuItem<String>(
|
return new DropdownMenuItem<String>(
|
||||||
value: value,
|
value: '$value',
|
||||||
child: Text(value),
|
child: Text('$value'),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (mode) => onChange(mode),
|
onChanged: (mode) => onChange(mode),
|
||||||
|
@ -61,6 +61,11 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
entityModel.entityWrapper.handleTap();
|
entityModel.entityWrapper.handleTap();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onDoubleTap: () {
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
entityModel.entityWrapper.handleDoubleTap();
|
||||||
|
}
|
||||||
|
},
|
||||||
child: result,
|
child: result,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -221,7 +221,7 @@ class Entity {
|
|||||||
|
|
||||||
String getAttribute(String attributeName) {
|
String getAttribute(String attributeName) {
|
||||||
if (attributes != null) {
|
if (attributes != null) {
|
||||||
return attributes["$attributeName"];
|
return attributes["$attributeName"].toString();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
70
lib/entities/entity_picture.widget.dart
Normal file
70
lib/entities/entity_picture.widget.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
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) {
|
||||||
|
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: EntityColor.defaultStateColor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,30 +2,29 @@ part of '../main.dart';
|
|||||||
|
|
||||||
class EntityWrapper {
|
class EntityWrapper {
|
||||||
|
|
||||||
String displayName;
|
String overrideName;
|
||||||
String icon;
|
final String overrideIcon;
|
||||||
String unitOfMeasurement;
|
|
||||||
String entityPicture;
|
|
||||||
EntityUIAction uiAction;
|
EntityUIAction uiAction;
|
||||||
Entity entity;
|
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({
|
EntityWrapper({
|
||||||
this.entity,
|
this.entity,
|
||||||
String icon,
|
this.overrideIcon,
|
||||||
String displayName,
|
this.overrideName,
|
||||||
this.uiAction
|
this.uiAction,
|
||||||
|
this.stateFilter
|
||||||
}) {
|
}) {
|
||||||
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
|
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||||
this.icon = icon ?? entity.icon;
|
|
||||||
if (icon == null) {
|
|
||||||
entityPicture = entity.entityPicture;
|
|
||||||
}
|
|
||||||
this.displayName = displayName ?? entity.displayName;
|
|
||||||
if (uiAction == null) {
|
if (uiAction == null) {
|
||||||
uiAction = EntityUIAction();
|
uiAction = EntityUIAction();
|
||||||
}
|
}
|
||||||
unitOfMeasurement = entity.unitOfMeasurement;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +57,7 @@ class EntityWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.navigate: {
|
case EntityUIAction.navigate: {
|
||||||
if (uiAction.tapService.startsWith("/")) {
|
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
|
||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Logger.w("Local urls is not supported yet");
|
Logger.w("Local urls is not supported yet");
|
||||||
} else {
|
} else {
|
||||||
@ -98,7 +97,7 @@ class EntityWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.navigate: {
|
case EntityUIAction.navigate: {
|
||||||
if (uiAction.holdService.startsWith("/")) {
|
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
|
||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Logger.w("Local urls is not supported yet");
|
Logger.w("Local urls is not supported yet");
|
||||||
} else {
|
} else {
|
||||||
@ -113,4 +112,44 @@ 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(entity: entity));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityUIAction.navigate: {
|
||||||
|
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
|
||||||
|
//TODO handle local urls
|
||||||
|
Logger.w("Local urls is not supported yet");
|
||||||
|
} else {
|
||||||
|
Launcher.launchURL(uiAction.doubleTapService);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -84,25 +84,22 @@ class MediaPlayerEntity extends Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool canCalculateActualPosition() {
|
bool canCalculateActualPosition() {
|
||||||
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds >= 0;
|
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
double getActualPosition() {
|
double getActualPosition() {
|
||||||
double result = 0;
|
double result = 0;
|
||||||
if (canCalculateActualPosition()) {
|
Duration durationD;
|
||||||
Duration durationD;
|
Duration positionD;
|
||||||
Duration positionD;
|
durationD = Duration(seconds: durationSeconds);
|
||||||
durationD = Duration(seconds: durationSeconds);
|
positionD = Duration(
|
||||||
positionD = Duration(
|
|
||||||
seconds: positionSeconds);
|
seconds: positionSeconds);
|
||||||
result = positionD.inSeconds.toDouble();
|
result = positionD.inSeconds.toDouble();
|
||||||
int differenceInSeconds = DateTime
|
int differenceInSeconds = DateTime
|
||||||
.now()
|
.now()
|
||||||
.difference(positionLastUpdated)
|
.difference(positionLastUpdated)
|
||||||
.inSeconds;
|
.inSeconds;
|
||||||
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
|
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,13 +22,13 @@ class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
||||||
double progress;
|
double progress = 0;
|
||||||
int currentPosition;
|
int currentPosition;
|
||||||
if (entity.canCalculateActualPosition()) {
|
if (entity.canCalculateActualPosition()) {
|
||||||
currentPosition = entity.getActualPosition().toInt();
|
currentPosition = entity.getActualPosition().toInt();
|
||||||
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
|
if (currentPosition > 0) {
|
||||||
} else {
|
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
|
||||||
progress = 0;
|
}
|
||||||
}
|
}
|
||||||
return LinearProgressIndicator(
|
return LinearProgressIndicator(
|
||||||
value: progress,
|
value: progress,
|
||||||
|
@ -356,13 +356,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
volumeStepWidget = Row(
|
volumeStepWidget = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
|
||||||
onPressed: () => _setVolumeUp(entity.entityId)
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||||
onPressed: () => _setVolumeDown(entity.entityId)
|
onPressed: () => _setVolumeDown(entity.entityId)
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||||
|
onPressed: () => _setVolumeUp(entity.entityId)
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -460,7 +460,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _duplicateTo(entity) {
|
void _duplicateTo(entity) {
|
||||||
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
|
if (entity.canCalculateActualPosition()) {
|
||||||
|
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
|
||||||
|
} else {
|
||||||
|
HomeAssistant().savedPlayerPosition = 0;
|
||||||
|
}
|
||||||
Navigator.of(context).pushNamed("/play-media", arguments: {
|
Navigator.of(context).pushNamed("/play-media", arguments: {
|
||||||
"url": entity.attributes["media_content_id"],
|
"url": entity.attributes["media_content_id"],
|
||||||
"type": entity.attributes["media_content_type"]
|
"type": entity.attributes["media_content_type"]
|
||||||
|
@ -152,39 +152,12 @@ class EntityCollection {
|
|||||||
return _allEntities[entityId] != null;
|
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 _allEntities.values.where((entity) {
|
||||||
return domains.contains(entity.domain) &&
|
return
|
||||||
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
(excludeDomains.isEmpty || !excludeDomains.contains(entity.domain)) &&
|
||||||
|
(includeDomains.isEmpty || includeDomains.contains(entity.domain)) &&
|
||||||
|
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
||||||
}).toList();
|
}).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 {
|
class HomeAssistant {
|
||||||
|
|
||||||
|
static const DEFAULT_DASHBOARD = 'lovelace';
|
||||||
|
|
||||||
static final HomeAssistant _instance = HomeAssistant._internal();
|
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||||
|
|
||||||
factory HomeAssistant() {
|
factory HomeAssistant() {
|
||||||
@ -11,27 +13,33 @@ class HomeAssistant {
|
|||||||
EntityCollection entities;
|
EntityCollection entities;
|
||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
Map services;
|
|
||||||
String _userName;
|
String _userName;
|
||||||
bool childMode;
|
String _lovelaceDashbordUrl;
|
||||||
HSVColor savedColor;
|
HSVColor savedColor;
|
||||||
int savedPlayerPosition;
|
int savedPlayerPosition;
|
||||||
String sendToPlayerId;
|
String sendToPlayerId;
|
||||||
String sendFromPlayerId;
|
String sendFromPlayerId;
|
||||||
|
Map services;
|
||||||
|
bool autoUi = false;
|
||||||
|
|
||||||
String fcmToken;
|
String fcmToken;
|
||||||
|
|
||||||
Map _rawLovelaceData;
|
Map _rawLovelaceData;
|
||||||
|
var _rawStates;
|
||||||
|
var _rawUserInfo;
|
||||||
|
var _rawPanels;
|
||||||
|
|
||||||
|
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
|
||||||
|
|
||||||
List<Panel> panels = [];
|
List<Panel> panels = [];
|
||||||
|
|
||||||
Duration fetchTimeout = Duration(seconds: 30);
|
Duration fetchTimeout = Duration(seconds: 30);
|
||||||
|
|
||||||
String get locationName {
|
String get locationName {
|
||||||
if (ConnectionManager().useLovelace) {
|
if (!autoUi) {
|
||||||
return ui?.title ?? "";
|
return ui?.title ?? "Home";
|
||||||
} else {
|
} else {
|
||||||
return _instanceConfig["location_name"] ?? "";
|
return _instanceConfig["location_name"] ?? "Home";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String get userName => _userName ?? locationName;
|
String get userName => _userName ?? locationName;
|
||||||
@ -42,38 +50,37 @@ class HomeAssistant {
|
|||||||
|
|
||||||
HomeAssistant._internal() {
|
HomeAssistant._internal() {
|
||||||
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||||
|
ConnectionManager().onLovelaceUpdatedCallback = _handleLovelaceUpdate;
|
||||||
DeviceInfoManager().loadDeviceInfo();
|
DeviceInfoManager().loadDeviceInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Completer _fetchCompleter;
|
Completer _fetchCompleter;
|
||||||
|
|
||||||
Future fetchData() {
|
Future fetchData(bool uiOnly) {
|
||||||
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||||
Logger.w("Previous data fetch is not completed yet");
|
Logger.w("Previous data fetch is not completed yet");
|
||||||
return _fetchCompleter.future;
|
return _fetchCompleter.future;
|
||||||
}
|
}
|
||||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
|
||||||
_fetchCompleter = Completer();
|
_fetchCompleter = Completer();
|
||||||
List<Future> futures = [];
|
List<Future> futures = [];
|
||||||
futures.add(_getStates());
|
if (!uiOnly) {
|
||||||
if (ConnectionManager().useLovelace) {
|
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||||
futures.add(_getLovelace());
|
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((_) {
|
Future.wait(futures).then((_) {
|
||||||
if (isMobileAppEnabled) {
|
if (isMobileAppEnabled) {
|
||||||
if (!childMode) _createUI();
|
_createUI();
|
||||||
_fetchCompleter.complete();
|
_fetchCompleter.complete();
|
||||||
MobileAppIntegrationManager.checkAppRegistration();
|
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
||||||
} else {
|
} 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(HAError("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) {
|
}).catchError((e) {
|
||||||
_fetchCompleter.completeError(e);
|
_fetchCompleter.completeError(e);
|
||||||
@ -81,6 +88,48 @@ class HomeAssistant {
|
|||||||
return _fetchCompleter.future;
|
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 (isMobileAppEnabled) {
|
||||||
|
_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) {
|
||||||
|
await prefs.setBool('cached', false);
|
||||||
|
Logger.e('Error saving cache: $e');
|
||||||
|
}
|
||||||
|
Logger.d('Done saving cache');
|
||||||
|
}
|
||||||
|
|
||||||
Future logout() async {
|
Future logout() async {
|
||||||
Logger.d("Logging out...");
|
Logger.d("Logging out...");
|
||||||
await ConnectionManager().logout().then((_) {
|
await ConnectionManager().logout().then((_) {
|
||||||
@ -90,80 +139,181 @@ class HomeAssistant {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getConfig() async {
|
Future _getConfig(SharedPreferences sharedPrefs) async {
|
||||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
if (sharedPrefs != null) {
|
||||||
_instanceConfig = Map.from(data);
|
try {
|
||||||
}).catchError((e) {
|
var data = json.decode(sharedPrefs.getString('cached_config'));
|
||||||
throw HAError("Error getting config: ${e}");
|
_parseConfig(data);
|
||||||
});
|
} catch (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"));
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
return completer.future;
|
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
|
||||||
|
throw HAError("Error getting config: $e");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getUserInfo() async {
|
void _parseConfig(data) {
|
||||||
|
_instanceConfig = Map.from(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getStates(SharedPreferences sharedPrefs) async {
|
||||||
|
if (sharedPrefs != null) {
|
||||||
|
try {
|
||||||
|
var data = json.decode(sharedPrefs.getString('cached_states'));
|
||||||
|
_parseStates(data);
|
||||||
|
} catch (e) {
|
||||||
|
throw HAError("Error getting states: $e");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||||
|
(data) => _parseStates(data)
|
||||||
|
).catchError((e) {
|
||||||
|
throw HAError("Error getting states: $e");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseStates(data) {
|
||||||
|
_rawStates = data;
|
||||||
|
entities.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getLovelace(SharedPreferences sharedPrefs) {
|
||||||
|
if (sharedPrefs != null) {
|
||||||
|
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 (_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" == "config_not_found") {
|
||||||
|
autoUi = true;
|
||||||
|
_rawLovelaceData = null;
|
||||||
|
completer.complete();
|
||||||
|
} else {
|
||||||
|
completer.completeError(HAError("Error getting lovelace config: $e"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getServices(SharedPreferences prefs) async {
|
||||||
|
if (prefs != null) {
|
||||||
|
try {
|
||||||
|
var data = json.decode(prefs.getString('cached_services'));
|
||||||
|
_parseServices(data);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.w("Can't get services: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
|
||||||
|
Logger.w("Can't get services: $e");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseServices(data) {
|
||||||
|
services = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getUserInfo(SharedPreferences sharedPrefs) async {
|
||||||
_userName = null;
|
_userName = null;
|
||||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) {
|
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
|
||||||
_userName = data["name"];
|
|
||||||
childMode = _userName.startsWith("[child]");
|
|
||||||
}).catchError((e) {
|
|
||||||
Logger.w("Can't get user info: $e");
|
Logger.w("Can't get user info: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getServices() async {
|
void _parseUserInfo(data) {
|
||||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
|
_rawUserInfo = data;
|
||||||
Logger.d("Got ${data.length} services");
|
_userName = data["name"];
|
||||||
services = data;
|
|
||||||
}).catchError((e) {
|
|
||||||
Logger.w("Can't get services: $e");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getPanels() async {
|
Future _getPanels(SharedPreferences sharedPrefs) async {
|
||||||
panels.clear();
|
if (sharedPrefs != null) {
|
||||||
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
|
try {
|
||||||
data.forEach((k,v) {
|
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
||||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
_parsePanels(data);
|
||||||
panels.add(Panel(
|
} catch (e) {
|
||||||
id: k,
|
throw HAError("Error getting panels list: $e");
|
||||||
type: v["component_name"],
|
}
|
||||||
title: title,
|
} else {
|
||||||
urlPath: v["url_path"],
|
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e) {
|
||||||
config: v["config"],
|
throw HAError("Error getting panels list: $e");
|
||||||
icon: v["icon"]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}
|
||||||
throw HAError("Error getting panels list: $e");
|
}
|
||||||
|
|
||||||
|
void _parsePanels(data) {
|
||||||
|
_rawPanels = data;
|
||||||
|
panels.clear();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleLovelaceUpdate() {
|
||||||
|
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||||
|
eventBus.fire(new LovelaceChangedEvent());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleEntityStateChange(Map eventData) {
|
void _handleEntityStateChange(Map eventData) {
|
||||||
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||||
if (_fetchCompleter.isCompleted) {
|
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||||
Map data = Map.from(eventData);
|
Map data = Map.from(eventData);
|
||||||
eventBus.fire(new StateChangedEvent(
|
eventBus.fire(new StateChangedEvent(
|
||||||
entityId: data["entity_id"],
|
entityId: data["entity_id"],
|
||||||
@ -172,212 +322,27 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseLovelace() {
|
bool isServiceExist(String service) {
|
||||||
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
return services != null &&
|
||||||
ui.title = _rawLovelaceData["title"];
|
services.isNotEmpty &&
|
||||||
int viewCounter = 0;
|
services.containsKey(service);
|
||||||
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'] ?? true,
|
|
||||||
showState: rawCardInfo['show_state'] ?? true,
|
|
||||||
showEmpty: rawCardInfo['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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _createUI() {
|
void _createUI() {
|
||||||
ui = HomeAssistantUI();
|
Logger.d("Creating Lovelace UI");
|
||||||
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
|
||||||
Logger.d("Creating Lovelace UI");
|
if (isServiceExist('zha_map')) {
|
||||||
_parseLovelace();
|
panels.add(
|
||||||
} else {
|
Panel(
|
||||||
Logger.d("Creating group-based UI");
|
id: 'haclient_zha',
|
||||||
int viewCounter = 0;
|
componentName: 'haclient_zha',
|
||||||
if (!entities.hasDefaultView) {
|
title: 'ZHA',
|
||||||
HAView view = HAView(
|
urlPath: '/haclient_zha',
|
||||||
count: viewCounter,
|
icon: 'mdi:zigbee'
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
119
lib/main.dart
119
lib/main.dart
@ -1,6 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -8,6 +10,7 @@ import 'package:web_socket_channel/io.dart';
|
|||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:date_format/date_format.dart';
|
import 'package:date_format/date_format.dart';
|
||||||
@ -22,15 +25,15 @@ import 'package:device_info/device_info.dart';
|
|||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
import 'plugins/circular_slider/single_circular_slider.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/dynamic_multi_column_layout.dart';
|
||||||
import 'plugins/spoiler_card.dart';
|
import 'plugins/spoiler_card.dart';
|
||||||
import 'package:uni_links/uni_links.dart';
|
|
||||||
import 'package:workmanager/workmanager.dart' as workManager;
|
import 'package:workmanager/workmanager.dart' as workManager;
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:battery/battery.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:video_player/video_player.dart';
|
||||||
|
|
||||||
import 'utils/logger.dart';
|
import 'utils/logger.dart';
|
||||||
|
|
||||||
@ -85,6 +88,7 @@ part 'entities/slider/widgets/slider_controls.dart';
|
|||||||
part 'entities/text/widgets/text_input_state.dart';
|
part 'entities/text/widgets/text_input_state.dart';
|
||||||
part 'entities/select/widgets/select_state.dart';
|
part 'entities/select/widgets/select_state.dart';
|
||||||
part 'entities/simple_state.widget.dart';
|
part 'entities/simple_state.widget.dart';
|
||||||
|
part 'entities/entity_picture.widget.dart';
|
||||||
part 'entities/timer/widgets/timer_state.dart';
|
part 'entities/timer/widgets/timer_state.dart';
|
||||||
part 'entities/climate/widgets/climate_state.widget.dart';
|
part 'entities/climate/widgets/climate_state.widget.dart';
|
||||||
part 'entities/cover/widgets/cover_state.dart';
|
part 'entities/cover/widgets/cover_state.dart';
|
||||||
@ -106,8 +110,9 @@ part 'pages/widgets/product_purchase.widget.dart';
|
|||||||
part 'pages/widgets/page_loading_indicator.dart';
|
part 'pages/widgets/page_loading_indicator.dart';
|
||||||
part 'pages/widgets/page_loading_error.dart';
|
part 'pages/widgets/page_loading_error.dart';
|
||||||
part 'pages/panel.page.dart';
|
part 'pages/panel.page.dart';
|
||||||
part 'pages/main.page.dart';
|
part 'pages/main/main.page.dart';
|
||||||
part 'pages/integration_settings.page.dart';
|
part 'pages/integration_settings.page.dart';
|
||||||
|
part 'pages/zha_page.dart';
|
||||||
part 'home_assistant.class.dart';
|
part 'home_assistant.class.dart';
|
||||||
part 'pages/log.page.dart';
|
part 'pages/log.page.dart';
|
||||||
part 'pages/entity.page.dart';
|
part 'pages/entity.page.dart';
|
||||||
@ -137,13 +142,13 @@ 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_seek_bar.widget.dart';
|
||||||
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
|
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
|
||||||
part 'pages/whats_new.page.dart';
|
part 'pages/whats_new.page.dart';
|
||||||
|
part 'pages/fullscreen.page.dart';
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
final SentryClient _sentry = SentryClient(dsn: "https://03ef364745cc4c23a60ddbc874c69925@sentry.io/1836118");
|
|
||||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersionNumber = "0.7.5";
|
const appVersionNumber = "0.8.0";
|
||||||
const appVersionAdd = "";
|
const appVersionAdd = "";
|
||||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
const appVersion = "$appVersionNumber$appVersionAdd";
|
||||||
|
|
||||||
@ -152,42 +157,69 @@ Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
|||||||
if (Logger.isInDebugMode) {
|
if (Logger.isInDebugMode) {
|
||||||
Logger.e('Caught error: $error');
|
Logger.e('Caught error: $error');
|
||||||
Logger.p(stackTrace);
|
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 {
|
void main() async {
|
||||||
|
Crashlytics.instance.enableInDevMode = false;
|
||||||
|
|
||||||
FlutterError.onError = (FlutterErrorDetails details) {
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
Logger.e(" Caut Flutter runtime error: ${details.exception}");
|
Logger.e(" Caut Flutter runtime error: ${details.exception}");
|
||||||
if (Logger.isInDebugMode) {
|
if (Logger.isInDebugMode) {
|
||||||
FlutterError.dumpErrorToConsole(details);
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
runZoned(() {
|
runZoned(() {
|
||||||
workManager.Workmanager.initialize(
|
|
||||||
updateDeviceLocationIsolate,
|
|
||||||
isInDebugMode: false
|
|
||||||
);
|
|
||||||
runApp(new HAClientApp());
|
runApp(new HAClientApp());
|
||||||
|
|
||||||
}, onError: (error, stack) {
|
}, onError: (error, stack) {
|
||||||
_reportError(error, stack);
|
_reportError(error, stack);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class HAClientApp extends StatelessWidget {
|
class HAClientApp extends StatefulWidget {
|
||||||
|
|
||||||
|
@override
|
||||||
|
_HAClientAppState createState() => new _HAClientAppState();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HAClientAppState extends State<HAClientApp> {
|
||||||
|
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
InAppPurchaseConnection.enablePendingPurchases();
|
||||||
|
final Stream purchaseUpdates =
|
||||||
|
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||||
|
_subscription = purchaseUpdates.listen((purchases) {
|
||||||
|
_handlePurchaseUpdates(purchases);
|
||||||
|
});
|
||||||
|
workManager.Workmanager.initialize(
|
||||||
|
updateDeviceLocationIsolate,
|
||||||
|
isInDebugMode: false
|
||||||
|
);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePurchaseUpdates(purchase) {
|
||||||
|
if (purchase is List<PurchaseDetails>) {
|
||||||
|
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||||
|
eventBus.fire(ShowPopupMessageEvent(
|
||||||
|
title: "Thanks a lot!",
|
||||||
|
body: "Thank you for supporting HA Client development!",
|
||||||
|
buttonText: "Ok"
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
@ -208,8 +240,43 @@ class HAClientApp extends StatelessWidget {
|
|||||||
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
|
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
|
||||||
),
|
),
|
||||||
"/log-view": (context) => LogViewPage(title: "Log"),
|
"/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(),
|
||||||
|
"/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: "http://ha-client.app/docs#authentication")
|
||||||
|
),
|
||||||
|
title: new Text("Login with HA"),
|
||||||
|
actions: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text("Manual", style: TextStyle(color: Colors.white)),
|
||||||
|
onPressed: () {
|
||||||
|
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,46 +9,37 @@ class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AuthManager._internal();
|
AuthManager._internal();
|
||||||
StreamSubscription deepLinksSubscription;
|
|
||||||
|
|
||||||
Future start({String oauthUrl}) {
|
Future start({String oauthUrl}) {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
deepLinksSubscription?.cancel();
|
final flutterWebviewPlugin = new standaloneWebview.FlutterWebviewPlugin();
|
||||||
deepLinksSubscription = getUriLinksStream().listen((Uri uri) {
|
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
||||||
Logger.d("[LINKED AUTH] We got something private");
|
if (url.startsWith("https://ha-client.app/service/auth_callback.html")) {
|
||||||
_getTempToken(oauthUrl, uri.queryParameters["code"])
|
Logger.d("url=$url");
|
||||||
.then((tempToken) => completer.complete(tempToken))
|
String authCode = url.split("=")[1];
|
||||||
.catchError((_){
|
Logger.d("authCode=$authCode");
|
||||||
completer.completeError(HAError("Auth error"));
|
Logger.d("We have auth code. Getting temporary access token...");
|
||||||
});
|
ConnectionManager().sendHTTPPost(
|
||||||
}, onError: (err) {
|
endPoint: "/auth/token",
|
||||||
Logger.e("[LINKED AUTH] Error handling linked auth: $e");
|
contentType: "application/x-www-form-urlencoded",
|
||||||
completer.completeError(HAError("Auth error"));
|
includeAuthHeader: false,
|
||||||
});
|
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('https://ha-client.app')}"
|
||||||
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')}"
|
|
||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("Got temp token");
|
Logger.d("Got temp token");
|
||||||
String tempToken = json.decode(response)['access_token'];
|
String tempToken = json.decode(response)['access_token'];
|
||||||
|
Logger.d("Closing webview...");
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
completer.complete(tempToken);
|
completer.complete(tempToken);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
//flutterWebviewPlugin.close();
|
|
||||||
Logger.e("Error getting temp token: ${e.toString()}");
|
Logger.e("Error getting temp token: ${e.toString()}");
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
completer.completeError(HAError("Error getting temp token"));
|
completer.completeError(HAError("Error getting temp token"));
|
||||||
});
|
}).whenComplete(() => flutterWebviewPlugin.close());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Logger.d("Launching OAuth");
|
||||||
|
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -19,7 +19,6 @@ class ConnectionManager {
|
|||||||
String _tempToken;
|
String _tempToken;
|
||||||
String oauthUrl;
|
String oauthUrl;
|
||||||
String webhookId;
|
String webhookId;
|
||||||
bool useLovelace = true;
|
|
||||||
bool settingsLoaded = false;
|
bool settingsLoaded = false;
|
||||||
bool get isAuthenticated => _token != null;
|
bool get isAuthenticated => _token != null;
|
||||||
StreamSubscription _socketSubscription;
|
StreamSubscription _socketSubscription;
|
||||||
@ -28,6 +27,7 @@ class ConnectionManager {
|
|||||||
bool isConnected = false;
|
bool isConnected = false;
|
||||||
|
|
||||||
var onStateChangeCallback;
|
var onStateChangeCallback;
|
||||||
|
var onLovelaceUpdatedCallback;
|
||||||
|
|
||||||
IOWebSocketChannel _socket;
|
IOWebSocketChannel _socket;
|
||||||
|
|
||||||
@ -38,9 +38,8 @@ class ConnectionManager {
|
|||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
bool stopInit = false;
|
bool stopInit = false;
|
||||||
if (loadSettings) {
|
if (loadSettings) {
|
||||||
Logger.e("Loading settings...");
|
Logger.d("Loading settings...");
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
|
||||||
_domain = prefs.getString('hassio-domain');
|
_domain = prefs.getString('hassio-domain');
|
||||||
_port = prefs.getString('hassio-port');
|
_port = prefs.getString('hassio-port');
|
||||||
webhookId = prefs.getString('app-webhook-id');
|
webhookId = prefs.getString('app-webhook-id');
|
||||||
@ -59,9 +58,9 @@ class ConnectionManager {
|
|||||||
_token = await storage.read(key: "hacl_llt");
|
_token = await storage.read(key: "hacl_llt");
|
||||||
Logger.e("Long-lived token read successful");
|
Logger.e("Long-lived token read successful");
|
||||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||||
'http://ha-client.homemade.systems')}&redirect_uri=${Uri
|
'https://ha-client.app')}&redirect_uri=${Uri
|
||||||
.encodeComponent(
|
.encodeComponent(
|
||||||
'haclient://auth')}";
|
'https://ha-client.app/service/auth_callback.html')}";
|
||||||
settingsLoaded = true;
|
settingsLoaded = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||||
@ -98,16 +97,23 @@ class ConnectionManager {
|
|||||||
|
|
||||||
void _doConnect({Completer completer, bool forceReconnect}) {
|
void _doConnect({Completer completer, bool forceReconnect}) {
|
||||||
if (forceReconnect || !isConnected) {
|
if (forceReconnect || !isConnected) {
|
||||||
_connect().timeout(connectTimeout, onTimeout: () {
|
_disconnect().then((_){
|
||||||
_disconnect().then((_) {
|
_connect().timeout(connectTimeout).then((_) {
|
||||||
if (completer != null && !completer.isCompleted) {
|
completer?.complete();
|
||||||
completer.completeError(HAError("Connection timeout"));
|
}).catchError((e) {
|
||||||
}
|
_disconnect().then((_) {
|
||||||
|
if (e is TimeoutException) {
|
||||||
|
if (connecting != null && !connecting.isCompleted) {
|
||||||
|
connecting.completeError(HAError("Connection timeout"));
|
||||||
|
}
|
||||||
|
completer?.completeError(HAError("Connection timeout"));
|
||||||
|
} else if (e is HAError) {
|
||||||
|
completer?.completeError(e);
|
||||||
|
} else {
|
||||||
|
completer?.completeError(HAError("${e.toString()}"));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}).then((_) {
|
|
||||||
completer?.complete();
|
|
||||||
}).catchError((e) {
|
|
||||||
completer?.completeError(e);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
completer?.complete();
|
completer?.complete();
|
||||||
@ -124,40 +130,54 @@ class ConnectionManager {
|
|||||||
connecting = Completer();
|
connecting = Completer();
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
Logger.d("Socket connecting...");
|
Logger.d("Socket connecting...");
|
||||||
_socket = IOWebSocketChannel.connect(
|
try {
|
||||||
|
_socket = IOWebSocketChannel.connect(
|
||||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||||
_socketSubscription = _socket.stream.listen(
|
_socketSubscription = _socket.stream.listen(
|
||||||
(message) {
|
(message) {
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
var data = json.decode(message);
|
var data = json.decode(message);
|
||||||
if (data["type"] == "auth_required") {
|
if (data["type"] == "auth_required") {
|
||||||
Logger.d("[Received] <== ${data.toString()}");
|
Logger.d("[Received] <== ${data.toString()}");
|
||||||
_authenticate().then((_) {
|
_authenticate().then((_) {
|
||||||
Logger.d('Authentication complete');
|
Logger.d('Authentication complete');
|
||||||
connecting.complete();
|
connecting.complete();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
if (!connecting.isCompleted) connecting.completeError(e);
|
if (!connecting.isCompleted) connecting.completeError(e);
|
||||||
});
|
});
|
||||||
} else if (data["type"] == "auth_ok") {
|
} else if (data["type"] == "auth_ok") {
|
||||||
Logger.d("[Received] <== ${data.toString()}");
|
Logger.d("[Received] <== ${data.toString()}");
|
||||||
_messageResolver["auth"]?.complete();
|
Logger.d("[Connection] Subscribing to events");
|
||||||
_messageResolver.remove("auth");
|
sendSocketMessage(
|
||||||
if (_token != null) {
|
type: "subscribe_events",
|
||||||
if (!connecting.isCompleted) connecting.complete();
|
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.remove("auth");
|
||||||
|
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||||
|
} else {
|
||||||
|
_handleMessage(data);
|
||||||
}
|
}
|
||||||
} else if (data["type"] == "auth_invalid") {
|
},
|
||||||
Logger.d("[Received] <== ${data.toString()}");
|
cancelOnError: true,
|
||||||
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
onDone: () => _handleSocketClose(connecting),
|
||||||
_messageResolver.remove("auth");
|
onError: (e) => _handleSocketError(e, connecting)
|
||||||
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
);
|
||||||
} else {
|
} catch(exeption) {
|
||||||
_handleMessage(data);
|
connecting.completeError(HAError("${exeption.toString()}"));
|
||||||
}
|
}
|
||||||
},
|
|
||||||
cancelOnError: true,
|
|
||||||
onDone: () => _handleSocketClose(connecting),
|
|
||||||
onError: (e) => _handleSocketError(e, connecting)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
return connecting.future;
|
return connecting.future;
|
||||||
}
|
}
|
||||||
@ -194,6 +214,17 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
_messageResolver.remove("${data["id"]}");
|
_messageResolver.remove("${data["id"]}");
|
||||||
} else if (data["type"] == "event") {
|
} else if (data["type"] == "event") {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||||
onStateChangeCallback(data["event"]["data"]);
|
onStateChangeCallback(data["event"]["data"]);
|
||||||
@ -209,38 +240,24 @@ class ConnectionManager {
|
|||||||
|
|
||||||
void _handleSocketClose(Completer connectionCompleter) {
|
void _handleSocketClose(Completer connectionCompleter) {
|
||||||
Logger.d("Socket disconnected.");
|
Logger.d("Socket disconnected.");
|
||||||
if (!connectionCompleter.isCompleted) {
|
_disconnect().then((_) {
|
||||||
isConnected = false;
|
if (!connectionCompleter.isCompleted) {
|
||||||
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
isConnected = false;
|
||||||
} else {
|
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||||
_disconnect().then((_) {
|
}
|
||||||
Timer(Duration(seconds: 5), () {
|
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||||
Logger.d("Trying to reconnect...");
|
});
|
||||||
_connect().catchError((e) {
|
|
||||||
isConnected = false;
|
|
||||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSocketError(e, Completer connectionCompleter) {
|
void _handleSocketError(e, Completer connectionCompleter) {
|
||||||
Logger.e("Socket stream Error: $e");
|
Logger.e("Socket stream Error: $e");
|
||||||
if (!connectionCompleter.isCompleted) {
|
_disconnect().then((_) {
|
||||||
isConnected = false;
|
if (!connectionCompleter.isCompleted) {
|
||||||
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
|
isConnected = false;
|
||||||
} else {
|
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||||
_disconnect().then((_) {
|
}
|
||||||
Timer(Duration(seconds: 5), () {
|
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||||
Logger.d("Trying to reconnect...");
|
});
|
||||||
_connect().catchError((e) {
|
|
||||||
isConnected = false;
|
|
||||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _authenticate() {
|
Future _authenticate() {
|
||||||
@ -329,13 +346,13 @@ class ConnectionManager {
|
|||||||
_messageResolver[callbackName] = _completer;
|
_messageResolver[callbackName] = _completer;
|
||||||
String rawMessage = json.encode(dataObject);
|
String rawMessage = json.encode(dataObject);
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
_connect().timeout(connectTimeout, onTimeout: (){
|
_connect().timeout(connectTimeout).then((_) {
|
||||||
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
|
||||||
}).then((_) {
|
|
||||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||||
_socket.sink.add(rawMessage);
|
_socket.sink.add(rawMessage);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_completer.completeError(e);
|
if (!_completer.isCompleted) {
|
||||||
|
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||||
@ -348,7 +365,7 @@ class ConnectionManager {
|
|||||||
_currentMessageId += 1;
|
_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));
|
eventBus.fire(NotifyServiceCallEvent(domain, service, entityId));
|
||||||
Logger.d("Service call: $domain.$service, $entityId, $data");
|
Logger.d("Service call: $domain.$service, $entityId, $data");
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
@ -363,12 +380,12 @@ class ConnectionManager {
|
|||||||
sendHTTPPost(
|
sendHTTPPost(
|
||||||
endPoint: "/api/services/$domain/$service",
|
endPoint: "/api/services/$domain/$service",
|
||||||
data: json.encode(serviceData)
|
data: json.encode(serviceData)
|
||||||
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));
|
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
|
||||||
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
|
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
|
||||||
else
|
else
|
||||||
sendHTTPPost(
|
sendHTTPPost(
|
||||||
endPoint: "/api/services/$domain/$service"
|
endPoint: "/api/services/$domain/$service"
|
||||||
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));
|
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
|
||||||
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
|
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
@ -414,7 +431,7 @@ class ConnectionManager {
|
|||||||
completer.complete(response.body);
|
completer.complete(response.body);
|
||||||
} else {
|
} else {
|
||||||
Logger.d("[Received] <== HTTP ${response.statusCode}: ${response.body}");
|
Logger.d("[Received] <== HTTP ${response.statusCode}: ${response.body}");
|
||||||
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
|
completer.completeError(response);
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
completer.completeError(e);
|
completer.completeError(e);
|
||||||
|
@ -82,8 +82,8 @@ class LocationManager {
|
|||||||
int delay = i*delayFactor;
|
int delay = i*delayFactor;
|
||||||
Logger.d("Scheduling location update task #$i for every ${interval.inMinutes} minutes in $delay minutes...");
|
Logger.d("Scheduling location update task #$i for every ${interval.inMinutes} minutes in $delay minutes...");
|
||||||
await workManager.Workmanager.registerPeriodicTask(
|
await workManager.Workmanager.registerPeriodicTask(
|
||||||
"$backgroundTaskId$n",
|
"$backgroundTaskId$i",
|
||||||
"haClientLocationTracking-0$n",
|
"haClientLocationTracking-0$i",
|
||||||
tag: backgroundTaskTag,
|
tag: backgroundTaskTag,
|
||||||
inputData: {
|
inputData: {
|
||||||
"webhookId": webhookId,
|
"webhookId": webhookId,
|
||||||
@ -95,8 +95,8 @@ class LocationManager {
|
|||||||
backoffPolicy: workManager.BackoffPolicy.linear,
|
backoffPolicy: workManager.BackoffPolicy.linear,
|
||||||
backoffPolicyDelay: interval,
|
backoffPolicyDelay: interval,
|
||||||
constraints: workManager.Constraints(
|
constraints: workManager.Constraints(
|
||||||
networkType: workManager.NetworkType.connected
|
networkType: workManager.NetworkType.connected,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,7 +104,7 @@ class LocationManager {
|
|||||||
|
|
||||||
_stopLocationService() async {
|
_stopLocationService() async {
|
||||||
Logger.d("Canceling previous schedule if any...");
|
Logger.d("Canceling previous schedule if any...");
|
||||||
await workManager.Workmanager.cancelByTag(backgroundTaskTag);
|
await workManager.Workmanager.cancelAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDeviceLocation() async {
|
updateDeviceLocation() async {
|
||||||
@ -148,52 +148,90 @@ class LocationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateDeviceLocationIsolate() {
|
void updateDeviceLocationIsolate() {
|
||||||
workManager.Workmanager.executeTask((backgroundTask, data) {
|
workManager.Workmanager.executeTask((backgroundTask, data) async {
|
||||||
print("[Background $backgroundTask] Started");
|
//print("[Background $backgroundTask] Started");
|
||||||
final SentryClient sentryBackgroundClient = SentryClient(dsn: "https://5c868e5ef26947e2b61b189e391ec31b@sentry.io/1836366");
|
|
||||||
Geolocator geolocator = Geolocator();
|
Geolocator geolocator = Geolocator();
|
||||||
var battery = Battery();
|
var battery = Battery();
|
||||||
int batteryLevel = 100;
|
|
||||||
String webhookId = data["webhookId"];
|
String webhookId = data["webhookId"];
|
||||||
String httpWebHost = data["httpWebHost"];
|
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) {
|
if (webhookId != null && webhookId.isNotEmpty) {
|
||||||
print("[Background $backgroundTask] hour=$battery");
|
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
Map<String, String> headers = {};
|
||||||
Map<String, String> headers = {};
|
headers["Content-Type"] = "application/json";
|
||||||
headers["Content-Type"] = "application/json";
|
Map data = {
|
||||||
Map data = {
|
"type": "update_location",
|
||||||
"type": "update_location",
|
"data": {
|
||||||
"data": {
|
"gps": [],
|
||||||
"gps": [],
|
"gps_accuracy": 0,
|
||||||
"gps_accuracy": 0,
|
"battery": 100
|
||||||
"battery": batteryLevel
|
}
|
||||||
}
|
};
|
||||||
};
|
//print("[Background $backgroundTask] Getting battery level...");
|
||||||
print("[Background $backgroundTask] Getting battery level...");
|
int batteryLevel;
|
||||||
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){
|
try {
|
||||||
print("[Background $backgroundTask] Getting device location...");
|
batteryLevel = await battery.batteryLevel;
|
||||||
geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways).then((location) {
|
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
|
||||||
if (location != null) {
|
} catch(e) {
|
||||||
print("[Background $backgroundTask] Got location: ${location.latitude} ${location.longitude}");
|
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
|
||||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
batteryLevel = 0;
|
||||||
data["data"]["gps_accuracy"] = location.accuracy;
|
//logData += 'Battery: error, $e';
|
||||||
print("[Background $backgroundTask] Sending data home.");
|
}
|
||||||
http.post(
|
if (batteryLevel != null) {
|
||||||
url,
|
data["data"]["battery"] = batteryLevel;
|
||||||
headers: headers,
|
//logData += 'Battery: success, $batteryLevel';
|
||||||
body: json.encode(data)
|
}/* else {
|
||||||
);
|
logData += 'Battery: error, level is null';
|
||||||
} else {
|
}*/
|
||||||
throw "Can't get device location. Location is null";
|
Position location;
|
||||||
}
|
try {
|
||||||
}).catchError((e) {
|
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
|
||||||
sentryBackgroundClient.captureException(
|
if (location != null && location.latitude != null) {
|
||||||
exception: "${e.toString()}"
|
//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;
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -81,7 +81,7 @@ class MobileAppIntegrationManager {
|
|||||||
}
|
}
|
||||||
completer.complete();
|
completer.complete();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
if (e['code'] != null && e['code'] == 410) {
|
if (e is http.Response && e.statusCode == 410) {
|
||||||
Logger.e("MobileApp integration was removed");
|
Logger.e("MobileApp integration was removed");
|
||||||
_askToRegisterApp();
|
_askToRegisterApp();
|
||||||
} else {
|
} else {
|
||||||
|
@ -9,12 +9,12 @@ class StartupUserMessagesManager {
|
|||||||
return _instance;
|
return _instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
StartupUserMessagesManager._internal() {}
|
StartupUserMessagesManager._internal();
|
||||||
|
|
||||||
bool _supportAppDevelopmentMessageShown;
|
bool _supportAppDevelopmentMessageShown;
|
||||||
bool _whatsNewMessageShown;
|
bool _whatsNewMessageShown;
|
||||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||||
static final _whatsNewMessageKey = "user-message-shown-whats-new-706";
|
static final _whatsNewMessageKey = "user-message-shown-whats-new-884";
|
||||||
|
|
||||||
void checkMessagesToShow() async {
|
void checkMessagesToShow() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
18
lib/pages/fullscreen.page.dart
Normal file
18
lib/pages/fullscreen.page.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class FullScreenPage extends StatelessWidget {
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const FullScreenPage({Key key, this.child}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.black,
|
||||||
|
child: Center(
|
||||||
|
child: this.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -118,7 +118,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#location-tracking"),
|
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Please read documentation!",
|
"Please read documentation!",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class MainPage extends StatefulWidget {
|
class MainPage extends StatefulWidget {
|
||||||
MainPage({Key key, this.title}) : super(key: key);
|
MainPage({Key key, this.title}) : super(key: key);
|
||||||
@ -9,10 +9,10 @@ class MainPage extends StatefulWidget {
|
|||||||
_MainPageState createState() => new _MainPageState();
|
_MainPageState createState() => new _MainPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||||
|
|
||||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
|
||||||
StreamSubscription _stateSubscription;
|
StreamSubscription _stateSubscription;
|
||||||
|
StreamSubscription _lovelaceSubscription;
|
||||||
StreamSubscription _settingsSubscription;
|
StreamSubscription _settingsSubscription;
|
||||||
StreamSubscription _serviceCallSubscription;
|
StreamSubscription _serviceCallSubscription;
|
||||||
StreamSubscription _showEntityPageSubscription;
|
StreamSubscription _showEntityPageSubscription;
|
||||||
@ -25,18 +25,11 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
int _previousViewCount;
|
int _previousViewCount;
|
||||||
bool _showLoginButton = false;
|
bool _showLoginButton = false;
|
||||||
bool _preventAppRefresh = false;
|
bool _preventAppRefresh = false;
|
||||||
String _savedSharedText;
|
|
||||||
Entity _entityToShow;
|
Entity _entityToShow;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
final Stream purchaseUpdates =
|
|
||||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
|
||||||
_subscription = purchaseUpdates.listen((purchases) {
|
|
||||||
_handlePurchaseUpdates(purchases);
|
|
||||||
});
|
|
||||||
super.initState();
|
super.initState();
|
||||||
enableShareReceiving();
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
_firebaseMessaging.configure(
|
_firebaseMessaging.configure(
|
||||||
@ -77,12 +70,6 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
_fullLoad();
|
_fullLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override void receiveShare(Share shared) {
|
|
||||||
if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) {
|
|
||||||
_savedSharedText = shared.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future onSelectNotification(String payload) async {
|
Future onSelectNotification(String payload) async {
|
||||||
if (payload != null) {
|
if (payload != null) {
|
||||||
Logger.d('Notification clicked: ' + payload);
|
Logger.d('Notification clicked: ' + payload);
|
||||||
@ -104,43 +91,40 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fullLoad() async {
|
void _fullLoad() {
|
||||||
_showInfoBottomBar(progress: true,);
|
_showInfoBottomBar(progress: true,);
|
||||||
_subscribe().then((_) {
|
_subscribe().then((_) {
|
||||||
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||||
_fetchData();
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
LocationManager();
|
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||||
StartupUserMessagesManager().checkMessagesToShow();
|
_fetchData(useCache: true);
|
||||||
|
LocationManager();
|
||||||
|
StartupUserMessagesManager().checkMessagesToShow();
|
||||||
|
});
|
||||||
}, onError: (e) {
|
}, onError: (e) {
|
||||||
_setErrorState(e);
|
_setErrorState(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _quickLoad() {
|
void _quickLoad({bool uiOnly: false}) {
|
||||||
_hideBottomBar();
|
_hideBottomBar();
|
||||||
_showInfoBottomBar(progress: true,);
|
_showInfoBottomBar(progress: true,);
|
||||||
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
||||||
_fetchData();
|
_fetchData(useCache: false, uiOnly: uiOnly);
|
||||||
}, onError: (e) {
|
}, onError: (e) {
|
||||||
_setErrorState(e);
|
_setErrorState(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_fetchData() async {
|
_fetchData({useCache: false, uiOnly: false}) async {
|
||||||
if (_savedSharedText != null && !HomeAssistant().isNoEntities) {
|
if (useCache && !uiOnly) {
|
||||||
Logger.d("Got shared text: $_savedSharedText");
|
HomeAssistant().fetchDataFromCache().then((_) {
|
||||||
Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText});
|
setState((){});
|
||||||
_savedSharedText = null;
|
});
|
||||||
}
|
}
|
||||||
await HomeAssistant().fetchData().then((_) {
|
await HomeAssistant().fetchData(uiOnly).then((_) {
|
||||||
_hideBottomBar();
|
_hideBottomBar();
|
||||||
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
|
||||||
if (_previousViewCount != currentViewCount) {
|
|
||||||
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
|
||||||
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
|
||||||
_previousViewCount = currentViewCount;
|
|
||||||
}
|
|
||||||
if (_entityToShow != null) {
|
if (_entityToShow != null) {
|
||||||
_entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
|
_entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
|
||||||
}
|
}
|
||||||
@ -159,40 +143,32 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
Logger.d("$state");
|
Logger.d("$state");
|
||||||
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||||
_quickLoad();
|
_quickLoad();
|
||||||
}
|
} else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||||
}
|
HomeAssistant().saveCache();
|
||||||
|
|
||||||
void _handlePurchaseUpdates(purchase) {
|
|
||||||
if (purchase is List<PurchaseDetails>) {
|
|
||||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
|
||||||
eventBus.fire(ShowPopupMessageEvent(
|
|
||||||
title: "Thanks a lot!",
|
|
||||||
body: "Thank you for supporting HA Client development!",
|
|
||||||
buttonText: "Ok"
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _subscribe() {
|
Future _subscribe() {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
|
|
||||||
if (_stateSubscription == null) {
|
if (_stateSubscription == null) {
|
||||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
if (event.needToRebuildUI) {
|
if (event.needToRebuildUI) {
|
||||||
Logger.d("New entity. Need to rebuild UI");
|
Logger.d("Need to rebuild UI");
|
||||||
_quickLoad();
|
_quickLoad();
|
||||||
} else {
|
} else {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (_lovelaceSubscription == null) {
|
||||||
|
_lovelaceSubscription = eventBus.on<LovelaceChangedEvent>().listen((event) {
|
||||||
|
_quickLoad();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (_reloadUISubscription == null) {
|
if (_reloadUISubscription == null) {
|
||||||
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
||||||
_quickLoad();
|
_quickLoad(uiOnly: true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (_showPopupDialogSubscription == null) {
|
if (_showPopupDialogSubscription == null) {
|
||||||
@ -254,6 +230,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
_showOAuth();
|
_showOAuth();
|
||||||
} else {
|
} else {
|
||||||
_preventAppRefresh = false;
|
_preventAppRefresh = false;
|
||||||
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -267,9 +244,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
|
|
||||||
void _showOAuth() {
|
void _showOAuth() {
|
||||||
_preventAppRefresh = true;
|
_preventAppRefresh = true;
|
||||||
Launcher.launchURLInCustomTab(
|
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
|
||||||
url: ConnectionManager().oauthUrl
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setErrorState(HAError e) {
|
_setErrorState(HAError e) {
|
||||||
@ -319,7 +294,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _notifyServiceCalled(String domain, String service, String entityId) {
|
void _notifyServiceCalled(String domain, String service, entityId) {
|
||||||
_showInfoBottomBar(
|
_showInfoBottomBar(
|
||||||
message: "Calling $domain.$service",
|
message: "Calling $domain.$service",
|
||||||
duration: Duration(seconds: 4)
|
duration: Duration(seconds: 4)
|
||||||
@ -328,7 +303,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
|
|
||||||
void _showEntityPage(String entityId) {
|
void _showEntityPage(String entityId) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_entityToShow = HomeAssistant().entities.get(entityId);
|
_entityToShow = HomeAssistant().entities?.get(entityId);
|
||||||
if (_entityToShow != null) {
|
if (_entityToShow != null) {
|
||||||
_mainScrollController?.jumpTo(0);
|
_mainScrollController?.jumpTo(0);
|
||||||
}
|
}
|
||||||
@ -392,7 +367,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("${panel.title}"),
|
Text("${panel.title}"),
|
||||||
Container(width: 4.0,),
|
Container(width: 4.0,),
|
||||||
panel.isWebView ? Text("WEB", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -456,26 +431,33 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
title: Text("Help"),
|
title: Text("Help"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURL("http://ha-client.homemade.systems/docs");
|
Launcher.launchURL("http://ha-client.app/docs");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:forum")),
|
||||||
title: Text("Join Discord channel"),
|
title: Text("Contacts/Discussion"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURL("https://discord.gg/AUzEvwn");
|
Launcher.launchURL("https://spectrum.chat/ha-client");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
new ListTile(
|
||||||
|
title: Text("What's new?"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pushNamed('/whats-new');
|
||||||
|
}
|
||||||
|
),
|
||||||
new AboutListTile(
|
new AboutListTile(
|
||||||
aboutBoxChildren: <Widget>[
|
aboutBoxChildren: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURL("http://ha-client.homemade.systems/");
|
Launcher.launchURL("http://ha-client.app/");
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"ha-client.homemade.systems",
|
"ha-client.app",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline
|
||||||
@ -488,7 +470,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
|
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/terms_and_conditions");
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"Terms and Conditions",
|
"Terms and Conditions",
|
||||||
@ -504,7 +486,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
|
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/privacy_policy");
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"Privacy Policy",
|
"Privacy Policy",
|
||||||
@ -639,6 +621,13 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
List<PopupMenuItem<String>> serviceMenuItems = [];
|
List<PopupMenuItem<String>> serviceMenuItems = [];
|
||||||
List<PopupMenuItem<String>> mediaMenuItems = [];
|
List<PopupMenuItem<String>> mediaMenuItems = [];
|
||||||
|
|
||||||
|
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||||
|
if (_previousViewCount != currentViewCount) {
|
||||||
|
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
||||||
|
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
||||||
|
_previousViewCount = currentViewCount;
|
||||||
|
}
|
||||||
|
|
||||||
serviceMenuItems.add(PopupMenuItem<String>(
|
serviceMenuItems.add(PopupMenuItem<String>(
|
||||||
child: new Text("Reload"),
|
child: new Text("Reload"),
|
||||||
value: "reload",
|
value: "reload",
|
||||||
@ -654,7 +643,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
Widget mediaMenuIcon;
|
Widget mediaMenuIcon;
|
||||||
int playersCount = 0;
|
int playersCount = 0;
|
||||||
if (!empty && !HomeAssistant().entities.isEmpty) {
|
if (!empty && !HomeAssistant().entities.isEmpty) {
|
||||||
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(domains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
||||||
playersCount = activePlayers.length;
|
playersCount = activePlayers.length;
|
||||||
mediaMenuItems.addAll(
|
mediaMenuItems.addAll(
|
||||||
activePlayers.map((entity) => PopupMenuItem<String>(
|
activePlayers.map((entity) => PopupMenuItem<String>(
|
||||||
@ -732,7 +721,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: HomeAssistant().buildViews(context, _viewsTabController),
|
child: HomeAssistant().ui.build(context, _viewsTabController),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
width: Sizes.mainPageScreenSeparatorWidth,
|
width: Sizes.mainPageScreenSeparatorWidth,
|
||||||
@ -747,7 +736,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
} else if (_entityToShow != null) {
|
} else if (_entityToShow != null) {
|
||||||
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
|
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
|
||||||
} else {
|
} else {
|
||||||
mainScrollBody = HomeAssistant().buildViews(context, _viewsTabController);
|
mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -785,7 +774,9 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
context: context,
|
context: context,
|
||||||
items: serviceMenuItems
|
items: serviceMenuItems
|
||||||
).then((String val) {
|
).then((String val) {
|
||||||
|
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
|
||||||
if (val == "reload") {
|
if (val == "reload") {
|
||||||
|
|
||||||
_quickLoad();
|
_quickLoad();
|
||||||
} else if (val == "logout") {
|
} else if (val == "logout") {
|
||||||
HomeAssistant().logout().then((_) {
|
HomeAssistant().logout().then((_) {
|
||||||
@ -868,41 +859,43 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This method is rerun every time setState is called.
|
|
||||||
if (HomeAssistant().isNoViews) {
|
if (HomeAssistant().isNoViews) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
primary: false,
|
primary: false,
|
||||||
drawer: _buildAppDrawer(),
|
drawer: _buildAppDrawer(),
|
||||||
bottomNavigationBar: bottomBar,
|
bottomNavigationBar: bottomBar,
|
||||||
body: _buildScaffoldBody(true)
|
body: _buildScaffoldBody(true)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
drawer: _buildAppDrawer(),
|
drawer: _buildAppDrawer(),
|
||||||
primary: false,
|
primary: false,
|
||||||
bottomNavigationBar: bottomBar,
|
bottomNavigationBar: bottomBar,
|
||||||
body: _buildScaffoldBody(false)
|
body: _buildScaffoldBody(false)
|
||||||
),
|
),
|
||||||
onWillPop: () {
|
onWillPop: () {
|
||||||
if (_entityToShow != null) {
|
if (_entityToShow != null) {
|
||||||
eventBus.fire(ShowEntityPageEvent());
|
eventBus.fire(ShowEntityPageEvent());
|
||||||
return Future.value(false);
|
return Future.value(false);
|
||||||
} else {
|
} else {
|
||||||
return Future.value(true);
|
return Future.value(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||||
|
//flutterWebviewPlugin.dispose();
|
||||||
_viewsTabController?.dispose();
|
_viewsTabController?.dispose();
|
||||||
_stateSubscription?.cancel();
|
_stateSubscription?.cancel();
|
||||||
|
_lovelaceSubscription?.cancel();
|
||||||
_settingsSubscription?.cancel();
|
_settingsSubscription?.cancel();
|
||||||
_serviceCallSubscription?.cancel();
|
_serviceCallSubscription?.cancel();
|
||||||
_showPopupDialogSubscription?.cancel();
|
_showPopupDialogSubscription?.cancel();
|
||||||
@ -910,7 +903,6 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
_showEntityPageSubscription?.cancel();
|
_showEntityPageSubscription?.cancel();
|
||||||
_showErrorSubscription?.cancel();
|
_showErrorSubscription?.cancel();
|
||||||
_startAuthSubscription?.cancel();
|
_startAuthSubscription?.cancel();
|
||||||
_subscription?.cancel();
|
|
||||||
_showPageSubscription?.cancel();
|
_showPageSubscription?.cancel();
|
||||||
_reloadUISubscription?.cancel();
|
_reloadUISubscription?.cancel();
|
||||||
//TODO disconnect
|
//TODO disconnect
|
@ -57,9 +57,9 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
_loaded = false;
|
_loaded = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
|
_isMediaExtractorExist = HomeAssistant().isServiceExist("media_extractor");
|
||||||
//_useMediaExtractor = _isMediaExtractorExist;
|
//_useMediaExtractor = _isMediaExtractorExist;
|
||||||
_players = HomeAssistant().entities.getByDomains(domains: ["media_player"]);
|
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_players.isNotEmpty) {
|
if (_players.isNotEmpty) {
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
|
@ -75,10 +75,16 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
|
|
||||||
_saveSettings() async {
|
_saveSettings() async {
|
||||||
_newHassioDomain = _newHassioDomain.trim();
|
_newHassioDomain = _newHassioDomain.trim();
|
||||||
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
|
||||||
|
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
|
||||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||||
}
|
}
|
||||||
_newHassioDomain = _newHassioDomain.split("/")[0];
|
_newHassioDomain = _newHassioDomain.split("/")[0];
|
||||||
|
if (_newHassioDomain.contains(":")) {
|
||||||
|
List<String> domainAndPort = _newHassioDomain.split(":");
|
||||||
|
_newHassioDomain = domainAndPort[0];
|
||||||
|
_newHassioPort = domainAndPort[1];
|
||||||
|
}
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
final storage = new FlutterSecureStorage();
|
final storage = new FlutterSecureStorage();
|
||||||
if (_newLongLivedToken.isNotEmpty) {
|
if (_newLongLivedToken.isNotEmpty) {
|
||||||
|
@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
|
|||||||
error = "";
|
error = "";
|
||||||
});
|
});
|
||||||
http.Response response;
|
http.Response response;
|
||||||
response = await http.get("http://ha-client.homemade.systems/service/whats_new_0.7.0.md");
|
response = await http.get("http://ha-client.app/service/whats_new_0.8.md");
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
setState(() {
|
setState(() {
|
||||||
data = response.body;
|
data = response.body;
|
||||||
|
90
lib/pages/zha_page.dart
Normal file
90
lib/pages/zha_page.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ZhaPage extends StatefulWidget {
|
||||||
|
ZhaPage({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ZhaPageState createState() => new _ZhaPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ZhaPageState extends State<ZhaPage> {
|
||||||
|
|
||||||
|
List data = [];
|
||||||
|
String error = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadData() async {
|
||||||
|
setState(() {
|
||||||
|
data.clear();
|
||||||
|
error = "";
|
||||||
|
});
|
||||||
|
ConnectionManager().sendSocketMessage(
|
||||||
|
type: 'zha_map/devices'
|
||||||
|
).then((response){
|
||||||
|
setState(() {
|
||||||
|
data = response['devices'];
|
||||||
|
});
|
||||||
|
}).catchError((e){
|
||||||
|
setState(() {
|
||||||
|
error = '$e';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget body;
|
||||||
|
if (error.isNotEmpty) {
|
||||||
|
body = PageLoadingError(errorText: error,);
|
||||||
|
} else if (data.isEmpty) {
|
||||||
|
body = PageLoadingIndicator();
|
||||||
|
} else {
|
||||||
|
List<Widget> devicesListWindet = [];
|
||||||
|
data.forEach((device) {
|
||||||
|
devicesListWindet.add(
|
||||||
|
Card(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
CardHeader(
|
||||||
|
name: '${device['ieee']}',
|
||||||
|
subtitle: Text('${device['manufacturer']}'),
|
||||||
|
),
|
||||||
|
Text('${device['device_type']}'),
|
||||||
|
Text('model: ${device['model']}'),
|
||||||
|
Text('offline: ${device['offline']}'),
|
||||||
|
Text('neighbours: ${device['neighbours'].length}'),
|
||||||
|
Text('raw: $device'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
body = ListView(
|
||||||
|
children: devicesListWindet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
onPressed: () => _loadData(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
// Here we take the value from the MyHomePage object that was created by
|
||||||
|
// the App.build method, and use it to set our appbar title.
|
||||||
|
title: new Text('ZHA'),
|
||||||
|
),
|
||||||
|
body: body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ class Panel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final String type;
|
final String componentName;
|
||||||
final String title;
|
final String title;
|
||||||
final String urlPath;
|
final String urlPath;
|
||||||
final Map config;
|
final Map config;
|
||||||
@ -19,34 +19,61 @@ class Panel {
|
|||||||
bool isHidden = true;
|
bool isHidden = true;
|
||||||
bool isWebView = false;
|
bool isWebView = false;
|
||||||
|
|
||||||
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
Panel({this.id, this.componentName, this.title, this.urlPath, this.icon, this.config}) {
|
||||||
if (icon == null || !icon.startsWith("mdi:")) {
|
if (icon == null || !icon.startsWith("mdi:")) {
|
||||||
icon = Panel.iconsByComponent[type];
|
icon = Panel.iconsByComponent[componentName];
|
||||||
}
|
}
|
||||||
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
|
isHidden = (componentName == 'kiosk' || componentName == 'states' || componentName == 'profile' || componentName == 'developer-tools');
|
||||||
isWebView = (type != 'config');
|
isWebView = (componentName != 'config' && componentName != 'lovelace' && !componentName.startsWith('haclient'));
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleOpen(BuildContext context) {
|
void handleOpen(BuildContext context) {
|
||||||
if (type == "config") {
|
if (componentName == "config") {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PanelPage(title: "$title", panel: this),
|
builder: (context) => PanelPage(title: "$title", panel: this),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else if (componentName.startsWith('haclient')) {
|
||||||
|
Navigator.of(context).pushNamed(urlPath);
|
||||||
|
} else if (componentName == 'lovelace') {
|
||||||
|
HomeAssistant().lovelaceDashboardUrl = this.urlPath;
|
||||||
|
HomeAssistant().autoUi = false;
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.setString('lovelace_dashboard_url', this.urlPath);
|
||||||
|
eventBus.fire(ReloadUIEvent());
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
Launcher.launchURLInCustomTab(url: "${ConnectionManager().httpWebHost}/$urlPath");
|
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget getMenuItemWidget(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(this.icon)),
|
||||||
|
title: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("${this.title}"),
|
||||||
|
Container(width: 4.0,),
|
||||||
|
isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
this.handleOpen(context);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget getWidget() {
|
Widget getWidget() {
|
||||||
switch (type) {
|
switch (componentName) {
|
||||||
case "config": {
|
case "config": {
|
||||||
return ConfigPanelWidget();
|
return ConfigPanelWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return Text("Unsupported panel component: $type");
|
return Text("Unsupported panel component: $componentName");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ class LinkToWebConfig extends StatelessWidget {
|
|||||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
||||||
subtitle: Text("Tap to open web version"),
|
subtitle: Text("Tap to open web version"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Launcher.launchURLInCustomTab(url: this.url);
|
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -12,6 +12,8 @@ class StateChangedEvent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LovelaceChangedEvent {}
|
||||||
|
|
||||||
class SettingsChangedEvent {
|
class SettingsChangedEvent {
|
||||||
bool reconnect;
|
bool reconnect;
|
||||||
|
|
||||||
@ -36,7 +38,7 @@ class StartAuthEvent {
|
|||||||
class NotifyServiceCallEvent {
|
class NotifyServiceCallEvent {
|
||||||
String domain;
|
String domain;
|
||||||
String service;
|
String service;
|
||||||
String entityId;
|
var entityId;
|
||||||
|
|
||||||
NotifyServiceCallEvent(this.domain, this.service, this.entityId);
|
NotifyServiceCallEvent(this.domain, this.service, this.entityId);
|
||||||
}
|
}
|
||||||
|
45
lib/ui.dart
45
lib/ui.dart
@ -6,8 +6,51 @@ class HomeAssistantUI {
|
|||||||
|
|
||||||
bool get isEmpty => views == null || views.isEmpty;
|
bool get isEmpty => views == null || views.isEmpty;
|
||||||
|
|
||||||
HomeAssistantUI() {
|
HomeAssistantUI({rawLovelaceConfig}) {
|
||||||
|
if (rawLovelaceConfig == null) {
|
||||||
|
rawLovelaceConfig = _generateLovelaceConfig();
|
||||||
|
}
|
||||||
views = [];
|
views = [];
|
||||||
|
Logger.d("--Title: ${rawLovelaceConfig["title"]}");
|
||||||
|
title = rawLovelaceConfig["title"];
|
||||||
|
int viewCounter = 0;
|
||||||
|
Logger.d("--Views count: ${rawLovelaceConfig['views'].length}");
|
||||||
|
rawLovelaceConfig["views"].forEach((rawView){
|
||||||
|
Logger.d("----view id: ${rawView['id']}");
|
||||||
|
HAView view = HAView(
|
||||||
|
count: viewCounter,
|
||||||
|
rawData: rawView
|
||||||
|
);
|
||||||
|
|
||||||
|
views.add(
|
||||||
|
view
|
||||||
|
);
|
||||||
|
viewCounter += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Map _generateLovelaceConfig() {
|
||||||
|
Map result = {};
|
||||||
|
result['title'] = 'Home';
|
||||||
|
result['views'] = [
|
||||||
|
{
|
||||||
|
'icon': 'mdi:home',
|
||||||
|
'badges': HomeAssistant().entities.getByDomains(
|
||||||
|
includeDomains: ['sensor', 'binary_sensor', 'device_tracker', 'person', 'sun']
|
||||||
|
).map(
|
||||||
|
(en) => en.entityId
|
||||||
|
).toList(),
|
||||||
|
'cards': [{
|
||||||
|
'type': 'entities',
|
||||||
|
'entities': HomeAssistant().entities.getByDomains(
|
||||||
|
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
|
||||||
|
).map(
|
||||||
|
(en) => en.entityId
|
||||||
|
).toList()
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget build(BuildContext context, TabController tabController) {
|
Widget build(BuildContext context, TabController tabController) {
|
||||||
|
@ -35,4 +35,28 @@ class Launcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void launchAuthenticatedWebView({BuildContext context, String url, String title}) {
|
||||||
|
if (url.contains("?")) {
|
||||||
|
url += "&external_auth=1";
|
||||||
|
} else {
|
||||||
|
url += "?external_auth=1";
|
||||||
|
}
|
||||||
|
final flutterWebViewPlugin = new standaloneWebview.FlutterWebviewPlugin();
|
||||||
|
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
|
||||||
|
if (viewState.type == standaloneWebview.WebViewState.startLoad) {
|
||||||
|
Logger.d("[WebView] Injecting external auth JS");
|
||||||
|
rootBundle.loadString('assets/js/externalAuth.js').then((js){
|
||||||
|
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
"/webview",
|
||||||
|
arguments: {
|
||||||
|
"url": "$url",
|
||||||
|
"title": "${title ?? ''}"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -4,77 +4,171 @@ class HAView {
|
|||||||
List<HACard> cards = [];
|
List<HACard> cards = [];
|
||||||
List<Entity> badges = [];
|
List<Entity> badges = [];
|
||||||
Entity linkedEntity;
|
Entity linkedEntity;
|
||||||
final String name;
|
String name;
|
||||||
final String id;
|
String id;
|
||||||
final String iconName;
|
String iconName;
|
||||||
final int count;
|
final int count;
|
||||||
final bool panel;
|
bool isPanel;
|
||||||
|
|
||||||
HAView({
|
HAView({@required this.count, @required rawData}) {
|
||||||
this.name,
|
id = "${rawData['id']}";
|
||||||
this.id,
|
name = rawData['title'];
|
||||||
this.count,
|
iconName = rawData['icon'];
|
||||||
this.iconName,
|
isPanel = rawData['panel'] ?? false;
|
||||||
this.panel: false,
|
|
||||||
List<Entity> childEntities
|
if (rawData['badges'] != null && rawData['badges'] is List) {
|
||||||
}) {
|
rawData['badges'].forEach((entity) {
|
||||||
if (childEntities != null) {
|
if (entity is String) {
|
||||||
_fillView(childEntities);
|
if (HomeAssistant().entities.isExist(entity)) {
|
||||||
}
|
Entity e = HomeAssistant().entities.get(entity);
|
||||||
|
badges.add(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String eId = '${entity['entity']}';
|
||||||
|
if (HomeAssistant().entities.isExist(eId)) {
|
||||||
|
Entity e = HomeAssistant().entities.get(eId);
|
||||||
|
badges.add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cards.addAll(_createLovelaceCards(rawData["cards"] ?? []));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fillView(List<Entity> childEntities) {
|
List<HACard> _createLovelaceCards(List rawCards) {
|
||||||
List<HACard> autoGeneratedCards = [];
|
List<HACard> result = [];
|
||||||
badges.addAll(childEntities.where((entity){ return entity.isBadge;}));
|
rawCards.forEach((rawCard){
|
||||||
childEntities.where((entity){ return entity.domain == "media_player";}).forEach((e){
|
try {
|
||||||
HACard card = HACard(
|
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||||
name: e.displayName,
|
var rawCardInfo = rawCard["card"] ?? rawCard;
|
||||||
id: e.entityId,
|
|
||||||
linkedEntityWrapper: EntityWrapper(entity: e),
|
|
||||||
type: CardType.MEDIA_CONTROL
|
|
||||||
);
|
|
||||||
cards.add(card);
|
|
||||||
});
|
|
||||||
childEntities.where((e){return (!e.isBadge && e.domain != "media_player");}).forEach((entity) {
|
|
||||||
if (!entity.isGroup) {
|
|
||||||
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
|
|
||||||
if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) {
|
|
||||||
HACard card = HACard(
|
|
||||||
id: groupIdToAdd,
|
|
||||||
name: entity.domain,
|
|
||||||
type: CardType.ENTITIES
|
|
||||||
);
|
|
||||||
card.entities.add(EntityWrapper(entity: entity));
|
|
||||||
autoGeneratedCards.add(card);
|
|
||||||
} else {
|
|
||||||
autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(EntityWrapper(entity: entity));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
HACard card = HACard(
|
HACard card = HACard(
|
||||||
name: entity.displayName,
|
id: "card",
|
||||||
id: entity.entityId,
|
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||||
type: CardType.ENTITIES
|
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||||
|
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
|
||||||
|
showHeaderToggle: (rawCardInfo['show_header_toggle'] ?? rawCard['show_header_toggle']) ?? 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']
|
||||||
);
|
);
|
||||||
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
|
if (rawCardInfo["cards"] != null) {
|
||||||
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
|
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||||
HACard mediaCard = HACard(
|
}
|
||||||
name: entity.displayName,
|
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
|
||||||
id: entity.entityId,
|
rawEntities?.forEach((rawEntity) {
|
||||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
if (rawEntity is String) {
|
||||||
type: CardType.MEDIA_CONTROL
|
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||||
);
|
card.entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||||
cards.add(mediaCard);
|
} 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 (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||||
|
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||||
|
card.entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
overrideName: rawEntity["name"],
|
||||||
|
overrideIcon: rawEntity["icon"],
|
||||||
|
stateFilter: rawEntity['state_filter'] ?? [],
|
||||||
|
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
cards.add(card);
|
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
|
||||||
|
if (rawSingleEntity != null) {
|
||||||
|
var en = rawSingleEntity;
|
||||||
|
if (en is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(en)) {
|
||||||
|
Entity e = HomeAssistant().entities.get(en);
|
||||||
|
card.linkedEntityWrapper = EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
overrideIcon: rawCardInfo["icon"],
|
||||||
|
overrideName: rawCardInfo["name"],
|
||||||
|
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (HomeAssistant().entities.isExist(en["entity"])) {
|
||||||
|
Entity e = HomeAssistant().entities.get(en["entity"]);
|
||||||
|
card.linkedEntityWrapper = EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
overrideIcon: en["icon"],
|
||||||
|
overrideName: en["name"],
|
||||||
|
stateFilter: en['state_filter'] ?? [],
|
||||||
|
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()}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cards.addAll(autoGeneratedCards);
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildTab() {
|
Widget buildTab() {
|
||||||
if (linkedEntity == null) {
|
if (linkedEntity == null) {
|
||||||
if (iconName != null) {
|
if (iconName != null && iconName.isNotEmpty) {
|
||||||
return
|
return
|
||||||
Tab(
|
Tab(
|
||||||
icon:
|
icon:
|
||||||
|
@ -10,22 +10,28 @@ class ViewWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (this.view.panel) {
|
if (this.view.isPanel) {
|
||||||
return FractionallySizedBox(
|
return FractionallySizedBox(
|
||||||
widthFactor: 1,
|
widthFactor: 1,
|
||||||
heightFactor: 1,
|
heightFactor: 1,
|
||||||
child: _buildPanelChild(context),
|
child: _buildPanelChild(context),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
Widget cardsContainer;
|
||||||
|
if (this.view.cards.isNotEmpty) {
|
||||||
|
cardsContainer = DynamicMultiColumnLayout(
|
||||||
|
minColumnWidth: Sizes.minViewColumnWidth,
|
||||||
|
children: this.view.cards.map((card) => card.build(context)).toList(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cardsContainer = Container();
|
||||||
|
}
|
||||||
return ListView(
|
return ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
padding: EdgeInsets.all(0),
|
padding: EdgeInsets.all(0),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildBadges(context),
|
_buildBadges(context),
|
||||||
DynamicMultiColumnLayout(
|
cardsContainer
|
||||||
minColumnWidth: Sizes.minViewColumnWidth,
|
|
||||||
children: this.view.cards.map((card) => card.build(context)).toList(),
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
46
pubspec.yaml
46
pubspec.yaml
@ -1,38 +1,39 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.7.5+750
|
version: 0.8.0+886
|
||||||
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
sdk: ">=2.2.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
web_socket_channel: any
|
web_socket_channel: ^1.1.0
|
||||||
shared_preferences: any
|
shared_preferences: ^0.5.6+1
|
||||||
progress_indicators: any
|
progress_indicators: ^0.1.4
|
||||||
event_bus: any
|
path_provider: ^1.6.5
|
||||||
cached_network_image: any
|
event_bus: ^1.1.1
|
||||||
url_launcher: any
|
cached_network_image: ^2.0.0
|
||||||
date_format: any
|
url_launcher: ^5.4.1
|
||||||
|
date_format: ^1.0.8
|
||||||
charts_flutter: ^0.8.1
|
charts_flutter: ^0.8.1
|
||||||
flutter_markdown: 0.3.0
|
flutter_markdown: ^0.3.3
|
||||||
in_app_purchase: ^0.2.1+4
|
in_app_purchase: ^0.3.0+3
|
||||||
flutter_custom_tabs: ^0.6.0
|
flutter_custom_tabs: ^0.6.0
|
||||||
firebase_messaging: ^5.1.6
|
flutter_webview_plugin: ^0.3.10+1
|
||||||
uni_links: ^0.2.0
|
webview_flutter: ^0.3.19+7
|
||||||
|
firebase_messaging: ^6.0.9
|
||||||
flutter_secure_storage: ^3.3.1+1
|
flutter_secure_storage: ^3.3.1+1
|
||||||
device_info: ^0.4.0+3
|
device_info: ^0.4.1+4
|
||||||
flutter_local_notifications: ^0.8.4
|
flutter_local_notifications: ^1.1.6
|
||||||
geolocator: ^5.1.5
|
geolocator: ^5.3.1
|
||||||
workmanager: ^0.1.3
|
workmanager: ^0.2.2
|
||||||
battery: ^0.3.1+1
|
battery: ^0.3.1+7
|
||||||
sentry: ^2.3.1
|
firebase_crashlytics: ^0.1.3+3
|
||||||
share:
|
video_player: ^0.10.7
|
||||||
git:
|
|
||||||
url: https://github.com/d-silveira/flutter-share.git
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -51,6 +52,7 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- images/hassio-192x192.png
|
- images/hassio-192x192.png
|
||||||
- assets/js/externalAuth.js
|
- assets/js/externalAuth.js
|
||||||
|
- assets/html/cameraView.html
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: "Material Design Icons"
|
- family: "Material Design Icons"
|
||||||
|
Reference in New Issue
Block a user