Compare commits
138 Commits
beta/0.7.4
...
beta/0.8.2
Author | SHA1 | Date | |
---|---|---|---|
89513ca4e5 | |||
a934ee2335 | |||
49aeea634f | |||
e18b9ebe14 | |||
08ee3f3d80 | |||
62d07bf8b9 | |||
ab398cbdc3 | |||
007d12719c | |||
524d195800 | |||
405de64249 | |||
f53554702e | |||
379e1a4a7e | |||
d6f7096055 | |||
37c721e4f6 | |||
d94235ef6d | |||
eb4184713f | |||
a0a0cb4612 | |||
f448a20784 | |||
36eff26862 | |||
5b2a1163b9 | |||
e627a8b963 | |||
4432124e8c | |||
b8ba3c59e9 | |||
c40a496b6b | |||
a7c3b46061 | |||
dfbaaeb06b | |||
f6ab20c6e8 | |||
7625099d74 | |||
32c8e76855 | |||
0aa2c974d5 | |||
9524c8587b | |||
c075db8b1a | |||
d0b7cc1929 | |||
d8df32f140 | |||
293b5e0242 | |||
2f517a3ad5 | |||
56d8e389db | |||
1377843350 | |||
8e31eaf8bb | |||
5ced01463f | |||
a3548455eb | |||
c40fceea4f | |||
6ad3938a91 | |||
bc642f81ad | |||
14ce608bbb | |||
c4c67747c5 | |||
5b3ceecb0e | |||
bf53e4b9df | |||
7e09d92fdf | |||
1ba9106d0b | |||
d727a29991 | |||
c5d617477f | |||
244a1984cc | |||
b00b745f27 | |||
959ff21b9b | |||
e6a7fd2dfe | |||
216276e5f3 | |||
3e6229cf3e | |||
fc4cb80b74 | |||
b907ff1e82 | |||
7536a52771 | |||
73a8c111d1 | |||
86a19eeec2 | |||
fba4459977 | |||
06f994a827 | |||
35d8607484 | |||
2f4c06e9b5 | |||
92e008a380 | |||
14c272af92 | |||
710de9f2b8 | |||
d9ad3b3083 | |||
b2686cb105 | |||
959e89de2b | |||
6e448d3458 | |||
6695756727 | |||
ed732e9b77 | |||
f495a6affc | |||
c8d7e1a95f | |||
e1ca2638e3 | |||
01226cb9eb | |||
8a80d0c5d1 | |||
f26f3e87c7 | |||
b750417415 | |||
2c35dd7c21 | |||
cff4a4feed | |||
62174b0651 | |||
d3ea4210c1 | |||
1c782bf64d | |||
bc96dab339 | |||
0f7179b944 | |||
1e3bfa8ff7 | |||
2bce86f905 | |||
0be00acc3a | |||
4e61adaeb1 | |||
49a8f08153 | |||
ce15658462 | |||
16d73ba7dd | |||
9f3e3c1917 | |||
f29e382a19 | |||
073562373a | |||
4298ebcd66 | |||
a121295bef | |||
9303e4c0a5 | |||
831fc98ab1 | |||
2003005e56 | |||
fda8fb7182 | |||
cf6039b279 | |||
41e552dce5 | |||
90043b5806 | |||
9eb74b5a8d | |||
9cc60a136b | |||
78eb1e779c | |||
8db2d8508e | |||
3f1ece26ec | |||
d1912a44c6 | |||
36a05eb390 | |||
4f39ea1ad8 | |||
a241cc1d61 | |||
8b4df98cb9 | |||
7d30c2f9d5 | |||
44acabadfe | |||
6f3a2bb78d | |||
dd5f8b155d | |||
cd81fc72fd | |||
890da650dc | |||
9897b6a44b | |||
7969f54d3b | |||
7c18454de3 | |||
dcf5efddd1 | |||
a6541134e0 | |||
90504047b4 | |||
ca1eec6602 | |||
edc01d14b7 | |||
6cb5463b13 | |||
63a789ebfb | |||
a0994e9a60 | |||
8d1b728194 | |||
1a9fec8b98 |
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
|
.secrets.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
|
|
@ -3,12 +3,12 @@ image:
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- before: |
|
- before: |
|
||||||
export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
||||||
mkdir -p /home/gitpod/.android
|
mkdir -p /home/gitpod/.android
|
||||||
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 [Discord](https://discord.gg/nd6FZQ) 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'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,3 +3,4 @@ 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'
|
61
assets/html/cameraLiveView.html
Normal file
61
assets/html/cameraLiveView.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
widows: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
var messageChannel = '{{message_channel}}';
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<video id="screen" width="100%" controls></video>
|
||||||
|
<script>
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
var video = document.getElementById('screen');
|
||||||
|
var hls = new Hls();
|
||||||
|
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||||
|
if (data.fatal) {
|
||||||
|
switch(data.type) {
|
||||||
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
// try to recover network error
|
||||||
|
console.log("fatal network error encountered, try to recover");
|
||||||
|
hls.startLoad();
|
||||||
|
break;
|
||||||
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
console.log("fatal media error encountered, try to recover");
|
||||||
|
hls.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// cannot recover
|
||||||
|
hls.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// bind them together
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
|
||||||
|
console.log("video and hls.js are now bound together !");
|
||||||
|
hls.loadSource("{{stream_url}}");
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
|
||||||
|
console.log("manifest loaded, found " + data.levels.length + " quality level");
|
||||||
|
video.play();
|
||||||
|
video.onloadedmetadata = function() {
|
||||||
|
window[messageChannel].postMessage(document.body.clientWidth / video.offsetHeight);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
28
assets/html/cameraView.html
Normal file
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>
|
@ -14,3 +14,22 @@ window.externalApp.getExternalAuth = function(options) {
|
|||||||
}, 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(
|
||||||
@ -172,9 +198,6 @@ class CardWidget extends StatelessWidget {
|
|||||||
body.add(CardHeader(
|
body.add(CardHeader(
|
||||||
name: card.name ?? "",
|
name: card.name ?? "",
|
||||||
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -280,21 +303,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 +335,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 +352,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>[
|
||||||
|
@ -18,7 +18,7 @@ class CardHeader extends StatelessWidget {
|
|||||||
title: Text("$name",
|
title: Text("$name",
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
style: Theme.of(context).textTheme.headline),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = new Container(width: 0.0, height: 0.0);
|
result = new Container(width: 0.0, height: 0.0);
|
||||||
|
@ -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,15 @@ class EntityButtonCardBody extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildName() {
|
Widget _buildName() {
|
||||||
|
if (showName) {
|
||||||
return EntityName(
|
return EntityName(
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||||
textOverflow: TextOverflow.ellipsis,
|
textOverflow: TextOverflow.ellipsis,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
wordsWrap: true,
|
wordsWrap: true,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center
|
||||||
fontSize: Sizes.nameFontSize,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return Container(width: 0, height: 0);
|
||||||
|
}
|
||||||
}
|
}
|
@ -14,10 +14,11 @@ class GaugeCardBody extends StatefulWidget {
|
|||||||
|
|
||||||
class _GaugeCardBodyState extends State<GaugeCardBody> {
|
class _GaugeCardBodyState extends State<GaugeCardBody> {
|
||||||
|
|
||||||
List<charts.Series> seriesList;
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
List<charts.Series<GaugeSegment, String>> _createData(double value) {
|
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
double fixedValue;
|
double fixedValue;
|
||||||
|
double value = entityWrapper.entity.doubleState;
|
||||||
if (value > widget.max) {
|
if (value > widget.max) {
|
||||||
fixedValue = widget.max.toDouble();
|
fixedValue = widget.max.toDouble();
|
||||||
} else if (value < widget.min) {
|
} else if (value < widget.min) {
|
||||||
@ -25,129 +26,151 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
|
|||||||
} else {
|
} else {
|
||||||
fixedValue = value;
|
fixedValue = value;
|
||||||
}
|
}
|
||||||
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
|
|
||||||
Color mainColor;
|
|
||||||
if (widget.severity != null) {
|
|
||||||
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
|
|
||||||
mainColor = Colors.red;
|
|
||||||
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
|
|
||||||
mainColor = Colors.amber;
|
|
||||||
} else {
|
|
||||||
mainColor = Colors.green;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mainColor = Colors.green;
|
|
||||||
}
|
|
||||||
final data = [
|
|
||||||
GaugeSegment('Main', toShow, mainColor),
|
|
||||||
GaugeSegment('Rest', 100 - toShow, Colors.black45),
|
|
||||||
];
|
|
||||||
|
|
||||||
return [
|
List<GaugeRange> ranges;
|
||||||
charts.Series<GaugeSegment, String>(
|
if (widget.severity != null && widget.severity["green"] is int && widget.severity["red"] is int && widget.severity["yellow"] is int) {
|
||||||
id: 'Segments',
|
List<RangeContainer> rangesList = <RangeContainer>[
|
||||||
domainFn: (GaugeSegment segment, _) => segment.segment,
|
RangeContainer(widget.severity["green"], HAClientTheme().getGreenGaugeColor()),
|
||||||
measureFn: (GaugeSegment segment, _) => segment.value,
|
RangeContainer(widget.severity["red"], HAClientTheme().getRedGaugeColor()),
|
||||||
colorFn: (GaugeSegment segment, _) => segment.color,
|
RangeContainer(widget.severity["yellow"], HAClientTheme().getYellowGaugeColor())
|
||||||
// Set a label accessor to control the text of the arc label.
|
];
|
||||||
labelAccessorFn: (GaugeSegment segment, _) =>
|
rangesList.sort((current, next) {
|
||||||
segment.segment == 'Main' ? '${segment.value}' : null,
|
if (current.startFrom > next.startFrom) {
|
||||||
data: data,
|
return 1;
|
||||||
|
}
|
||||||
|
if (current.startFrom < next.startFrom) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
ranges = [
|
||||||
|
GaugeRange(
|
||||||
|
startValue: rangesList[0].startFrom.toDouble(),
|
||||||
|
endValue: rangesList[1].startFrom.toDouble(),
|
||||||
|
color: fixedValue < rangesList[1].startFrom ? rangesList[0].color : rangesList[0].color.withOpacity(0.1),
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
endWidth: 0.3,
|
||||||
|
startWidth: 0.3
|
||||||
|
),
|
||||||
|
GaugeRange(
|
||||||
|
startValue: rangesList[1].startFrom.toDouble(),
|
||||||
|
endValue: rangesList[2].startFrom.toDouble(),
|
||||||
|
color: (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) ? rangesList[1].color : rangesList[1].color.withOpacity(0.1),
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
endWidth: 0.3,
|
||||||
|
startWidth: 0.3
|
||||||
|
),
|
||||||
|
GaugeRange(
|
||||||
|
startValue: rangesList[2].startFrom.toDouble(),
|
||||||
|
endValue: widget.max.toDouble(),
|
||||||
|
color: fixedValue >= rangesList[2].startFrom ? rangesList[2].color : rangesList[2].color.withOpacity(0.1),
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
endWidth: 0.3,
|
||||||
|
startWidth: 0.3
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (ranges == null) {
|
||||||
|
ranges = <GaugeRange>[
|
||||||
|
GaugeRange(
|
||||||
|
startValue: widget.min.toDouble(),
|
||||||
|
endValue: widget.max.toDouble(),
|
||||||
|
color: Theme.of(context).primaryColorDark,
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
endWidth: 0.3,
|
||||||
|
startWidth: 0.3
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
|
||||||
|
|
||||||
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: 2,
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
overflow: Overflow.clip,
|
|
||||||
children: [
|
|
||||||
LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
double verticalOffset;
|
|
||||||
if(constraints.maxWidth > 150.0) {
|
|
||||||
verticalOffset = 0.2;
|
|
||||||
} else if (constraints.maxWidth > 100.0) {
|
|
||||||
verticalOffset = 0.3;
|
|
||||||
} else {
|
|
||||||
verticalOffset = 0.3;
|
|
||||||
}
|
|
||||||
return FractionallySizedBox(
|
|
||||||
heightFactor: 2,
|
|
||||||
widthFactor: 1,
|
|
||||||
alignment: FractionalOffset(0,verticalOffset),
|
|
||||||
child: charts.PieChart(
|
|
||||||
_createData(entityWrapper.entity.doubleState),
|
|
||||||
animate: false,
|
|
||||||
defaultRenderer: charts.ArcRendererConfig(
|
|
||||||
arcRatio: 0.4,
|
|
||||||
startAngle: pi,
|
|
||||||
arcLength: pi,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
double fontSize = constraints.maxHeight / 7;
|
double fontSizeFactor;
|
||||||
return Padding(
|
if (constraints.maxWidth > 300.0) {
|
||||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
fontSizeFactor = 1.6;
|
||||||
child: SimpleEntityState(
|
} else if (constraints.maxWidth > 150.0) {
|
||||||
//textAlign: TextAlign.center,
|
fontSizeFactor = 1;
|
||||||
|
} else if (constraints.maxWidth > 100.0) {
|
||||||
|
fontSizeFactor = 0.6;
|
||||||
|
} else {
|
||||||
|
fontSizeFactor = 0.4;
|
||||||
|
}
|
||||||
|
return SfRadialGauge(
|
||||||
|
axes: <RadialAxis>[
|
||||||
|
RadialAxis(
|
||||||
|
maximum: widget.max.toDouble(),
|
||||||
|
minimum: widget.min.toDouble(),
|
||||||
|
showLabels: false,
|
||||||
|
showTicks: false,
|
||||||
|
canScaleToFit: true,
|
||||||
|
ranges: ranges,
|
||||||
|
annotations: <GaugeAnnotation>[
|
||||||
|
GaugeAnnotation(
|
||||||
|
angle: -90,
|
||||||
|
positionFactor: 1.3,
|
||||||
|
//verticalAlignment: GaugeAlignment.far,
|
||||||
|
widget: EntityName(
|
||||||
|
textStyle: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
fontSize: Theme.of(context).textTheme.body1.fontSize * fontSizeFactor
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GaugeAnnotation(
|
||||||
|
angle: 180,
|
||||||
|
positionFactor: 0,
|
||||||
|
verticalAlignment: GaugeAlignment.center,
|
||||||
|
widget: SimpleEntityState(
|
||||||
expanded: false,
|
expanded: false,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
bold: true,
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
padding: EdgeInsets.all(0.0),
|
textStyle: Theme.of(context).textTheme.title.copyWith(
|
||||||
fontSize: fontSize,
|
fontSize: Theme.of(context).textTheme.title.fontSize * fontSizeFactor,
|
||||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Align(
|
)
|
||||||
alignment: Alignment.bottomCenter,
|
],
|
||||||
child: LayoutBuilder(
|
axisLineStyle: AxisLineStyle(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
thickness: 0.3,
|
||||||
double fontSize = constraints.maxHeight / 7;
|
thicknessUnit: GaugeSizeUnit.factor
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: fontSize),
|
|
||||||
child: EntityName(
|
|
||||||
fontSize: fontSize,
|
|
||||||
maxLines: 1,
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
|
startAngle: 180,
|
||||||
|
endAngle: 0,
|
||||||
|
pointers: <GaugePointer>[
|
||||||
|
NeedlePointer(
|
||||||
|
value: fixedValue,
|
||||||
|
lengthUnit: GaugeSizeUnit.factor,
|
||||||
|
needleLength: 0.9,
|
||||||
|
needleColor: Theme.of(context).accentColor,
|
||||||
|
enableAnimation: true,
|
||||||
|
needleStartWidth: 1,
|
||||||
|
animationType: AnimationType.bounceOut,
|
||||||
|
needleEndWidth: 3,
|
||||||
|
knobStyle: KnobStyle(
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
color: Theme.of(context).buttonColor,
|
||||||
|
knobRadius: 0.1
|
||||||
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GaugeSegment {
|
class RangeContainer {
|
||||||
final String segment;
|
final int startFrom;
|
||||||
final double value;
|
Color color;
|
||||||
final charts.Color color;
|
|
||||||
|
|
||||||
GaugeSegment(this.segment, this.value, Color color)
|
RangeContainer(this.startFrom, this.color);
|
||||||
: this.color = charts.Color(
|
|
||||||
r: color.red, g: color.green, b: color.blue, a: color.alpha);
|
|
||||||
}
|
}
|
@ -6,7 +6,6 @@ class GlanceCardEntityContainer extends StatelessWidget {
|
|||||||
final bool showState;
|
final bool showState;
|
||||||
final bool nameInTheBottom;
|
final bool nameInTheBottom;
|
||||||
final double iconSize;
|
final double iconSize;
|
||||||
final double nameFontSize;
|
|
||||||
final bool wordsWrapInName;
|
final bool wordsWrapInName;
|
||||||
|
|
||||||
GlanceCardEntityContainer({
|
GlanceCardEntityContainer({
|
||||||
@ -15,7 +14,6 @@ class GlanceCardEntityContainer extends StatelessWidget {
|
|||||||
@required this.showState,
|
@required this.showState,
|
||||||
this.nameInTheBottom: false,
|
this.nameInTheBottom: false,
|
||||||
this.iconSize: Sizes.iconSize,
|
this.iconSize: Sizes.iconSize,
|
||||||
this.nameFontSize: Sizes.smallFontSize,
|
|
||||||
this.wordsWrapInName: false
|
this.wordsWrapInName: false
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -31,7 +29,7 @@ class GlanceCardEntityContainer extends StatelessWidget {
|
|||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (!nameInTheBottom) {
|
if (!nameInTheBottom) {
|
||||||
if (showName) {
|
if (showName) {
|
||||||
result.add(_buildName());
|
result.add(_buildName(context));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (showState) {
|
if (showState) {
|
||||||
@ -49,7 +47,7 @@ class GlanceCardEntityContainer extends StatelessWidget {
|
|||||||
result.add(_buildState());
|
result.add(_buildState());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result.add(_buildName());
|
result.add(_buildName(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
@ -60,17 +58,18 @@ class GlanceCardEntityContainer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildName() {
|
Widget _buildName(BuildContext context) {
|
||||||
return EntityName(
|
return EntityName(
|
||||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||||
textOverflow: TextOverflow.ellipsis,
|
textOverflow: TextOverflow.ellipsis,
|
||||||
wordsWrap: wordsWrapInName,
|
wordsWrap: wordsWrapInName,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
fontSize: nameFontSize,
|
textStyle: Theme.of(context).textTheme.body1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,57 +34,5 @@ class _LightCardBodyState extends State<LightCardBody> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1.5,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
overflow: Overflow.clip,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
double fontSize = constraints.maxHeight / 7;
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
|
||||||
child: SimpleEntityState(
|
|
||||||
//textAlign: TextAlign.center,
|
|
||||||
expanded: false,
|
|
||||||
maxLines: 1,
|
|
||||||
bold: true,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
fontSize: fontSize,
|
|
||||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
double fontSize = constraints.maxHeight / 7;
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: fontSize),
|
|
||||||
child: EntityName(
|
|
||||||
fontSize: fontSize,
|
|
||||||
maxLines: 1,
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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"];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,10 +126,10 @@ class Sizes {
|
|||||||
static const extendedWidgetHeight = 50.0;
|
static const extendedWidgetHeight = 50.0;
|
||||||
static const iconSize = 28.0;
|
static const iconSize = 28.0;
|
||||||
static const largeIconSize = 46.0;
|
static const largeIconSize = 46.0;
|
||||||
static const stateFontSize = 15.0;
|
//static const stateFontSize = 15.0;
|
||||||
static const nameFontSize = 15.0;
|
//static const nameFontSize = 15.0;
|
||||||
static const smallFontSize = 14.0;
|
//static const smallFontSize = 14.0;
|
||||||
static const largeFontSize = 24.0;
|
//static const largeFontSize = 24.0;
|
||||||
static const inputWidth = 160.0;
|
static const inputWidth = 160.0;
|
||||||
static const rowPadding = 10.0;
|
static const rowPadding = 10.0;
|
||||||
static const doubleRowPadding = rowPadding*2;
|
static const doubleRowPadding = rowPadding*2;
|
||||||
|
@ -248,7 +248,9 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
|||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
"TRIGGER",
|
"TRIGGER",
|
||||||
style: TextStyle(color: Colors.redAccent)
|
style: Theme.of(context).textTheme.subhead.copyWith(
|
||||||
|
color: Theme.of(context).errorColor
|
||||||
|
)
|
||||||
),
|
),
|
||||||
onPressed: () => _askToTrigger(entity),
|
onPressed: () => _askToTrigger(entity),
|
||||||
)
|
)
|
||||||
|
@ -7,8 +7,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
double iconSize = 26.0;
|
double iconSize = 26.0;
|
||||||
Widget badgeIcon;
|
Widget badgeIcon;
|
||||||
String onBadgeTextValue;
|
String onBadgeTextValue;
|
||||||
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
|
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
||||||
EntityColor.badgeColors["default"];
|
|
||||||
switch (entityModel.entityWrapper.entity.domain) {
|
switch (entityModel.entityWrapper.entity.domain) {
|
||||||
case "sun":
|
case "sun":
|
||||||
{
|
{
|
||||||
@ -30,7 +29,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
badgeIcon = EntityIcon(
|
badgeIcon = EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
color: Colors.black
|
color: Theme.of(context).textTheme.body1.color
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -40,7 +39,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
badgeIcon = EntityIcon(
|
badgeIcon = EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
color: Colors.black
|
color: Theme.of(context).textTheme.body1.color
|
||||||
);
|
);
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||||
break;
|
break;
|
||||||
@ -64,7 +63,9 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: stateFontSize),
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
fontSize: stateFontSize
|
||||||
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -77,7 +78,9 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
onBadgeText = Container(
|
onBadgeText = Container(
|
||||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||||
child: Text("$onBadgeTextValue",
|
child: Text("$onBadgeTextValue",
|
||||||
style: TextStyle(fontSize: 12.0, color: Colors.white),
|
style: Theme.of(context).textTheme.overline.copyWith(
|
||||||
|
color: HAClientTheme().getOnBadgeTextColor()
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.fade),
|
overflow: TextOverflow.fade),
|
||||||
@ -98,7 +101,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
decoration: new BoxDecoration(
|
decoration: new BoxDecoration(
|
||||||
// Circle shape
|
// Circle shape
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: Colors.white,
|
color: Theme.of(context).cardColor,
|
||||||
// The border you want
|
// The border you want
|
||||||
border: new Border.all(
|
border: new Border.all(
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
@ -131,7 +134,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
"${entityModel.entityWrapper.displayName}",
|
"${entityModel.entityWrapper.displayName}",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 12.0),
|
style: Theme.of(context).textTheme.caption,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
@ -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,45 +12,165 @@ class CameraStreamView extends StatefulWidget {
|
|||||||
|
|
||||||
class _CameraStreamViewState extends State<CameraStreamView> {
|
class _CameraStreamViewState extends State<CameraStreamView> {
|
||||||
|
|
||||||
|
CameraEntity _entity;
|
||||||
|
String _streamUrl = "";
|
||||||
|
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) {
|
||||||
|
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||||
|
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
|
||||||
|
_webViewHtml = Uri.dataFromString(
|
||||||
|
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||||
|
mimeType: 'text/html',
|
||||||
|
encoding: Encoding.getByName('utf-8')
|
||||||
|
).toString();
|
||||||
|
_loading.complete();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catchError((e) {
|
||||||
|
_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() {
|
Widget _buildScreen() {
|
||||||
Launcher.launchURLInCustomTab(
|
Widget screenWidget;
|
||||||
context: context,
|
if (!_isLoaded) {
|
||||||
url: streamUrl
|
screenWidget = Center(
|
||||||
|
child: EntityPicture(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
screenWidget = WebView(
|
||||||
|
initialUrl: _webViewHtml,
|
||||||
|
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
|
||||||
|
debuggingEnabled: Logger.isInDebugMode,
|
||||||
|
gestureNavigationEnabled: false,
|
||||||
|
javascriptMode: JavascriptMode.unrestricted,
|
||||||
|
javascriptChannels: {
|
||||||
|
JavascriptChannel(
|
||||||
|
name: _jsMessageChannelName,
|
||||||
|
onMessageReceived: ((message) {
|
||||||
|
Logger.d('[Camera Player] Message from page: $message');
|
||||||
|
setState((){
|
||||||
|
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AspectRatio(
|
||||||
|
aspectRatio: _aspectRatio,
|
||||||
|
child: screenWidget
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildControls() {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
iconSize: 40,
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
onPressed: _isLoaded ? () {
|
||||||
|
setState(() {
|
||||||
|
_isLoaded = false;
|
||||||
|
});
|
||||||
|
} : null,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.fullscreen),
|
||||||
|
iconSize: 40,
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
onPressed: _isLoaded ? () {
|
||||||
|
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(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
_buildScreen(),
|
||||||
padding: const EdgeInsets.all(20.0),
|
_buildControls()
|
||||||
child: IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
|
||||||
iconSize: 50.0,
|
|
||||||
onPressed: () => launchStream(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return _buildScreen();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -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) {
|
||||||
|
if (!_temperaturePending) {
|
||||||
_tmpTemperature = entity.temperature;
|
_tmpTemperature = entity.temperature;
|
||||||
_tmpTargetHigh = entity.targetHigh;
|
_tmpTargetHigh = entity.targetHigh;
|
||||||
_tmpTargetLow = entity.targetLow;
|
_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(
|
||||||
@ -222,20 +204,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
//_buildOnOffControl(entity),
|
//_buildOnOffControl(entity),
|
||||||
_buildTemperatureControls(entity),
|
_buildTemperatureControls(entity, context),
|
||||||
_buildTargetTemperatureControls(entity),
|
_buildTargetTemperatureControls(entity, context),
|
||||||
_buildHumidityControls(entity),
|
_buildHumidityControls(entity, context),
|
||||||
_buildOperationControl(entity),
|
_buildOperationControl(entity, context),
|
||||||
_buildFanControl(entity),
|
_buildFanControl(entity, context),
|
||||||
_buildSwingControl(entity),
|
_buildSwingControl(entity, context),
|
||||||
_buildPresetModeControl(entity),
|
_buildPresetModeControl(entity, context),
|
||||||
_buildAuxHeatControl(entity)
|
_buildAuxHeatControl(entity, context)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPresetModeControl(ClimateEntity entity) {
|
Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportPresetMode) {
|
if (entity.supportPresetMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.presetModes,
|
options: entity.presetModes,
|
||||||
@ -260,7 +242,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportAuxHeat ) {
|
if (entity.supportAuxHeat ) {
|
||||||
return ModeSwitchWidget(
|
return ModeSwitchWidget(
|
||||||
caption: "Aux heat",
|
caption: "Aux heat",
|
||||||
@ -272,7 +254,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOperationControl(ClimateEntity entity) {
|
Widget _buildOperationControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.hvacModes != null) {
|
if (entity.hvacModes != null) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setHVACMode(entity, mode),
|
onChange: (mode) => _setHVACMode(entity, mode),
|
||||||
@ -285,7 +267,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFanControl(ClimateEntity entity) {
|
Widget _buildFanControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportFanMode) {
|
if (entity.supportFanMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.fanModes,
|
options: entity.fanModes,
|
||||||
@ -298,7 +280,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSwingControl(ClimateEntity entity) {
|
Widget _buildSwingControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportSwingMode) {
|
if (entity.supportSwingMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setSwingMode(entity, mode),
|
onChange: (mode) => _setSwingMode(entity, mode),
|
||||||
@ -311,17 +293,15 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTemperatureControls(ClimateEntity entity) {
|
Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) {
|
||||||
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Target temperature", style: TextStyle(
|
Text("Target temperature", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTemperature,
|
value: _tmpTemperature,
|
||||||
fontColor: _showPending ? Colors.red : Colors.black,
|
active: _temperaturePending,
|
||||||
onDec: () => _temperatureDown(entity),
|
onDec: () => _temperatureDown(entity),
|
||||||
onInc: () => _temperatureUp(entity),
|
onInc: () => _temperatureUp(entity),
|
||||||
)
|
)
|
||||||
@ -332,13 +312,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) {
|
||||||
List<Widget> controls = [];
|
List<Widget> controls = [];
|
||||||
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
||||||
controls.addAll(<Widget>[
|
controls.addAll(<Widget>[
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetLow,
|
value: _tmpTargetLow,
|
||||||
fontColor: _showPending ? Colors.red : Colors.black,
|
active: _temperaturePending,
|
||||||
onDec: () => _targetLowDown(entity),
|
onDec: () => _targetLowDown(entity),
|
||||||
onInc: () => _targetLowUp(entity),
|
onInc: () => _targetLowUp(entity),
|
||||||
),
|
),
|
||||||
@ -351,7 +331,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
controls.add(
|
controls.add(
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetHigh,
|
value: _tmpTargetHigh,
|
||||||
fontColor: _showPending ? Colors.red : Colors.black,
|
active: _temperaturePending,
|
||||||
onDec: () => _targetHighDown(entity),
|
onDec: () => _targetHighDown(entity),
|
||||||
onInc: () => _targetHighUp(entity),
|
onInc: () => _targetHighUp(entity),
|
||||||
)
|
)
|
||||||
@ -361,9 +341,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Target temperature range", style: TextStyle(
|
Text("Target temperature range", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
Row(
|
Row(
|
||||||
children: controls,
|
children: controls,
|
||||||
)
|
)
|
||||||
@ -374,13 +352,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHumidityControls(ClimateEntity entity) {
|
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (entity.supportTargetHumidity) {
|
if (entity.supportTargetHumidity) {
|
||||||
result.addAll(<Widget>[
|
result.addAll(<Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$_tmpTargetHumidity%",
|
"$_tmpTargetHumidity%",
|
||||||
style: TextStyle(fontSize: Sizes.largeFontSize),
|
style: Theme.of(context).textTheme.display1,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Slider(
|
child: Slider(
|
||||||
@ -405,9 +383,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
child: Text("Target humidity", style: TextStyle(
|
child: Text("Target humidity", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@ -429,7 +405,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_resetTimer?.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,23 +33,16 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("$displayState",
|
Text("$displayState",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: Theme.of(context).textTheme.body2),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
)),
|
|
||||||
Text(" $targetTemp",
|
Text(" $targetTemp",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: Theme.of(context).textTheme.body1)
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
entity.currentTemperature != null ?
|
entity.currentTemperature != null ?
|
||||||
Text("Currently: ${entity.currentTemperature}",
|
Text("Currently: ${entity.currentTemperature}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: Theme.of(context).textTheme.subtitle
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
color: Colors.black45)
|
|
||||||
) :
|
) :
|
||||||
Container(height: 0.0,)
|
Container(height: 0.0,)
|
||||||
],
|
],
|
||||||
|
@ -3,10 +3,8 @@ 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 valueFontSize;
|
|
||||||
final onChange;
|
final onChange;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
|
|
||||||
@ -16,8 +14,6 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
@required this.options,
|
@required this.options,
|
||||||
this.value,
|
this.value,
|
||||||
@required this.onChange,
|
@required this.onChange,
|
||||||
this.captionFontSize,
|
|
||||||
this.valueFontSize,
|
|
||||||
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -28,9 +24,7 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("$caption", style: TextStyle(
|
Text("$caption", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -40,15 +34,12 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
value: value,
|
value: value,
|
||||||
iconSize: 30.0,
|
iconSize: 30.0,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.title,
|
||||||
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
|
||||||
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),
|
||||||
|
@ -4,7 +4,6 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
final onChange;
|
final onChange;
|
||||||
final double captionFontSize;
|
|
||||||
final bool value;
|
final bool value;
|
||||||
final bool expanded;
|
final bool expanded;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
@ -13,7 +12,6 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
Key key,
|
Key key,
|
||||||
@required this.caption,
|
@required this.caption,
|
||||||
@required this.onChange,
|
@required this.onChange,
|
||||||
this.captionFontSize,
|
|
||||||
this.value,
|
this.value,
|
||||||
this.expanded: true,
|
this.expanded: true,
|
||||||
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
|
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
|
||||||
@ -25,7 +23,7 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
padding: this.padding,
|
padding: this.padding,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildCaption(),
|
_buildCaption(context),
|
||||||
Switch(
|
Switch(
|
||||||
onChanged: (value) => onChange(value),
|
onChanged: (value) => onChange(value),
|
||||||
value: value ?? false,
|
value: value ?? false,
|
||||||
@ -35,12 +33,10 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCaption() {
|
Widget _buildCaption(BuildContext context) {
|
||||||
Widget captionWidget = Text(
|
Widget captionWidget = Text(
|
||||||
"$caption",
|
"$caption",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1,
|
||||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
|
@ -2,8 +2,7 @@ part of '../../../main.dart';
|
|||||||
|
|
||||||
class TemperatureControlWidget extends StatelessWidget {
|
class TemperatureControlWidget extends StatelessWidget {
|
||||||
final double value;
|
final double value;
|
||||||
final double fontSize;
|
final bool active;
|
||||||
final Color fontColor;
|
|
||||||
final onInc;
|
final onInc;
|
||||||
final onDec;
|
final onDec;
|
||||||
|
|
||||||
@ -12,8 +11,9 @@ class TemperatureControlWidget extends StatelessWidget {
|
|||||||
@required this.value,
|
@required this.value,
|
||||||
@required this.onInc,
|
@required this.onInc,
|
||||||
@required this.onDec,
|
@required this.onDec,
|
||||||
this.fontSize,
|
//this.fontSize,
|
||||||
this.fontColor})
|
this.active: false
|
||||||
|
})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -23,10 +23,7 @@ class TemperatureControlWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$value",
|
"$value",
|
||||||
style: TextStyle(
|
style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1,
|
||||||
fontSize: fontSize ?? 24.0,
|
|
||||||
color: fontColor ?? Colors.black
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
@ -64,9 +64,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
child: Text("Position", style: TextStyle(
|
child: Text("Position"),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
Slider(
|
Slider(
|
||||||
value: _tmpPosition,
|
value: _tmpPosition,
|
||||||
@ -118,9 +116,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
controls.insert(0, Padding(
|
controls.insert(0, Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
child: Text("Tilt position", style: TextStyle(
|
child: Text("Tilt position"),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
));
|
));
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
@ -9,10 +9,8 @@ class DateTimeStateWidget extends StatelessWidget {
|
|||||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Text("${entity.formattedState}",
|
child: Text("${entity.formattedState}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right
|
||||||
style: new TextStyle(
|
),
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
)),
|
|
||||||
onTap: () => _handleStateTap(context, entity),
|
onTap: () => _handleStateTap(context, entity),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -15,21 +15,19 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
return MissedEntityWidget();
|
return MissedEntityWidget();
|
||||||
}
|
}
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
||||||
return Divider(
|
return Divider();
|
||||||
color: Colors.black45,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Divider(
|
Divider(),
|
||||||
color: Colors.black45,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
"${entityModel.entityWrapper.entity.displayName}",
|
"${entityModel.entityWrapper.entity.displayName}",
|
||||||
style: TextStyle(color: Colors.blue),
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
color: Theme.of(context).primaryColor
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -61,6 +59,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;
|
||||||
}
|
}
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityColor {
|
|
||||||
|
|
||||||
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
|
|
||||||
|
|
||||||
static const badgeColors = {
|
|
||||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
|
||||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
|
||||||
};
|
|
||||||
|
|
||||||
static const _stateColors = {
|
|
||||||
EntityState.on: Colors.amber,
|
|
||||||
"auto": Colors.amber,
|
|
||||||
EntityState.active: Colors.amber,
|
|
||||||
EntityState.playing: Colors.amber,
|
|
||||||
EntityState.paused: Colors.amber,
|
|
||||||
"above_horizon": Colors.amber,
|
|
||||||
EntityState.home: Colors.amber,
|
|
||||||
EntityState.open: Colors.amber,
|
|
||||||
EntityState.cleaning: Colors.amber,
|
|
||||||
EntityState.returning: Colors.amber,
|
|
||||||
EntityState.off: defaultStateColor,
|
|
||||||
EntityState.closed: defaultStateColor,
|
|
||||||
"below_horizon": defaultStateColor,
|
|
||||||
"default": defaultStateColor,
|
|
||||||
EntityState.idle: defaultStateColor,
|
|
||||||
"heat": Colors.redAccent,
|
|
||||||
"cool": Colors.lightBlue,
|
|
||||||
EntityState.unavailable: Colors.black26,
|
|
||||||
EntityState.unknown: Colors.black26,
|
|
||||||
EntityState.alarm_disarmed: Colors.green,
|
|
||||||
EntityState.alarm_armed_away: Colors.redAccent,
|
|
||||||
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
|
|
||||||
EntityState.alarm_armed_home: Colors.redAccent,
|
|
||||||
EntityState.alarm_armed_night: Colors.redAccent,
|
|
||||||
EntityState.alarm_triggered: Colors.redAccent,
|
|
||||||
EntityState.alarm_arming: Colors.amber,
|
|
||||||
EntityState.alarm_disarming: Colors.amber,
|
|
||||||
EntityState.alarm_pending: Colors.amber,
|
|
||||||
};
|
|
||||||
|
|
||||||
static Color stateColor(String state) {
|
|
||||||
return _stateColors[state] ?? _stateColors["default"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static charts.Color chartHistoryStateColor(String state, int id) {
|
|
||||||
Color c = _stateColors[state];
|
|
||||||
if (c != null) {
|
|
||||||
return charts.Color(
|
|
||||||
r: c.red,
|
|
||||||
g: c.green,
|
|
||||||
b: c.blue,
|
|
||||||
a: c.alpha
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
double r = id.toDouble() % 10;
|
|
||||||
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Color historyStateColor(String state, int id) {
|
|
||||||
Color c = _stateColors[state];
|
|
||||||
if (c != null) {
|
|
||||||
return c;
|
|
||||||
} else {
|
|
||||||
if (id > -1) {
|
|
||||||
double r = id.toDouble() % 10;
|
|
||||||
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
|
||||||
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
|
|
||||||
} else {
|
|
||||||
return _stateColors[EntityState.on];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -67,7 +67,7 @@ class EntityIcon extends StatelessWidget {
|
|||||||
padding: padding,
|
padding: padding,
|
||||||
child: buildIcon(
|
child: buildIcon(
|
||||||
entityWrapper,
|
entityWrapper,
|
||||||
color ?? EntityColor.stateColor(entityWrapper.entity.state)
|
color ?? HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,18 +5,24 @@ class EntityName extends StatelessWidget {
|
|||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final TextOverflow textOverflow;
|
final TextOverflow textOverflow;
|
||||||
final bool wordsWrap;
|
final bool wordsWrap;
|
||||||
final double fontSize;
|
|
||||||
final TextAlign textAlign;
|
final TextAlign textAlign;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
|
final TextStyle textStyle;
|
||||||
|
|
||||||
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
|
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.textStyle, this.wordsWrap: true, this.textAlign: TextAlign.left}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
TextStyle textStyle = TextStyle(fontSize: fontSize);
|
TextStyle tStyle;
|
||||||
|
if (textStyle == null) {
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||||
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
|
tStyle = HAClientTheme().getLinkTextStyle(context);
|
||||||
|
} else {
|
||||||
|
tStyle = Theme.of(context).textTheme.body1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tStyle = textStyle;
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
@ -25,7 +31,7 @@ class EntityName extends StatelessWidget {
|
|||||||
overflow: textOverflow,
|
overflow: textOverflow,
|
||||||
softWrap: wordsWrap,
|
softWrap: wordsWrap,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
style: textStyle,
|
style: tStyle,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -16,7 +16,7 @@ class EntityPageLayout extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
showClose ?
|
showClose ?
|
||||||
Container(
|
Container(
|
||||||
color: Colors.blue[300],
|
color: Theme.of(context).primaryColor,
|
||||||
height: 40,
|
height: 40,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -25,18 +25,14 @@ class EntityPageLayout extends StatelessWidget {
|
|||||||
padding: EdgeInsets.only(left: 8),
|
padding: EdgeInsets.only(left: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
entity.displayName,
|
entity.displayName,
|
||||||
style: TextStyle(
|
style: Theme.of(context).primaryTextTheme.headline
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 22
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
padding: EdgeInsets.all(0),
|
padding: EdgeInsets.all(0),
|
||||||
icon: Icon(Icons.close),
|
icon: Icon(Icons.close),
|
||||||
color: Colors.white,
|
color: Theme.of(context).primaryTextTheme.headline.color,
|
||||||
iconSize: 36.0,
|
iconSize: 36.0,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
eventBus.fire(ShowEntityPageEvent());
|
eventBus.fire(ShowEntityPageEvent());
|
||||||
|
71
lib/entities/entity_picture.widget.dart
Normal file
71
lib/entities/entity_picture.widget.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityPicture extends StatelessWidget {
|
||||||
|
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
|
const EntityPicture({Key key, this.padding: const EdgeInsets.all(0.0), this.fit: BoxFit.cover}) : super(key: key);
|
||||||
|
|
||||||
|
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||||
|
String domain = entityId.split(".")[0];
|
||||||
|
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
|
||||||
|
String iconNameByDeviceClass;
|
||||||
|
if (deviceClass != null) {
|
||||||
|
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
|
||||||
|
}
|
||||||
|
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
|
||||||
|
if (iconName != null) {
|
||||||
|
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildIcon(EntityWrapper data, BuildContext context) {
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String iconName = data.icon;
|
||||||
|
int iconCode = 0;
|
||||||
|
if (iconName.length > 0) {
|
||||||
|
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||||
|
} else {
|
||||||
|
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||||
|
data.entity.deviceClass, data.entity.state); //
|
||||||
|
}
|
||||||
|
Widget iconPicture = Container(
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
|
size: Sizes.largeIconSize,
|
||||||
|
color: HAClientTheme().getOffStateColor(context),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (data.entityPicture != null) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: data.entityPicture,
|
||||||
|
fit: this.fit,
|
||||||
|
errorWidget: (context, _, __) => iconPicture,
|
||||||
|
placeholder: (context, _) => iconPicture,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconPicture;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
return Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: buildIcon(
|
||||||
|
entityWrapper,
|
||||||
|
context
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,30 +2,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -6,7 +6,6 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
final String serviceName;
|
final String serviceName;
|
||||||
final String entityId;
|
final String entityId;
|
||||||
final String text;
|
final String text;
|
||||||
final double fontSize;
|
|
||||||
|
|
||||||
FlatServiceButton({
|
FlatServiceButton({
|
||||||
Key key,
|
Key key,
|
||||||
@ -14,7 +13,6 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
@required this.serviceName,
|
@required this.serviceName,
|
||||||
@required this.entityId,
|
@required this.entityId,
|
||||||
@required this.text,
|
@required this.text,
|
||||||
this.fontSize: Sizes.stateFontSize
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
void _setNewState() {
|
void _setNewState() {
|
||||||
@ -24,7 +22,7 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: fontSize*2.5,
|
height: Theme.of(context).textTheme.subhead.fontSize*2.5,
|
||||||
child: FlatButton(
|
child: FlatButton(
|
||||||
onPressed: (() {
|
onPressed: (() {
|
||||||
_setNewState();
|
_setNewState();
|
||||||
@ -32,8 +30,7 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context),
|
||||||
new TextStyle(fontSize: fontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -183,7 +183,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
}
|
}
|
||||||
return UniversalSlider(
|
return UniversalSlider(
|
||||||
title: "Color temperature",
|
title: "Color temperature",
|
||||||
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
|
leading: Text("Cold", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.lightBlue)),
|
||||||
value: val,
|
value: val,
|
||||||
onChangeEnd: (value) => _setColorTemp(entity, value),
|
onChangeEnd: (value) => _setColorTemp(entity, value),
|
||||||
max: entity.maxMireds,
|
max: entity.maxMireds,
|
||||||
@ -194,7 +194,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpColorTemp = value.round();
|
_tmpColorTemp = value.round();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
|
closing: Text("Warm", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.amberAccent),),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0.0, height: 0.0);
|
return Container(width: 0.0, height: 0.0);
|
||||||
@ -224,7 +224,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
color: savedColor?.toColor() ?? Colors.transparent,
|
color: savedColor?.toColor() ?? Theme.of(context).backgroundColor,
|
||||||
child: Text('Paste color'),
|
child: Text('Paste color'),
|
||||||
onPressed: savedColor == null ? null : () {
|
onPressed: savedColor == null ? null : () {
|
||||||
_setColor(entity, savedColor);
|
_setColor(entity, savedColor);
|
||||||
|
@ -28,8 +28,7 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
onPressed: () => _unlock(entity),
|
onPressed: () => _unlock(entity),
|
||||||
child: Text("UNLOCK",
|
child: Text("UNLOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context)
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -39,8 +38,7 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
onPressed: () => _lock(entity),
|
onPressed: () => _lock(entity),
|
||||||
child: Text("LOCK",
|
child: Text("LOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context),
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -56,8 +54,7 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
entity.isLocked ? "UNLOCK" : "LOCK",
|
entity.isLocked ? "UNLOCK" : "LOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context),
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -84,12 +84,11 @@ 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);
|
||||||
@ -101,8 +100,6 @@ class MediaPlayerEntity extends Entity {
|
|||||||
.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,18 +22,18 @@ 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();
|
||||||
|
if (currentPosition > 0) {
|
||||||
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
|
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
|
||||||
} else {
|
}
|
||||||
progress = 0;
|
|
||||||
}
|
}
|
||||||
return LinearProgressIndicator(
|
return LinearProgressIndicator(
|
||||||
value: progress,
|
value: progress,
|
||||||
backgroundColor: Colors.black45,
|
backgroundColor: Colors.black45,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
|
valueColor: AlwaysStoppedAnimation<Color>(HAClientTheme().getOnStateColor(context)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,12 +13,6 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
double _currentPosition = 0;
|
double _currentPosition = 0;
|
||||||
int _savedPosition = 0;
|
int _savedPosition = 0;
|
||||||
|
|
||||||
final TextStyle _seekTextStyle = TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
color: Colors.blue,
|
|
||||||
fontWeight: FontWeight.bold
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -53,8 +47,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
buttons.add(
|
buttons.add(
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
|
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
|
||||||
color: Colors.orange,
|
color: Theme.of(context).accentColor,
|
||||||
focusColor: Colors.white,
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ConnectionManager().callService(
|
ConnectionManager().callService(
|
||||||
domain: "media_player",
|
domain: "media_player",
|
||||||
@ -79,7 +72,13 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("00:00"),
|
Text("00:00"),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle),
|
child: Text(
|
||||||
|
"${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.title.copyWith(
|
||||||
|
color: Colors.blue
|
||||||
|
)
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
|
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
|
||||||
],
|
],
|
||||||
@ -87,8 +86,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
Container(height: 10,),
|
Container(height: 10,),
|
||||||
Slider(
|
Slider(
|
||||||
min: 0,
|
min: 0,
|
||||||
activeColor: Colors.amber,
|
activeColor: Theme.of(context).accentColor,
|
||||||
inactiveColor: Colors.black26,
|
|
||||||
max: entity.durationSeconds.toDouble(),
|
max: entity.durationSeconds.toDouble(),
|
||||||
value: _currentPosition,
|
value: _currentPosition,
|
||||||
onChangeStart: (val) {
|
onChangeStart: (val) {
|
||||||
|
@ -12,14 +12,14 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
Stack(
|
Stack(
|
||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildImage(entity),
|
_buildImage(entity, context),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0.0,
|
bottom: 0.0,
|
||||||
left: 0.0,
|
left: 0.0,
|
||||||
right: 0.0,
|
right: 0.0,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black45,
|
color: Colors.black45,
|
||||||
child: _buildState(entity),
|
child: _buildState(entity, context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -35,12 +35,9 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildState(MediaPlayerEntity entity) {
|
Widget _buildState(MediaPlayerEntity entity, BuildContext context) {
|
||||||
TextStyle style = TextStyle(
|
TextStyle style = Theme.of(context).textTheme.body1.copyWith(
|
||||||
fontSize: 14.0,
|
color: Colors.white
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
height: 1.2
|
|
||||||
);
|
);
|
||||||
List<Widget> states = [];
|
List<Widget> states = [];
|
||||||
states.add(Text("${entity.displayName}", style: style));
|
states.add(Text("${entity.displayName}", style: style));
|
||||||
@ -71,7 +68,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImage(MediaPlayerEntity entity) {
|
Widget _buildImage(MediaPlayerEntity entity, BuildContext context) {
|
||||||
String state = entity.state;
|
String state = entity.state;
|
||||||
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -97,7 +94,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
||||||
size: 150.0,
|
size: 150.0,
|
||||||
color: EntityColor.stateColor("$state"),
|
color: HAClientTheme().getColorByEntityState("$state", context),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -356,13 +353,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 +457,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _duplicateTo(entity) {
|
void _duplicateTo(entity) {
|
||||||
|
if (entity.canCalculateActualPosition()) {
|
||||||
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
|
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"]
|
||||||
|
@ -7,10 +7,10 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
final String customValue;
|
final String customValue;
|
||||||
final double fontSize;
|
final TextStyle textStyle;
|
||||||
final bool bold;
|
//final bool bold;
|
||||||
|
|
||||||
const SimpleEntityState({Key key,this.bold: false, this.maxLines: 10, this.fontSize: Sizes.stateFontSize, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
const SimpleEntityState({Key key,/*this.bold: false,*/ this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.textStyle, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -22,16 +22,19 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
state = customValue;
|
state = customValue;
|
||||||
}
|
}
|
||||||
TextStyle textStyle = TextStyle(
|
TextStyle tStyle;
|
||||||
fontSize: this.fontSize,
|
if (textStyle != null) {
|
||||||
fontWeight: FontWeight.normal
|
tStyle = textStyle;
|
||||||
|
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||||
|
tStyle = Theme.of(context).textTheme.subhead.copyWith(
|
||||||
|
color: Colors.blue
|
||||||
);
|
);
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
} else {
|
||||||
textStyle = textStyle.apply(color: Colors.blue);
|
tStyle = Theme.of(context).textTheme.body1;
|
||||||
}
|
}
|
||||||
if (this.bold) {
|
/*if (this.bold) {
|
||||||
textStyle = textStyle.apply(fontWeightDelta: 100);
|
textStyle = textStyle.apply(fontWeightDelta: 100);
|
||||||
}
|
}*/
|
||||||
while (state.contains(" ")){
|
while (state.contains(" ")){
|
||||||
state = state.replaceAll(" ", " ");
|
state = state.replaceAll(" ", " ");
|
||||||
}
|
}
|
||||||
@ -43,7 +46,7 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
style: textStyle
|
style: tStyle
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
|
@ -62,8 +62,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$_newValue",
|
"$_newValue",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.display1.copyWith(
|
||||||
fontSize: Sizes.largeFontSize,
|
|
||||||
color: Colors.blue
|
color: Colors.blue
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -40,10 +40,7 @@ class UniversalSlider extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Text(
|
Text("$title"),
|
||||||
"$title",
|
|
||||||
style: TextStyle(fontSize: Sizes.stateFontSize),
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -10,7 +10,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildStatusAndBattery(entity),
|
_buildStatusAndBattery(entity, context),
|
||||||
_buildCommands(entity),
|
_buildCommands(entity),
|
||||||
_buildFanSpeed(entity),
|
_buildFanSpeed(entity),
|
||||||
_buildAdditionalInfo(entity)
|
_buildAdditionalInfo(entity)
|
||||||
@ -19,12 +19,12 @@ class VacuumControls extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatusAndBattery(VacuumEntity entity) {
|
Widget _buildStatusAndBattery(VacuumEntity entity, BuildContext context) {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (entity.supportStatus) {
|
if (entity.supportStatus) {
|
||||||
result.addAll(
|
result.addAll(
|
||||||
<Widget>[
|
<Widget>[
|
||||||
Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),),
|
Text("Status:"),
|
||||||
Container(width: 6,),
|
Container(width: 6,),
|
||||||
Expanded(
|
Expanded(
|
||||||
//flex: 1,
|
//flex: 1,
|
||||||
@ -33,10 +33,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body2,
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
fontWeight: FontWeight.bold
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -48,7 +45,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
result.addAll(<Widget>[
|
result.addAll(<Widget>[
|
||||||
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
|
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
|
||||||
Container(width: 6,),
|
Container(width: 6,),
|
||||||
Text("$batteryLevel %", style: TextStyle(fontSize: Sizes.stateFontSize))
|
Text("$batteryLevel %")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -172,7 +169,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Vacuum cleaner commands:", style: TextStyle(fontSize: Sizes.stateFontSize)),
|
Text("Vacuum cleaner commands:"),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
@ -27,10 +27,7 @@ class VacuumStateButton extends StatelessWidget {
|
|||||||
text: "RETURN TO DOCK"
|
text: "RETURN TO DOCK"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = Text(entity.state.toUpperCase(), style: TextStyle(
|
result = Text(entity.state.toUpperCase(), style: Theme.of(context).textTheme.subhead);
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.grey
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(right: 15),
|
padding: EdgeInsets.only(right: 15),
|
||||||
|
@ -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
|
||||||
|
(excludeDomains.isEmpty || !excludeDomains.contains(entity.domain)) &&
|
||||||
|
(includeDomains.isEmpty || includeDomains.contains(entity.domain)) &&
|
||||||
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
((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,30 +139,74 @@ 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");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
|
||||||
|
throw HAError("Error getting config: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future _getStates() 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(
|
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||||
(data) => entities.parse(data)
|
(data) => _parseStates(data)
|
||||||
).catchError((e) {
|
).catchError((e) {
|
||||||
throw HAError("Error getting states: $e");
|
throw HAError("Error getting states: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future _getLovelace() {
|
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();
|
Completer completer = Completer();
|
||||||
ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) {
|
var additionalData;
|
||||||
|
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
|
||||||
|
additionalData = {
|
||||||
|
'url_path': _lovelaceDashbordUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ConnectionManager().sendSocketMessage(
|
||||||
|
type: 'lovelace/config',
|
||||||
|
additionalData: additionalData
|
||||||
|
).then((data) {
|
||||||
_rawLovelaceData = data;
|
_rawLovelaceData = data;
|
||||||
completer.complete();
|
completer.complete();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
if ("$e" == "config_not_found") {
|
if ("$e" == "config_not_found") {
|
||||||
ConnectionManager().useLovelace = false;
|
autoUi = true;
|
||||||
|
_rawLovelaceData = null;
|
||||||
completer.complete();
|
completer.complete();
|
||||||
} else {
|
} else {
|
||||||
completer.completeError(HAError("Error getting lovelace config: $e"));
|
completer.completeError(HAError("Error getting lovelace config: $e"));
|
||||||
@ -121,49 +214,106 @@ class HomeAssistant {
|
|||||||
});
|
});
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getUserInfo() async {
|
|
||||||
_userName = null;
|
|
||||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) {
|
|
||||||
_userName = data["name"];
|
|
||||||
childMode = _userName.startsWith("[child]");
|
|
||||||
}).catchError((e) {
|
|
||||||
Logger.w("Can't get user info: $e");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getServices() async {
|
Future _getServices(SharedPreferences prefs) async {
|
||||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
|
if (prefs != null) {
|
||||||
Logger.d("Got ${data.length} services");
|
try {
|
||||||
services = data;
|
var data = json.decode(prefs.getString('cached_services'));
|
||||||
}).catchError((e) {
|
_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");
|
Logger.w("Can't get services: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getPanels() async {
|
void _parseServices(data) {
|
||||||
panels.clear();
|
services = data;
|
||||||
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
|
}
|
||||||
data.forEach((k,v) {
|
|
||||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
Future _getUserInfo(SharedPreferences sharedPrefs) async {
|
||||||
panels.add(Panel(
|
_userName = null;
|
||||||
id: k,
|
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
|
||||||
type: v["component_name"],
|
Logger.w("Can't get user info: $e");
|
||||||
title: title,
|
|
||||||
urlPath: v["url_path"],
|
|
||||||
config: v["config"],
|
|
||||||
icon: v["icon"]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}
|
||||||
|
|
||||||
|
void _parseUserInfo(data) {
|
||||||
|
_rawUserInfo = data;
|
||||||
|
_userName = data["name"];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getPanels(SharedPreferences sharedPrefs) async {
|
||||||
|
if (sharedPrefs != null) {
|
||||||
|
try {
|
||||||
|
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
||||||
|
_parsePanels(data);
|
||||||
|
} catch (e) {
|
||||||
|
throw HAError("Error getting panels list: $e");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e) {
|
||||||
throw HAError("Error getting panels list: $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,211 +322,26 @@ 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();
|
|
||||||
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
|
||||||
Logger.d("Creating Lovelace UI");
|
Logger.d("Creating Lovelace UI");
|
||||||
_parseLovelace();
|
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
|
||||||
} else {
|
/*if (isServiceExist('zha_map')) {
|
||||||
Logger.d("Creating group-based UI");
|
panels.add(
|
||||||
int viewCounter = 0;
|
Panel(
|
||||||
if (!entities.hasDefaultView) {
|
id: 'haclient_zha',
|
||||||
HAView view = HAView(
|
componentName: 'haclient_zha',
|
||||||
count: viewCounter,
|
title: 'ZHA',
|
||||||
id: "group.default_view",
|
urlPath: '/haclient_zha',
|
||||||
name: "Home",
|
icon: 'mdi:zigbee'
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
137
lib/main.dart
137
lib/main.dart
@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
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';
|
||||||
@ -22,16 +22,19 @@ 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:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
|
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
||||||
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
|
import 'package:syncfusion_flutter_core/core.dart';
|
||||||
|
import 'package:syncfusion_flutter_gauges/gauges.dart';
|
||||||
|
|
||||||
import 'utils/logger.dart';
|
import 'utils/logger.dart';
|
||||||
|
import '.secrets.dart';
|
||||||
|
|
||||||
part 'const.dart';
|
part 'const.dart';
|
||||||
part 'utils/launcher.dart';
|
part 'utils/launcher.dart';
|
||||||
@ -72,7 +75,6 @@ part 'entities/universal_slider.widget.dart';
|
|||||||
part 'entities/flat_service_button.widget.dart';
|
part 'entities/flat_service_button.widget.dart';
|
||||||
part 'entities/light/widgets/light_color_picker.dart';
|
part 'entities/light/widgets/light_color_picker.dart';
|
||||||
part 'entities/camera/widgets/camera_stream_view.dart';
|
part 'entities/camera/widgets/camera_stream_view.dart';
|
||||||
part 'entities/entity_colors.class.dart';
|
|
||||||
part 'plugins/history_chart/entity_history.dart';
|
part 'plugins/history_chart/entity_history.dart';
|
||||||
part 'plugins/history_chart/simple_state_history_chart.dart';
|
part 'plugins/history_chart/simple_state_history_chart.dart';
|
||||||
part 'plugins/history_chart/numeric_state_history_chart.dart';
|
part 'plugins/history_chart/numeric_state_history_chart.dart';
|
||||||
@ -84,6 +86,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';
|
||||||
@ -105,8 +108,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';
|
||||||
@ -118,6 +122,7 @@ part 'managers/mobile_app_integration_manager.class.dart';
|
|||||||
part 'managers/connection_manager.class.dart';
|
part 'managers/connection_manager.class.dart';
|
||||||
part 'managers/device_info_manager.class.dart';
|
part 'managers/device_info_manager.class.dart';
|
||||||
part 'managers/startup_user_messages_manager.class.dart';
|
part 'managers/startup_user_messages_manager.class.dart';
|
||||||
|
part 'managers/theme_manager.dart';
|
||||||
part 'ui.dart';
|
part 'ui.dart';
|
||||||
part 'view.class.dart';
|
part 'view.class.dart';
|
||||||
part 'cards/card.class.dart';
|
part 'cards/card.class.dart';
|
||||||
@ -136,49 +141,94 @@ 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 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.4";
|
const appVersionNumber = "0.8.2";
|
||||||
const appVersionAdd = "";
|
const appVersionAdd = "";
|
||||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
const appVersion = "$appVersionNumber$appVersionAdd";
|
||||||
|
|
||||||
void main() async {
|
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||||
FlutterError.onError = (errorDetails) {
|
// Print the exception to the console.
|
||||||
Logger.e( "${errorDetails.exception}");
|
|
||||||
if (Logger.isInDebugMode) {
|
if (Logger.isInDebugMode) {
|
||||||
FlutterError.dumpErrorToConsole(errorDetails);
|
Logger.e('Caught error: $error');
|
||||||
|
Logger.p(stackTrace);
|
||||||
}
|
}
|
||||||
|
Crashlytics.instance.recordError(error, stackTrace);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
Crashlytics.instance.enableInDevMode = false;
|
||||||
|
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
|
||||||
|
|
||||||
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
|
Logger.e(" Caut Flutter runtime error: ${details.exception}");
|
||||||
|
if (Logger.isInDebugMode) {
|
||||||
|
FlutterError.dumpErrorToConsole(details);
|
||||||
|
}
|
||||||
|
Crashlytics.instance.recordFlutterError(details);
|
||||||
};
|
};
|
||||||
|
|
||||||
runZoned(() {
|
runZoned(() {
|
||||||
|
runApp(new HAClientApp());
|
||||||
|
}, onError: (error, stack) {
|
||||||
|
_reportError(error, stack);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
workManager.Workmanager.initialize(
|
||||||
updateDeviceLocationIsolate,
|
updateDeviceLocationIsolate,
|
||||||
isInDebugMode: false
|
isInDebugMode: false
|
||||||
);
|
);
|
||||||
runApp(new HAClientApp());
|
super.initState();
|
||||||
|
|
||||||
}, onError: (error, stack) {
|
|
||||||
Logger.e("$error");
|
|
||||||
Logger.e("$stack");
|
|
||||||
if (Logger.isInDebugMode) {
|
|
||||||
debugPrint("$stack");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HAClientApp extends StatelessWidget {
|
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
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new MaterialApp(
|
return new MaterialApp(
|
||||||
title: appName,
|
title: appName,
|
||||||
theme: new ThemeData(
|
theme: HAClientTheme().lightTheme,
|
||||||
primarySwatch: Colors.blue,
|
darkTheme: HAClientTheme().darkTheme,
|
||||||
),
|
debugShowCheckedModeBanner: false,
|
||||||
initialRoute: "/",
|
initialRoute: "/",
|
||||||
routes: {
|
routes: {
|
||||||
"/": (context) => MainPage(title: 'HA Client'),
|
"/": (context) => MainPage(title: 'HA Client'),
|
||||||
@ -190,8 +240,45 @@ 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: Theme.of(context).textTheme.button.copyWith(
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
)),
|
||||||
|
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')}"
|
||||||
|
).then((response) {
|
||||||
|
Logger.d("Got temp token");
|
||||||
|
String tempToken = json.decode(response)['access_token'];
|
||||||
|
Logger.d("Closing webview...");
|
||||||
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
|
completer.complete(tempToken);
|
||||||
|
}).catchError((e) {
|
||||||
|
Logger.e("Error getting temp token: ${e.toString()}");
|
||||||
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
|
completer.completeError(HAError("Error getting temp token"));
|
||||||
|
}).whenComplete(() => flutterWebviewPlugin.close());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Logger.d("Launching OAuth");
|
Logger.d("Launching OAuth");
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||||
return completer.future;
|
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) {
|
|
||||||
Logger.d("Got temp token");
|
|
||||||
String tempToken = json.decode(response)['access_token'];
|
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
|
||||||
completer.complete(tempToken);
|
|
||||||
}).catchError((e) {
|
|
||||||
//flutterWebviewPlugin.close();
|
|
||||||
Logger.e("Error getting temp token: ${e.toString()}");
|
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
|
||||||
completer.completeError(HAError("Error getting temp token"));
|
|
||||||
});
|
|
||||||
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((_){
|
||||||
if (completer != null && !completer.isCompleted) {
|
_connect().timeout(connectTimeout).then((_) {
|
||||||
completer.completeError(HAError("Connection timeout"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).then((_) {
|
|
||||||
completer?.complete();
|
completer?.complete();
|
||||||
}).catchError((e) {
|
}).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);
|
completer?.completeError(e);
|
||||||
|
} else {
|
||||||
|
completer?.completeError(HAError("${e.toString()}"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
completer?.complete();
|
completer?.complete();
|
||||||
@ -124,6 +130,7 @@ class ConnectionManager {
|
|||||||
connecting = Completer();
|
connecting = Completer();
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
Logger.d("Socket connecting...");
|
Logger.d("Socket connecting...");
|
||||||
|
try {
|
||||||
_socket = IOWebSocketChannel.connect(
|
_socket = IOWebSocketChannel.connect(
|
||||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||||
_socketSubscription = _socket.stream.listen(
|
_socketSubscription = _socket.stream.listen(
|
||||||
@ -140,11 +147,21 @@ class ConnectionManager {
|
|||||||
});
|
});
|
||||||
} else if (data["type"] == "auth_ok") {
|
} else if (data["type"] == "auth_ok") {
|
||||||
Logger.d("[Received] <== ${data.toString()}");
|
Logger.d("[Received] <== ${data.toString()}");
|
||||||
|
Logger.d("[Connection] Subscribing to events");
|
||||||
|
sendSocketMessage(
|
||||||
|
type: "subscribe_events",
|
||||||
|
additionalData: {"event_type": "lovelace_updated"},
|
||||||
|
);
|
||||||
|
sendSocketMessage(
|
||||||
|
type: "subscribe_events",
|
||||||
|
additionalData: {"event_type": "state_changed"},
|
||||||
|
).whenComplete((){
|
||||||
_messageResolver["auth"]?.complete();
|
_messageResolver["auth"]?.complete();
|
||||||
_messageResolver.remove("auth");
|
_messageResolver.remove("auth");
|
||||||
if (_token != null) {
|
if (_token != null) {
|
||||||
if (!connecting.isCompleted) connecting.complete();
|
if (!connecting.isCompleted) connecting.complete();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} else if (data["type"] == "auth_invalid") {
|
} else if (data["type"] == "auth_invalid") {
|
||||||
Logger.d("[Received] <== ${data.toString()}");
|
Logger.d("[Received] <== ${data.toString()}");
|
||||||
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||||
@ -158,6 +175,9 @@ class ConnectionManager {
|
|||||||
onDone: () => _handleSocketClose(connecting),
|
onDone: () => _handleSocketClose(connecting),
|
||||||
onError: (e) => _handleSocketError(e, connecting)
|
onError: (e) => _handleSocketError(e, connecting)
|
||||||
);
|
);
|
||||||
|
} catch(exeption) {
|
||||||
|
connecting.completeError(HAError("${exeption.toString()}"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return connecting.future;
|
return connecting.future;
|
||||||
}
|
}
|
||||||
@ -194,13 +214,14 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
_messageResolver.remove("${data["id"]}");
|
_messageResolver.remove("${data["id"]}");
|
||||||
} else if (data["type"] == "event") {
|
} else if (data["type"] == "event") {
|
||||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
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"]}");
|
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||||
onStateChangeCallback(data["event"]["data"]);
|
onStateChangeCallback(data["event"]["data"]);
|
||||||
} else if (data["event"] != null) {
|
} else if (data["event"]["event_type"] == "lovelace_updated") {
|
||||||
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
|
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: $data");
|
||||||
} else {
|
onLovelaceUpdatedCallback();
|
||||||
Logger.e("Event is null: $data");
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.d("[Received unhandled] <== ${data.toString()}");
|
Logger.d("[Received unhandled] <== ${data.toString()}");
|
||||||
@ -209,38 +230,24 @@ class ConnectionManager {
|
|||||||
|
|
||||||
void _handleSocketClose(Completer connectionCompleter) {
|
void _handleSocketClose(Completer connectionCompleter) {
|
||||||
Logger.d("Socket disconnected.");
|
Logger.d("Socket disconnected.");
|
||||||
|
_disconnect().then((_) {
|
||||||
if (!connectionCompleter.isCompleted) {
|
if (!connectionCompleter.isCompleted) {
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||||
} else {
|
}
|
||||||
_disconnect().then((_) {
|
|
||||||
Timer(Duration(seconds: 5), () {
|
|
||||||
Logger.d("Trying to reconnect...");
|
|
||||||
_connect().catchError((e) {
|
|
||||||
isConnected = false;
|
|
||||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
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");
|
||||||
|
_disconnect().then((_) {
|
||||||
if (!connectionCompleter.isCompleted) {
|
if (!connectionCompleter.isCompleted) {
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
|
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||||
} else {
|
}
|
||||||
_disconnect().then((_) {
|
|
||||||
Timer(Duration(seconds: 5), () {
|
|
||||||
Logger.d("Trying to reconnect...");
|
|
||||||
_connect().catchError((e) {
|
|
||||||
isConnected = false;
|
|
||||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _authenticate() {
|
Future _authenticate() {
|
||||||
@ -329,13 +336,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 +355,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 +370,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 +421,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",
|
"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,13 +104,12 @@ 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 {
|
||||||
Logger.d("[Foreground location] Started");
|
Logger.d("[Foreground location] Started");
|
||||||
//Logger.d("[Foreground location] Forcing Android location manager...");
|
Geolocator geolocator = Geolocator();
|
||||||
Geolocator geolocator = Geolocator()..forceAndroidLocationManager = true;
|
|
||||||
var battery = Battery();
|
var battery = Battery();
|
||||||
String webhookId = ConnectionManager().webhookId;
|
String webhookId = ConnectionManager().webhookId;
|
||||||
String httpWebHost = ConnectionManager().httpWebHost;
|
String httpWebHost = ConnectionManager().httpWebHost;
|
||||||
@ -149,15 +148,19 @@ 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");
|
||||||
Geolocator geolocator = Geolocator()..forceAndroidLocationManager = true;
|
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";
|
||||||
@ -166,42 +169,69 @@ void updateDeviceLocationIsolate() {
|
|||||||
"data": {
|
"data": {
|
||||||
"gps": [],
|
"gps": [],
|
||||||
"gps_accuracy": 0,
|
"gps_accuracy": 0,
|
||||||
"battery": batteryLevel
|
"battery": 100
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
//print("[Background $backgroundTask] Getting battery level...");
|
//print("[Background $backgroundTask] Getting battery level...");
|
||||||
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){
|
int batteryLevel;
|
||||||
//print("[Background $backgroundTask] Getting device location...");
|
try {
|
||||||
geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways).then((location) {
|
batteryLevel = await battery.batteryLevel;
|
||||||
//print("[Background $backgroundTask] Got location: ${location.latitude} ${location.longitude}");
|
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
|
||||||
if (location != null) {
|
} catch(e) {
|
||||||
|
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
|
||||||
|
batteryLevel = 0;
|
||||||
|
//logData += 'Battery: error, $e';
|
||||||
|
}
|
||||||
|
if (batteryLevel != null) {
|
||||||
|
data["data"]["battery"] = batteryLevel;
|
||||||
|
//logData += 'Battery: success, $batteryLevel';
|
||||||
|
}/* else {
|
||||||
|
logData += 'Battery: error, level is null';
|
||||||
|
}*/
|
||||||
|
Position location;
|
||||||
|
try {
|
||||||
|
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
|
||||||
|
if (location != null && location.latitude != null) {
|
||||||
|
//logData += ' || Location: success, ${location.latitude} ${location.longitude} (${location.timestamp})';
|
||||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||||
data["data"]["gps_accuracy"] = location.accuracy;
|
data["data"]["gps_accuracy"] = location.accuracy;
|
||||||
//print("[Background $backgroundTask] Sending data home...");
|
try {
|
||||||
http.post(
|
http.Response response = await http.post(
|
||||||
url,
|
url,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: json.encode(data)
|
body: json.encode(data)
|
||||||
);
|
);
|
||||||
|
/*if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
|
logData += ' || Post: success, ${response.statusCode}';
|
||||||
|
} else {
|
||||||
|
logData += ' || Post: error, ${response.statusCode}';
|
||||||
|
}*/
|
||||||
|
} catch(e) {
|
||||||
|
//logData += ' || Post: error, $e';
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}/* else {
|
||||||
//print("[Background $backgroundTask] Error getting current location: ${e.toString()}. Trying last known...");
|
logData += ' || Location: error, location is null';
|
||||||
geolocator.getLastKnownPosition(desiredAccuracy: LocationAccuracy.medium).then((location){
|
}*/
|
||||||
//print("[Background $backgroundTask] Got last known location: ${location.latitude} ${location.longitude}");
|
} catch (e) {
|
||||||
if (location != null) {
|
//print("[Background $backgroundTask] Location error: $e");
|
||||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
//logData += ' || Location: error, $e';
|
||||||
data["data"]["gps_accuracy"] = location.accuracy;
|
|
||||||
//print("[Background $backgroundTask] Sending data home...");
|
|
||||||
http.post(
|
|
||||||
url,
|
|
||||||
headers: headers,
|
|
||||||
body: json.encode(data)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
}/* else {
|
||||||
});
|
logData += 'Not configured';
|
||||||
});
|
}*/
|
||||||
}
|
//print("[Background $backgroundTask] Writing log data...");
|
||||||
return Future.value(true);
|
/*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");
|
||||||
|
}
|
||||||
|
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-887";
|
||||||
|
|
||||||
void checkMessagesToShow() async {
|
void checkMessagesToShow() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
223
lib/managers/theme_manager.dart
Normal file
223
lib/managers/theme_manager.dart
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class HAClientTheme {
|
||||||
|
|
||||||
|
static const TextTheme textTheme = TextTheme(
|
||||||
|
display1: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
|
||||||
|
display2: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
|
||||||
|
headline: TextStyle(fontSize: 24, fontWeight: FontWeight.normal),
|
||||||
|
title: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
|
||||||
|
subhead: TextStyle(fontSize: 16, fontWeight: FontWeight.normal),
|
||||||
|
body1: TextStyle(fontSize: 15, fontWeight: FontWeight.normal),
|
||||||
|
body2: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||||
|
subtitle: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||||
|
caption: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
|
||||||
|
overline: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
button: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
|
);
|
||||||
|
|
||||||
|
static const offEntityStates = [
|
||||||
|
EntityState.off,
|
||||||
|
EntityState.closed,
|
||||||
|
"below_horizon",
|
||||||
|
"default",
|
||||||
|
EntityState.idle,
|
||||||
|
EntityState.alarm_disarmed,
|
||||||
|
];
|
||||||
|
|
||||||
|
static const onEntityStates = [
|
||||||
|
EntityState.on,
|
||||||
|
"auto",
|
||||||
|
EntityState.active,
|
||||||
|
EntityState.playing,
|
||||||
|
EntityState.paused,
|
||||||
|
"above_horizon",
|
||||||
|
EntityState.home,
|
||||||
|
EntityState.open,
|
||||||
|
EntityState.cleaning,
|
||||||
|
EntityState.returning,
|
||||||
|
"cool",
|
||||||
|
EntityState.alarm_arming,
|
||||||
|
EntityState.alarm_disarming,
|
||||||
|
EntityState.alarm_pending,
|
||||||
|
];
|
||||||
|
|
||||||
|
static const disabledEntityStates = [
|
||||||
|
EntityState.unavailable,
|
||||||
|
EntityState.unknown,
|
||||||
|
];
|
||||||
|
|
||||||
|
static const alarmEntityStates = [
|
||||||
|
EntityState.alarm_armed_away,
|
||||||
|
EntityState.alarm_armed_custom_bypass,
|
||||||
|
EntityState.alarm_armed_home,
|
||||||
|
EntityState.alarm_armed_night,
|
||||||
|
EntityState.alarm_triggered,
|
||||||
|
"heat",
|
||||||
|
];
|
||||||
|
|
||||||
|
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
|
||||||
|
|
||||||
|
static const badgeColors = {
|
||||||
|
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||||
|
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
static final HAClientTheme _instance = HAClientTheme
|
||||||
|
._internal();
|
||||||
|
|
||||||
|
factory HAClientTheme() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
HAClientTheme._internal();
|
||||||
|
|
||||||
|
final ThemeData lightTheme = ThemeData.from(
|
||||||
|
colorScheme: ColorScheme(
|
||||||
|
primary: Color.fromRGBO(112, 154, 193, 1),
|
||||||
|
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
||||||
|
secondary: Color.fromRGBO(253, 216, 53, 1),
|
||||||
|
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
||||||
|
background: Color.fromRGBO(250, 250, 250, 1),
|
||||||
|
surface: Colors.white,
|
||||||
|
error: Colors.red,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSecondary: Colors.black87,
|
||||||
|
onBackground: Colors.black87,
|
||||||
|
onSurface: Colors.black87,
|
||||||
|
onError: Colors.white,
|
||||||
|
brightness: Brightness.light
|
||||||
|
),
|
||||||
|
textTheme: ThemeData.light().textTheme.copyWith(
|
||||||
|
display1: textTheme.display1.copyWith(color: Colors.black54),
|
||||||
|
display2: textTheme.display2.copyWith(color: Colors.redAccent),
|
||||||
|
headline: textTheme.headline.copyWith(color: Colors.black87),
|
||||||
|
title: textTheme.title.copyWith(color: Colors.black87),
|
||||||
|
subhead: textTheme.subhead.copyWith(color: Colors.black54),
|
||||||
|
body1: textTheme.body1.copyWith(color: Colors.black87),
|
||||||
|
body2: textTheme.body2.copyWith(color: Colors.black87),
|
||||||
|
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
|
||||||
|
caption: textTheme.caption.copyWith(color: Colors.black45),
|
||||||
|
overline: textTheme.overline.copyWith(color: Colors.black26),
|
||||||
|
button: textTheme.button.copyWith(color: Colors.white),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
final ThemeData darkTheme = ThemeData.from(
|
||||||
|
colorScheme: ColorScheme(
|
||||||
|
primary: Color.fromRGBO(112, 154, 193, 1),
|
||||||
|
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
||||||
|
secondary: Color.fromRGBO(253, 216, 53, 1),
|
||||||
|
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
||||||
|
background: Color.fromRGBO(47, 49, 54, 1),
|
||||||
|
surface: Color.fromRGBO(54, 57, 63, 1),
|
||||||
|
error: Color.fromRGBO(183, 109, 109, 1),
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSecondary: Colors.black87,
|
||||||
|
onBackground: Color.fromRGBO(220, 221, 222, 1),
|
||||||
|
onSurface: Colors.white,
|
||||||
|
onError: Colors.white,
|
||||||
|
brightness: Brightness.dark
|
||||||
|
),
|
||||||
|
textTheme: textTheme
|
||||||
|
);
|
||||||
|
|
||||||
|
Color getOnStateColor(BuildContext context) {
|
||||||
|
return Theme.of(context).colorScheme.secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getOffStateColor(BuildContext context) {
|
||||||
|
return Theme.of(context).colorScheme.primaryVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getDisabledStateColor(BuildContext context) {
|
||||||
|
return Theme.of(context).disabledColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getAlertStateColor(BuildContext context) {
|
||||||
|
return Theme.of(context).colorScheme.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getColorByEntityState(String state, BuildContext context) {
|
||||||
|
if (onEntityStates.contains(state)) {
|
||||||
|
return getOnStateColor(context);
|
||||||
|
} else if (disabledEntityStates.contains(state)) {
|
||||||
|
return getDisabledStateColor(context);
|
||||||
|
} else if (alarmEntityStates.contains(state)) {
|
||||||
|
return getAlertStateColor(context);
|
||||||
|
} else {
|
||||||
|
return getOffStateColor(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getGreenGaugeColor() {
|
||||||
|
return Colors.green;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getYellowGaugeColor() {
|
||||||
|
return Colors.yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getRedGaugeColor() {
|
||||||
|
return Colors.red;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle getLinkTextStyle(BuildContext context) {
|
||||||
|
ThemeData theme = Theme.of(context);
|
||||||
|
return theme.textTheme.body1.copyWith(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle getActionTextStyle(BuildContext context) {
|
||||||
|
ThemeData theme = Theme.of(context);
|
||||||
|
return theme.textTheme.subhead.copyWith(
|
||||||
|
color: Colors.blue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getBadgeColor(String entityDomain) {
|
||||||
|
return badgeColors[entityDomain] ??
|
||||||
|
badgeColors["default"];
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getOnBadgeTextColor() {
|
||||||
|
return Colors.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
charts.Color chartHistoryStateColor(String state, int id, BuildContext context) {
|
||||||
|
Color c = getColorByEntityState(state, context);
|
||||||
|
if (c != null) {
|
||||||
|
return charts.Color(
|
||||||
|
r: c.red,
|
||||||
|
g: c.green,
|
||||||
|
b: c.blue,
|
||||||
|
a: c.alpha
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
double r = id.toDouble() % 10;
|
||||||
|
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color historyStateColor(String state, int id, BuildContext context) {
|
||||||
|
Color c = getColorByEntityState(state, context);
|
||||||
|
if (c != null) {
|
||||||
|
return c;
|
||||||
|
} else {
|
||||||
|
if (id > -1) {
|
||||||
|
double r = id.toDouble() % 10;
|
||||||
|
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||||
|
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
|
||||||
|
} else {
|
||||||
|
return getOnStateColor(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -115,15 +115,14 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
Text("Location tracking", style: Theme.of(context).textTheme.title),
|
||||||
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: Theme.of(context).textTheme.subhead.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
fontSize: 16,
|
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -153,21 +152,24 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
//Expanded(child: Container(),),
|
//Expanded(child: Container(),),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
child: Text("-", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
child: Text("-", style: Theme.of(context).textTheme.title),
|
||||||
onPressed: () => decLocationInterval(),
|
onPressed: () => decLocationInterval(),
|
||||||
),
|
),
|
||||||
Text("$_locationInterval", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
child: Text("+", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
child: Text("+", style: Theme.of(context).textTheme.title),
|
||||||
onPressed: () => incLocationInterval(),
|
onPressed: () => incLocationInterval(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Divider(),
|
Divider(),
|
||||||
Text("Integration status", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
Text("Integration status", style: Theme.of(context).textTheme.title),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Text("${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}"),
|
Text(
|
||||||
|
"${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}",
|
||||||
|
style: Theme.of(context).textTheme.subtitle,
|
||||||
|
),
|
||||||
Container(height: 6.0,),
|
Container(height: 6.0,),
|
||||||
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
|
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
|
||||||
//Divider(),
|
//Divider(),
|
||||||
@ -177,13 +179,13 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
RaisedButton(
|
RaisedButton(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onPressed: () => updateRegistration(),
|
onPressed: () => updateRegistration(),
|
||||||
child: Text("Check integration", style: TextStyle(color: Colors.white))
|
child: Text("Check integration", style: Theme.of(context).textTheme.button)
|
||||||
),
|
),
|
||||||
Container(width: 10.0,),
|
Container(width: 10.0,),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
color: Colors.redAccent,
|
color: Colors.redAccent,
|
||||||
onPressed: () => resetRegistration(),
|
onPressed: () => resetRegistration(),
|
||||||
child: Text("Reset integration", style: TextStyle(color: Colors.white))
|
child: Text("Reset integration", style: Theme.of(context).textTheme.button)
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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) {
|
||||||
|
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||||
|
_fetchData(useCache: true);
|
||||||
LocationManager();
|
LocationManager();
|
||||||
StartupUserMessagesManager().checkMessagesToShow();
|
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);
|
||||||
}
|
}
|
||||||
@ -372,11 +347,10 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
accountName: Text(HomeAssistant().userName),
|
accountName: Text(HomeAssistant().userName),
|
||||||
accountEmail: Text(HomeAssistant().locationName ?? ""),
|
accountEmail: Text(HomeAssistant().locationName ?? ""),
|
||||||
currentAccountPicture: CircleAvatar(
|
currentAccountPicture: CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(context).backgroundColor,
|
||||||
child: Text(
|
child: Text(
|
||||||
HomeAssistant().userAvatarText,
|
HomeAssistant().userAvatarText,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.display1
|
||||||
fontSize: 32.0
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -385,21 +359,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
HomeAssistant().panels.forEach((Panel panel) {
|
HomeAssistant().panels.forEach((Panel panel) {
|
||||||
if (!panel.isHidden) {
|
if (!panel.isHidden) {
|
||||||
menuItems.add(
|
menuItems.add(
|
||||||
new ListTile(
|
panel.getMenuItemWidget(context)
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
|
||||||
title: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text("${panel.title}"),
|
|
||||||
Container(width: 4.0,),
|
|
||||||
panel.isWebView ? Text("WEB", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
panel.handleOpen(context);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -456,29 +416,36 @@ 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:discord")),
|
||||||
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://discord.gg/nd6FZQ");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
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: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -488,13 +455,13 @@ 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",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -504,13 +471,13 @@ 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",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -537,13 +504,13 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
bool _showBottomBar = false;
|
bool _showBottomBar = false;
|
||||||
String _bottomBarText;
|
String _bottomBarText;
|
||||||
bool _bottomBarProgress;
|
bool _bottomBarProgress;
|
||||||
Color _bottomBarColor;
|
bool _bottomBarErrorColor;
|
||||||
Timer _bottomBarTimer;
|
Timer _bottomBarTimer;
|
||||||
|
|
||||||
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
||||||
_bottomBarTimer?.cancel();
|
_bottomBarTimer?.cancel();
|
||||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
||||||
_bottomBarColor = Colors.grey.shade50;
|
_bottomBarErrorColor = false;
|
||||||
setState(() {
|
setState(() {
|
||||||
_bottomBarText = message;
|
_bottomBarText = message;
|
||||||
_bottomBarProgress = progress;
|
_bottomBarProgress = progress;
|
||||||
@ -557,11 +524,10 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showErrorBottomBar(HAError error) {
|
void _showErrorBottomBar(HAError error) {
|
||||||
TextStyle textStyle = TextStyle(
|
TextStyle textStyle = Theme.of(context).textTheme.button.copyWith(
|
||||||
color: Colors.blue,
|
decoration: TextDecoration.underline
|
||||||
fontSize: Sizes.nameFontSize
|
|
||||||
);
|
);
|
||||||
_bottomBarColor = Colors.red.shade100;
|
_bottomBarErrorColor = true;
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
error.actions.forEach((HAErrorAction action) {
|
error.actions.forEach((HAErrorAction action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -639,6 +605,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,15 +627,15 @@ 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>(
|
||||||
child: Text(
|
child: Text(
|
||||||
"${entity.displayName}",
|
"${entity.displayName}",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: EntityColor.stateColor(entity.state)
|
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
||||||
),
|
)
|
||||||
),
|
),
|
||||||
value: "${entity.entityId}",
|
value: "${entity.entityId}",
|
||||||
)).toList()
|
)).toList()
|
||||||
@ -691,7 +664,12 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text("$playersCount", style: TextStyle(fontSize: 12)),
|
child: Text(
|
||||||
|
"$playersCount",
|
||||||
|
style: Theme.of(context).textTheme.caption.copyWith(
|
||||||
|
color: Colors.white
|
||||||
|
)
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -709,7 +687,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
child: Text("Login with Home Assistant", style: Theme.of(context).textTheme.button),
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onPressed: () => _fullLoad(),
|
onPressed: () => _fullLoad(),
|
||||||
)
|
)
|
||||||
@ -732,7 +710,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 +725,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 +763,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((_) {
|
||||||
@ -842,16 +822,16 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
bottomBarChildren.add(
|
bottomBarChildren.add(
|
||||||
CollectionScaleTransition(
|
CollectionScaleTransition(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
|
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOnStateColor(context),),
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
|
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getDisabledStateColor(context),),
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
|
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOffStateColor(context),),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (bottomBarChildren.isNotEmpty) {
|
if (bottomBarChildren.isNotEmpty) {
|
||||||
bottomBar = Container(
|
bottomBar = Container(
|
||||||
color: _bottomBarColor,
|
color: _bottomBarErrorColor ? Theme.of(context).errorColor : Theme.of(context).primaryColorLight,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -868,7 +848,6 @@ 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,
|
||||||
@ -901,8 +880,11 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
|||||||
@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 +892,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;
|
||||||
@ -135,7 +135,9 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
if (_validationMessage.isNotEmpty) {
|
if (_validationMessage.isNotEmpty) {
|
||||||
children.add(Text(
|
children.add(Text(
|
||||||
"$_validationMessage",
|
"$_validationMessage",
|
||||||
style: TextStyle(color: Colors.red)
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
color: Theme.of(context).errorColor
|
||||||
|
)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
children.addAll(<Widget>[
|
children.addAll(<Widget>[
|
||||||
@ -193,7 +195,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"How?",
|
"How?",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline
|
||||||
),
|
),
|
||||||
|
@ -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) {
|
||||||
@ -132,10 +138,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"Connection settings",
|
"Connection settings",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.headline,
|
||||||
color: Colors.black45,
|
|
||||||
fontSize: 20.0
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
new Row(
|
new Row(
|
||||||
children: [
|
children: [
|
||||||
@ -170,16 +173,13 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
),
|
),
|
||||||
new Text(
|
new Text(
|
||||||
"Try ports 80 and 443 if default is not working and you don't know why.",
|
"Try ports 80 and 443 if default is not working and you don't know why.",
|
||||||
style: TextStyle(color: Colors.grey),
|
style: Theme.of(context).textTheme.caption,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 20.0),
|
padding: EdgeInsets.only(top: 20.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"UI",
|
"UI",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.headline,
|
||||||
color: Colors.black45,
|
|
||||||
fontSize: 20.0
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
new Row(
|
new Row(
|
||||||
@ -197,15 +197,14 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"Authentication settings",
|
"Authentication settings",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.headline,
|
||||||
color: Colors.black45,
|
|
||||||
fontSize: 20.0
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Container(height: 10.0,),
|
Container(height: 10.0,),
|
||||||
Text(
|
Text(
|
||||||
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
|
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
|
||||||
style: TextStyle(color: Colors.redAccent),
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
color: Colors.redAccent
|
||||||
|
),
|
||||||
),
|
),
|
||||||
new TextField(
|
new TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
@ -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.2.md");
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
setState(() {
|
setState(() {
|
||||||
data = response.body;
|
data = response.body;
|
||||||
|
@ -10,8 +10,7 @@ class LastUpdatedWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'${entityModel.entityWrapper.entity.lastUpdated}',
|
'${entityModel.entityWrapper.entity.lastUpdated}',
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.caption
|
||||||
fontSize: Sizes.smallFontSize, color: Colors.black26),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ class PageLoadingError extends StatelessWidget {
|
|||||||
size: 48.0
|
size: 48.0
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
Text(this.errorText, style: TextStyle(color: Colors.black45))
|
Text(this.errorText, style: Theme.of(context).textTheme.subtitle)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -14,7 +14,7 @@ class PageLoadingIndicator extends StatelessWidget {
|
|||||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||||
child: CircularProgressIndicator()
|
child: CircularProgressIndicator()
|
||||||
),
|
),
|
||||||
Text("Loading...", style: TextStyle(color: Colors.black45))
|
Text("Loading...", style: Theme.of(context).textTheme.subtitle)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -40,10 +40,7 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"${product.title}",
|
"${product.title}",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body2,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16.0
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Text(
|
Text(
|
||||||
@ -53,7 +50,9 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
softWrap: true,
|
softWrap: true,
|
||||||
),
|
),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Text("${product.price} $period", style: TextStyle(color: priceColor)),
|
Text("${product.price} $period", style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
color: priceColor
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -61,7 +60,7 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: RaisedButton(
|
child: RaisedButton(
|
||||||
child: Text(this.purchased ? buttonTextInactive : buttonText, style: TextStyle(color: Colors.white)),
|
child: Text(this.purchased ? buttonTextInactive : buttonText, style: Theme.of(context).textTheme.button),
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
||||||
),
|
),
|
||||||
|
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: Theme.of(context).textTheme.overline) : 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,10 @@ class LinkToWebConfig extends StatelessWidget {
|
|||||||
title: Text("${this.name}",
|
title: Text("${this.name}",
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
style: Theme.of(context).textTheme.headline),
|
||||||
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);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -156,7 +156,8 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
result.add(
|
result.add(
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: "value",
|
id: "value",
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor("_", historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor("_", historyMoment.colorId, context),
|
||||||
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
|
||||||
if (historyMoment.hiddenDot) {
|
if (historyMoment.hiddenDot) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@ -179,7 +180,8 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'state',
|
id: 'state',
|
||||||
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0,
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0,
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
||||||
|
@ -28,7 +28,7 @@ class HistoryControlWidget extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(right: 10.0),
|
padding: EdgeInsets.only(right: 10.0),
|
||||||
child: _buildStates(),
|
child: _buildStates(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildTime(),
|
_buildTime(),
|
||||||
@ -46,18 +46,16 @@ class HistoryControlWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStates() {
|
Widget _buildStates(BuildContext context) {
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
for (int i = 0; i < selectedStates.length; i++) {
|
for (int i = 0; i < selectedStates.length; i++) {
|
||||||
children.add(
|
children.add(
|
||||||
Text(
|
Text(
|
||||||
"${selectedStates[i] ?? '-'}",
|
"${selectedStates[i] ?? '-'}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.title.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
color: HAClientTheme().historyStateColor(selectedStates[i], colorIndexes[i], context)
|
||||||
color: EntityColor.historyStateColor(selectedStates[i], colorIndexes[i]),
|
)
|
||||||
fontSize: 22.0
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,8 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
return [
|
return [
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'State',
|
id: 'State',
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(EntityState.on, -1),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(EntityState.on, -1, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
|
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -107,7 +107,8 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'State',
|
id: 'State',
|
||||||
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0,
|
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0,
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
||||||
data: data,
|
data: data,
|
||||||
@ -115,7 +116,8 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'State',
|
id: 'State',
|
||||||
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
||||||
data: data,
|
data: data,
|
||||||
@ -123,7 +125,8 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'State',
|
id: 'State',
|
||||||
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
||||||
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -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 ?? ''}"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -23,6 +23,10 @@ class Logger {
|
|||||||
return inDebugMode;
|
return inDebugMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void p(data) {
|
||||||
|
print(data);
|
||||||
|
}
|
||||||
|
|
||||||
static void e(String message) {
|
static void e(String message) {
|
||||||
_writeToLog("Error", message);
|
_writeToLog("Error", message);
|
||||||
}
|
}
|
||||||
|
@ -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 (childEntities != null) {
|
|
||||||
_fillView(childEntities);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _fillView(List<Entity> childEntities) {
|
if (rawData['badges'] != null && rawData['badges'] is List) {
|
||||||
List<HACard> autoGeneratedCards = [];
|
rawData['badges'].forEach((entity) {
|
||||||
badges.addAll(childEntities.where((entity){ return entity.isBadge;}));
|
if (entity is String) {
|
||||||
childEntities.where((entity){ return entity.domain == "media_player";}).forEach((e){
|
if (HomeAssistant().entities.isExist(entity)) {
|
||||||
HACard card = HACard(
|
Entity e = HomeAssistant().entities.get(entity);
|
||||||
name: e.displayName,
|
badges.add(e);
|
||||||
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 {
|
} else {
|
||||||
HACard card = HACard(
|
String eId = '${entity['entity']}';
|
||||||
name: entity.displayName,
|
if (HomeAssistant().entities.isExist(eId)) {
|
||||||
id: entity.entityId,
|
Entity e = HomeAssistant().entities.get(eId);
|
||||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
badges.add(e);
|
||||||
type: CardType.ENTITIES
|
}
|
||||||
);
|
|
||||||
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
|
|
||||||
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
|
|
||||||
HACard mediaCard = HACard(
|
|
||||||
name: entity.displayName,
|
|
||||||
id: entity.entityId,
|
|
||||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
|
||||||
type: CardType.MEDIA_CONTROL
|
|
||||||
);
|
|
||||||
cards.add(mediaCard);
|
|
||||||
});
|
|
||||||
cards.add(card);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cards.addAll(autoGeneratedCards);
|
}
|
||||||
|
|
||||||
|
cards.addAll(_createLovelaceCards(rawData["cards"] ?? []));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HACard> _createLovelaceCards(List rawCards) {
|
||||||
|
List<HACard> result = [];
|
||||||
|
rawCards.forEach((rawCard){
|
||||||
|
try {
|
||||||
|
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||||
|
var rawCardInfo = rawCard["card"] ?? rawCard;
|
||||||
|
HACard card = HACard(
|
||||||
|
id: "card",
|
||||||
|
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||||
|
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||||
|
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||||
|
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
|
||||||
|
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']
|
||||||
|
);
|
||||||
|
if (rawCardInfo["cards"] != null) {
|
||||||
|
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||||
|
}
|
||||||
|
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
|
||||||
|
rawEntities?.forEach((rawEntity) {
|
||||||
|
if (rawEntity is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||||
|
card.entities.add(EntityWrapper(entity: HomeAssistant().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 (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"])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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()}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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(),
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
47
pubspec.yaml
47
pubspec.yaml
@ -1,37 +1,40 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.7.4+740
|
version: 0.8.2+887
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
share:
|
firebase_crashlytics: ^0.1.3+3
|
||||||
git:
|
syncfusion_flutter_core: ^18.1.43
|
||||||
url: https://github.com/d-silveira/flutter-share.git
|
syncfusion_flutter_gauges: ^18.1.43
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -50,6 +53,8 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- images/hassio-192x192.png
|
- images/hassio-192x192.png
|
||||||
- assets/js/externalAuth.js
|
- assets/js/externalAuth.js
|
||||||
|
- assets/html/cameraView.html
|
||||||
|
- assets/html/cameraLiveView.html
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: "Material Design Icons"
|
- family: "Material Design Icons"
|
||||||
|
11
tool/secrets.dart
Normal file
11
tool/secrets.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
final config = {
|
||||||
|
'syncfusion_license_key': Platform.environment['SYNCFUSION_LICENSE_KEY'],
|
||||||
|
};
|
||||||
|
|
||||||
|
final filename = 'lib/.secrets.dart';
|
||||||
|
File(filename).writeAsString('final secrets = ${json.encode(config)};');
|
||||||
|
}
|
Reference in New Issue
Block a user