Compare commits
1 Commits
beta/0.8.5
...
beta/0.7.1
Author | SHA1 | Date | |
---|---|---|---|
3234ffc20c |
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,16 +7,35 @@ 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:**
|
**Home Assistant version:** <!-- 0.94.1 for example -->
|
||||||
|
|
||||||
**Device name:**
|
**Device name:** <!-- Pixel 2 for example -->
|
||||||
|
|
||||||
**Android version:**
|
**Android version:** <!-- 8.1 for example -->
|
||||||
|
|
||||||
|
**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,8 +15,7 @@ build/
|
|||||||
.settings/
|
.settings/
|
||||||
|
|
||||||
flutter_export_environment.sh
|
flutter_export_environment.sh
|
||||||
.flutter-plugins-dependencies
|
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
.secrets.dart
|
premium_features_manager.class.dart
|
||||||
pubspec.lock
|
pubspec.lock
|
@ -4,5 +4,9 @@ ENV ANDROID_HOME=/workspace/android-sdk \
|
|||||||
FLUTTER_ROOT=/workspace/flutter \
|
FLUTTER_ROOT=/workspace/flutter \
|
||||||
FLUTTER_HOME=/workspace/flutter
|
FLUTTER_HOME=/workspace/flutter
|
||||||
|
|
||||||
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
|
USER root
|
||||||
&& 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:$FLUTTER_HOME/bin/cache/dart-sdk/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
export PATH=$FLUTTER_HOME/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.12.13+hotfix.7-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.9.1+hotfix.4-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,6 +18,7 @@ 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:
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -1,14 +1,13 @@
|
|||||||
|
[](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 [ha-client.app](http://ha-client.app/) for more info.
|
Visit [homemade.systems](http://ha-client.homemade.systems/) 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 on [Discord](https://discord.gg/nd6FZQ) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
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)
|
||||||
|
|
||||||
[](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,11 +78,10 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
implementation 'com.google.firebase:firebase-core:16.0.8'
|
||||||
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'
|
||||||
|
@ -3,13 +3,17 @@
|
|||||||
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen"
|
<uses-feature android:name="android.hardware.touchscreen"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
flutter needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
<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
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
@ -17,15 +21,11 @@
|
|||||||
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="io.flutter.app.FlutterApplication"
|
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" />
|
||||||
@ -37,12 +37,13 @@
|
|||||||
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.embedding.android.SplashScreenDrawable"
|
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||||
android:resource="@drawable/launch_background" />
|
android:value="true" />-->
|
||||||
<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" />
|
||||||
@ -51,6 +52,14 @@
|
|||||||
<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
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
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 androidx.annotation.NonNull;
|
import android.os.Bundle;
|
||||||
import io.flutter.embedding.android.FlutterActivity;
|
import io.flutter.app.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 FlutterActivity {
|
public class MainActivity extends FlutterShareReceiverActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
super.onCreate(savedInstanceState);
|
||||||
|
GeneratedPluginRegistrant.registerWith(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,4 @@
|
|||||||
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,15 +2,11 @@ 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.3.3'
|
classpath 'com.google.gms:google-services:4.2.0'
|
||||||
classpath 'io.fabric.tools:gradle:1.26.1'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,9 +14,6 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
maven {
|
|
||||||
url 'https://maven.fabric.io/public'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,4 +3,3 @@ 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 +0,0 @@
|
|||||||
include ':app'
|
|
@ -1,61 +0,0 @@
|
|||||||
<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>
|
|
@ -1,28 +0,0 @@
|
|||||||
<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,22 +14,3 @@ 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');
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,62 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class AlarmPanelCard extends StatelessWidget {
|
|
||||||
final AlarmPanelCardData card;
|
|
||||||
|
|
||||||
const AlarmPanelCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: MissedEntityWidget(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
List<Widget> body = [];
|
|
||||||
body.add(CardHeader(
|
|
||||||
name: card.name ?? "",
|
|
||||||
subtitle: Text("${card.entity.entity.displayState}",
|
|
||||||
),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
EntityIcon(
|
|
||||||
size: 50.0,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 26.0,
|
|
||||||
child: IconButton(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
"mdi:dots-vertical")),
|
|
||||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.entity.entity))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
));
|
|
||||||
body.add(
|
|
||||||
AlarmControlPanelControlsWidget(
|
|
||||||
extended: true,
|
|
||||||
states: card.states,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return CardWrapper(
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
handleTap: null,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: body
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,510 +1,64 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class CardData {
|
class HACard {
|
||||||
|
|
||||||
String type;
|
|
||||||
List<EntityWrapper> entities = [];
|
List<EntityWrapper> entities = [];
|
||||||
List conditions;
|
List<HACard> childCards = [];
|
||||||
bool showEmpty;
|
EntityWrapper linkedEntityWrapper;
|
||||||
List stateFilter;
|
|
||||||
bool stateColor = true;
|
|
||||||
|
|
||||||
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
|
|
||||||
|
|
||||||
factory CardData.parse(Map<String, dynamic> rawData) {
|
|
||||||
switch (rawData['type']) {
|
|
||||||
case CardType.ENTITIES:
|
|
||||||
return EntitiesCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.ALARM_PANEL:
|
|
||||||
return AlarmPanelCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.BUTTON:
|
|
||||||
return ButtonCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.ENTITY_BUTTON:
|
|
||||||
return ButtonCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.CONDITIONAL:
|
|
||||||
return CardData.parse(rawData['card']);
|
|
||||||
break;
|
|
||||||
case CardType.ENTITY_FILTER:
|
|
||||||
Map<String, dynamic> cardData = Map.from(rawData);
|
|
||||||
cardData.remove('type');
|
|
||||||
if (rawData.containsKey('card')) {
|
|
||||||
cardData.addAll(rawData['card']);
|
|
||||||
}
|
|
||||||
cardData['type'] ??= CardType.ENTITIES;
|
|
||||||
return CardData.parse(cardData);
|
|
||||||
break;
|
|
||||||
case CardType.GAUGE:
|
|
||||||
return GaugeCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.GLANCE:
|
|
||||||
return GlanceCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.HORIZONTAL_STACK:
|
|
||||||
return HorizontalStackCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.VERTICAL_STACK:
|
|
||||||
return VerticalStackCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.MARKDOWN:
|
|
||||||
return MarkdownCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.MEDIA_CONTROL:
|
|
||||||
return MediaControlCardData(rawData);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (rawData.containsKey('entities')) {
|
|
||||||
return EntitiesCardData(rawData);
|
|
||||||
} else if (rawData.containsKey('entity')) {
|
|
||||||
rawData['entities'] = [rawData['entity']];
|
|
||||||
return EntitiesCardData(rawData);
|
|
||||||
}
|
|
||||||
return CardData(rawData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CardData(Map<String, dynamic> rawData) {
|
|
||||||
type = rawData['type'] ?? CardType.ENTITIES;
|
|
||||||
conditions = rawData['conditions'] ?? [];
|
|
||||||
showEmpty = rawData['show_empty'] ?? true;
|
|
||||||
stateFilter = rawData['state_filter'] ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return UnsupportedCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<EntityWrapper> getEntitiesToShow() {
|
|
||||||
return entities.where((entityWrapper) {
|
|
||||||
if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
List currentStateFilter;
|
|
||||||
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
|
|
||||||
currentStateFilter = entityWrapper.stateFilter;
|
|
||||||
} else {
|
|
||||||
currentStateFilter = stateFilter;
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class EntitiesCardData extends CardData {
|
|
||||||
|
|
||||||
String title;
|
|
||||||
String icon;
|
|
||||||
bool showHeaderToggle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return EntitiesCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
EntitiesCardData(Map<String, dynamic> rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
title = rawData["title"];
|
|
||||||
icon = rawData['icon'];
|
|
||||||
stateColor = rawData['state_color'] ?? false;
|
|
||||||
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
|
||||||
//Parsing entities
|
|
||||||
var rawEntities = rawData["entities"] ?? [];
|
|
||||||
rawEntities.forEach((rawEntity) {
|
|
||||||
if (rawEntity is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
|
||||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rawEntity["type"] == "divider") {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.divider()));
|
|
||||||
} else if (rawEntity["type"] == "section") {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
|
||||||
} else if (rawEntity["type"] == "call-service") {
|
|
||||||
Map uiActionData = {
|
|
||||||
"tap_action": {
|
|
||||||
"action": EntityUIAction.callService,
|
|
||||||
"service": rawEntity["service"],
|
|
||||||
"service_data": rawEntity["service_data"]
|
|
||||||
},
|
|
||||||
"hold_action": EntityUIAction.none
|
|
||||||
};
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: Entity.callService(
|
|
||||||
icon: rawEntity["icon"],
|
|
||||||
name: rawEntity["name"],
|
|
||||||
service: rawEntity["service"],
|
|
||||||
actionName: rawEntity["action_name"]
|
|
||||||
),
|
|
||||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
|
||||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (rawEntity["type"] == "weblink") {
|
|
||||||
Map uiActionData = {
|
|
||||||
"tap_action": {
|
|
||||||
"action": EntityUIAction.navigate,
|
|
||||||
"service": rawEntity["url"]
|
|
||||||
},
|
|
||||||
"hold_action": EntityUIAction.none
|
|
||||||
};
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: Entity.weblink(
|
|
||||||
icon: rawEntity["icon"],
|
|
||||||
name: rawEntity["name"],
|
|
||||||
url: rawEntity["url"]
|
|
||||||
),
|
|
||||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
|
||||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
|
||||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
|
||||||
overrideName: rawEntity["name"],
|
|
||||||
overrideIcon: rawEntity["icon"],
|
|
||||||
stateFilter: rawEntity['state_filter'] ?? [],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlarmPanelCardData extends CardData {
|
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
List<dynamic> states;
|
String id;
|
||||||
|
String type;
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return AlarmPanelCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
AlarmPanelCardData(Map<String, dynamic> rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
name = rawData['name'];
|
|
||||||
states = rawData['states'];
|
|
||||||
//Parsing entity
|
|
||||||
var entitiId = rawData["entity"];
|
|
||||||
if (entitiId != null && entitiId is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(entitiId),
|
|
||||||
stateColor: true,
|
|
||||||
overrideName: name
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class ButtonCardData extends CardData {
|
|
||||||
|
|
||||||
String name;
|
|
||||||
String icon;
|
|
||||||
bool showName;
|
bool showName;
|
||||||
bool showIcon;
|
bool showState;
|
||||||
double iconHeightPx = 0;
|
bool showEmpty;
|
||||||
double iconHeightRem = 0;
|
int columnsCount;
|
||||||
|
List stateFilter;
|
||||||
@override
|
List states;
|
||||||
Widget buildCardWidget() {
|
List conditions;
|
||||||
return EntityButtonCard(card: this);
|
String content;
|
||||||
}
|
|
||||||
|
|
||||||
ButtonCardData(Map<String, dynamic> rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
name = rawData['name'];
|
|
||||||
icon = rawData['icon'];
|
|
||||||
showName = rawData['show_name'] ?? true;
|
|
||||||
showIcon = rawData['show_icon'] ?? true;
|
|
||||||
stateColor = rawData['state_color'] ?? true;
|
|
||||||
var rawHeight = rawData['icon_height'];
|
|
||||||
if (rawHeight != null && rawHeight is String) {
|
|
||||||
if (rawHeight.contains('px')) {
|
|
||||||
iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0;
|
|
||||||
} else if (rawHeight.contains('rem')) {
|
|
||||||
iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;
|
|
||||||
} else if (rawHeight.contains('em')) {
|
|
||||||
iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Parsing entity
|
|
||||||
var entitiId = rawData["entity"];
|
|
||||||
if (entitiId != null && entitiId is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(entitiId),
|
|
||||||
overrideName: name,
|
|
||||||
overrideIcon: icon,
|
|
||||||
stateColor: stateColor,
|
|
||||||
uiAction: EntityUIAction(
|
|
||||||
rawEntityData: rawData
|
|
||||||
)
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
|
||||||
}
|
|
||||||
} else if (entitiId == null) {
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: Entity.ghost(
|
|
||||||
name,
|
|
||||||
icon,
|
|
||||||
),
|
|
||||||
stateColor: stateColor,
|
|
||||||
uiAction: EntityUIAction(
|
|
||||||
rawEntityData: rawData
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GaugeCardData extends CardData {
|
|
||||||
|
|
||||||
String name;
|
|
||||||
String unit;
|
String unit;
|
||||||
int min;
|
int min;
|
||||||
int max;
|
int max;
|
||||||
Map severity;
|
Map severity;
|
||||||
|
|
||||||
@override
|
HACard({
|
||||||
Widget buildCardWidget() {
|
this.name,
|
||||||
return GaugeCard(card: this);
|
this.id,
|
||||||
|
this.linkedEntityWrapper,
|
||||||
|
this.columnsCount: 4,
|
||||||
|
this.showName: true,
|
||||||
|
this.showState: true,
|
||||||
|
this.stateFilter: const [],
|
||||||
|
this.showEmpty: true,
|
||||||
|
this.content,
|
||||||
|
this.states,
|
||||||
|
this.conditions: const [],
|
||||||
|
this.unit,
|
||||||
|
this.min,
|
||||||
|
this.max,
|
||||||
|
this.severity,
|
||||||
|
@required this.type
|
||||||
|
}) {
|
||||||
|
if (this.columnsCount <= 0) {
|
||||||
|
this.columnsCount = 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GaugeCardData(Map<String, dynamic> rawData) : super(rawData) {
|
List<EntityWrapper> getEntitiesToShow() {
|
||||||
//Parsing card data
|
return entities.where((entityWrapper) {
|
||||||
name = rawData['name'];
|
if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) {
|
||||||
unit = rawData['unit'];
|
return false;
|
||||||
min = rawData['min'] ?? 0;
|
|
||||||
max = rawData['max'] ?? 100;
|
|
||||||
severity = rawData['severity'];
|
|
||||||
//Parsing entity
|
|
||||||
var entitiId = rawData["entity"];
|
|
||||||
if (entitiId != null && entitiId is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(entitiId),
|
|
||||||
overrideName: name
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
|
||||||
}
|
}
|
||||||
} else {
|
if (stateFilter.isNotEmpty) {
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
return stateFilter.contains(entityWrapper.entity.state);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
Widget build(BuildContext context) {
|
||||||
|
return CardWidget(
|
||||||
}
|
card: this,
|
||||||
|
|
||||||
class GlanceCardData extends CardData {
|
|
||||||
|
|
||||||
String title;
|
|
||||||
bool showName;
|
|
||||||
bool showIcon;
|
|
||||||
bool showState;
|
|
||||||
bool stateColor;
|
|
||||||
int columnsCount;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return GlanceCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
GlanceCardData(Map<String, dynamic> rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
title = rawData["title"];
|
|
||||||
showName = rawData['show_name'] ?? true;
|
|
||||||
showIcon = rawData['show_icon'] ?? true;
|
|
||||||
showState = rawData['show_state'] ?? true;
|
|
||||||
stateColor = rawData['state_color'] ?? true;
|
|
||||||
columnsCount = rawData['columns'] ?? 4;
|
|
||||||
//Parsing entities
|
|
||||||
var rawEntities = rawData["entities"] ?? [];
|
|
||||||
rawEntities.forEach((rawEntity) {
|
|
||||||
if (rawEntity is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
|
||||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
|
||||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
stateColor: stateColor,
|
|
||||||
overrideName: rawEntity["name"],
|
|
||||||
overrideIcon: rawEntity["icon"],
|
|
||||||
stateFilter: rawEntity['state_filter'] ?? [],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class HorizontalStackCardData extends CardData {
|
|
||||||
|
|
||||||
List<CardData> childCards;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return HorizontalStackCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
|
||||||
if (rawData.containsKey('cards')) {
|
|
||||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
|
||||||
return CardData.parse(childCard);
|
|
||||||
}).toList();
|
|
||||||
} else {
|
|
||||||
childCards = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class VerticalStackCardData extends CardData {
|
|
||||||
|
|
||||||
List<CardData> childCards;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return VerticalStackCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
VerticalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
|
||||||
if (rawData.containsKey('cards')) {
|
|
||||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
|
||||||
return CardData.parse(childCard);
|
|
||||||
}).toList();
|
|
||||||
} else {
|
|
||||||
childCards = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class MarkdownCardData extends CardData {
|
|
||||||
|
|
||||||
String title;
|
|
||||||
String content;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return MarkdownCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
MarkdownCardData(Map<String, dynamic> rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
title = rawData['title'];
|
|
||||||
content = rawData['content'];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class MediaControlCardData extends CardData {
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return MediaControlsCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaControlCardData(Map<String, dynamic> rawData) : super(rawData) {
|
|
||||||
var entitiId = rawData["entity"];
|
|
||||||
if (entitiId != null && entitiId is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(entitiId),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
361
lib/cards/card_widget.dart
Normal file
361
lib/cards/card_widget.dart
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class CardWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final HACard card;
|
||||||
|
|
||||||
|
const CardWidget({
|
||||||
|
Key key,
|
||||||
|
this.card
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (card.linkedEntityWrapper!= null) {
|
||||||
|
if (card.linkedEntityWrapper.entity.isHidden) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: MissedEntityWidget(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (card.conditions.isNotEmpty) {
|
||||||
|
bool showCardByConditions = true;
|
||||||
|
for (var condition in card.conditions) {
|
||||||
|
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
||||||
|
if (conditionEntity != null &&
|
||||||
|
((condition['state'] != null && conditionEntity.state != condition['state']) ||
|
||||||
|
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
|
||||||
|
) {
|
||||||
|
showCardByConditions = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!showCardByConditions) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (card.type) {
|
||||||
|
|
||||||
|
case CardType.ENTITIES: {
|
||||||
|
return _buildEntitiesCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.GLANCE: {
|
||||||
|
return _buildGlanceCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.MEDIA_CONTROL: {
|
||||||
|
return _buildMediaControlsCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.ENTITY_BUTTON: {
|
||||||
|
return _buildEntityButtonCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.GAUGE: {
|
||||||
|
return _buildGaugeCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* case CardType.LIGHT: {
|
||||||
|
return _buildLightCard(context);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
case CardType.MARKDOWN: {
|
||||||
|
return _buildMarkdownCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.ALARM_PANEL: {
|
||||||
|
return _buildAlarmPanelCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.HORIZONTAL_STACK: {
|
||||||
|
if (card.childCards.isNotEmpty) {
|
||||||
|
List<Widget> children = [];
|
||||||
|
card.childCards.forEach((card) {
|
||||||
|
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
|
||||||
|
children.add(
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
child: card.build(context),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.VERTICAL_STACK: {
|
||||||
|
if (card.childCards.isNotEmpty) {
|
||||||
|
List<Widget> children = [];
|
||||||
|
card.childCards.forEach((card) {
|
||||||
|
children.add(
|
||||||
|
card.build(context)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
|
||||||
|
return _buildEntitiesCard(context);
|
||||||
|
} else {
|
||||||
|
return _buildUnsupportedCard(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntitiesCard(BuildContext context) {
|
||||||
|
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||||
|
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(name: card.name));
|
||||||
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
|
body.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
handleTap: true,
|
||||||
|
child: entity.entity.buildDefaultWidget(context)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMarkdownCard(BuildContext context) {
|
||||||
|
if (card.content == null) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(name: card.name));
|
||||||
|
body.add(MarkdownBody(data: card.content));
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(
|
||||||
|
name: card.name ?? "",
|
||||||
|
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
EntityIcon(
|
||||||
|
size: 50.0,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 26.0,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
"mdi:dots-vertical")),
|
||||||
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
));
|
||||||
|
body.add(
|
||||||
|
AlarmControlPanelControlsWidget(
|
||||||
|
extended: true,
|
||||||
|
states: card.states,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
handleTap: null,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: body
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGlanceCard(BuildContext context) {
|
||||||
|
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||||
|
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> rows = [];
|
||||||
|
rows.add(CardHeader(name: card.name));
|
||||||
|
|
||||||
|
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||||
|
|
||||||
|
rows.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
widthFactor: 1,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
List<Widget> buttons = [];
|
||||||
|
double buttonWidth = constraints.maxWidth / columnsCount;
|
||||||
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
|
buttons.add(
|
||||||
|
SizedBox(
|
||||||
|
width: buttonWidth,
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
child: GlanceCardEntityContainer(
|
||||||
|
showName: card.showName,
|
||||||
|
showState: card.showState,
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Wrap(
|
||||||
|
//spacing: 5.0,
|
||||||
|
//alignment: WrapAlignment.spaceEvenly,
|
||||||
|
runSpacing: Sizes.doubleRowPadding,
|
||||||
|
children: buttons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: rows
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMediaControlsCard(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
handleTap: null,
|
||||||
|
child: MediaPlayerWidget()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntityButtonCard(BuildContext context) {
|
||||||
|
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
||||||
|
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: EntityButtonCardBody(),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGaugeCard(BuildContext context) {
|
||||||
|
card.linkedEntityWrapper.displayName = card.name ??
|
||||||
|
card.linkedEntityWrapper.displayName;
|
||||||
|
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
|
||||||
|
card.linkedEntityWrapper.unitOfMeasurement;
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: GaugeCardBody(
|
||||||
|
min: card.min,
|
||||||
|
max: card.max,
|
||||||
|
severity: card.severity,
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLightCard(BuildContext context) {
|
||||||
|
card.linkedEntityWrapper.displayName = card.name ??
|
||||||
|
card.linkedEntityWrapper.displayName;
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: LightCardBody(
|
||||||
|
min: card.min,
|
||||||
|
max: card.max,
|
||||||
|
severity: card.severity,
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUnsupportedCard(BuildContext context) {
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(name: card.name ?? ""));
|
||||||
|
List<Widget> result = [];
|
||||||
|
if (card.linkedEntityWrapper != null) {
|
||||||
|
result.addAll(<Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
handleTap: true,
|
||||||
|
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
result.addAll(<Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Text("'${card.type}' card is not supported yet"),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
body.addAll(result);
|
||||||
|
return Card(
|
||||||
|
child: new Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: body
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,79 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntitiesCard extends StatelessWidget {
|
|
||||||
final EntitiesCardData card;
|
|
||||||
|
|
||||||
const EntitiesCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
|
||||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
List<Widget> body = [];
|
|
||||||
Widget headerSwitch;
|
|
||||||
if (card.showHeaderToggle) {
|
|
||||||
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
|
|
||||||
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
|
|
||||||
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
|
|
||||||
}).map((EntityWrapper en) {
|
|
||||||
return en.entity.entityId;
|
|
||||||
}).toList();
|
|
||||||
headerSwitch = Switch(
|
|
||||||
value: headerToggleVal,
|
|
||||||
onChanged: (val) {
|
|
||||||
if (entitiesToToggle.isNotEmpty) {
|
|
||||||
ConnectionManager().callService(
|
|
||||||
domain: "homeassistant",
|
|
||||||
service: val ? "turn_on" : "turn_off",
|
|
||||||
entityId: entitiesToToggle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
body.add(
|
|
||||||
CardHeader(
|
|
||||||
name: card.title,
|
|
||||||
trailing: headerSwitch,
|
|
||||||
emptyPadding: Sizes.rowPadding,
|
|
||||||
leading: card.icon != null ? Icon(
|
|
||||||
MaterialDesignIcons.getIconDataFromIconName(card.icon),
|
|
||||||
size: Sizes.iconSize,
|
|
||||||
color: Theme.of(context).textTheme.headline.color
|
|
||||||
) : null,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
body.addAll(
|
|
||||||
entitiesToShow.map((EntityWrapper entity) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: entity,
|
|
||||||
handleTap: true,
|
|
||||||
child: entity.entity.buildDefaultWidget(context)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return CardWrapper(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
right: Sizes.rightWidgetPadding,
|
|
||||||
left: Sizes.leftWidgetPadding,
|
|
||||||
bottom: Sizes.rowPadding,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: body
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityButtonCard extends StatelessWidget {
|
|
||||||
|
|
||||||
final ButtonCardData card;
|
|
||||||
|
|
||||||
EntityButtonCard({
|
|
||||||
Key key, this.card
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
EntityWrapper entityWrapper = card.entity;
|
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: MissedEntityWidget(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
} else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
|
||||||
return Container(width: 0.0, height: 0.0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize);
|
|
||||||
|
|
||||||
Widget buttonIcon;
|
|
||||||
if (!card.showIcon) {
|
|
||||||
buttonIcon = Container(height: Sizes.rowPadding, width: 10);
|
|
||||||
} else if (iconSize > 0) {
|
|
||||||
buttonIcon = SizedBox(
|
|
||||||
height: iconSize,
|
|
||||||
child: FractionallySizedBox(
|
|
||||||
widthFactor: 0.5,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
child: EntityIcon(
|
|
||||||
//padding: EdgeInsets.only(top: 6),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
buttonIcon = AspectRatio(
|
|
||||||
aspectRatio: 2,
|
|
||||||
child: FractionallySizedBox(
|
|
||||||
widthFactor: 0.5,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.fitWidth,
|
|
||||||
child: EntityIcon(
|
|
||||||
//padding: EdgeInsets.only(top: 6),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CardWrapper(
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
buttonIcon,
|
|
||||||
_buildName(context)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
handleTap: true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildName(BuildContext context) {
|
|
||||||
if (card.showName) {
|
|
||||||
return EntityName(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 3,
|
|
||||||
textStyle: Theme.of(context).textTheme.subhead,
|
|
||||||
wordsWrap: true,
|
|
||||||
textAlign: TextAlign.center
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Container(width: 0, height: 0);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,198 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class GaugeCard extends StatelessWidget {
|
|
||||||
|
|
||||||
final GaugeCardData card;
|
|
||||||
|
|
||||||
GaugeCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
EntityWrapper entityWrapper = card.entity;
|
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: MissedEntityWidget(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
entityWrapper.overrideName = card.name ??
|
|
||||||
entityWrapper.displayName;
|
|
||||||
entityWrapper.unitOfMeasurementOverride = card.unit ??
|
|
||||||
entityWrapper.unitOfMeasurement;
|
|
||||||
double fixedValue;
|
|
||||||
double value = entityWrapper.entity.doubleState;
|
|
||||||
if (value > card.max) {
|
|
||||||
fixedValue = card.max.toDouble();
|
|
||||||
} else if (value < card.min) {
|
|
||||||
fixedValue = card.min.toDouble();
|
|
||||||
} else {
|
|
||||||
fixedValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<GaugeRange> ranges;
|
|
||||||
Color currentColor;
|
|
||||||
if (card.severity != null && card.severity["green"] is int && card.severity["red"] is int && card.severity["yellow"] is int) {
|
|
||||||
List<RangeContainer> rangesList = <RangeContainer>[
|
|
||||||
RangeContainer(card.severity["green"], HAClientTheme().getGreenGaugeColor()),
|
|
||||||
RangeContainer(card.severity["red"], HAClientTheme().getRedGaugeColor()),
|
|
||||||
RangeContainer(card.severity["yellow"], HAClientTheme().getYellowGaugeColor())
|
|
||||||
];
|
|
||||||
rangesList.sort((current, next) {
|
|
||||||
if (current.startFrom > next.startFrom) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (current.startFrom < next.startFrom) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fixedValue < rangesList[1].startFrom) {
|
|
||||||
currentColor = rangesList[0].color;
|
|
||||||
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
|
|
||||||
currentColor = rangesList[1].color;
|
|
||||||
} else {
|
|
||||||
currentColor = rangesList[2].color;
|
|
||||||
}
|
|
||||||
|
|
||||||
ranges = [
|
|
||||||
GaugeRange(
|
|
||||||
startValue: rangesList[0].startFrom.toDouble(),
|
|
||||||
endValue: rangesList[1].startFrom.toDouble(),
|
|
||||||
color: rangesList[0].color.withOpacity(0.1),
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
endWidth: 0.3,
|
|
||||||
startWidth: 0.3
|
|
||||||
),
|
|
||||||
GaugeRange(
|
|
||||||
startValue: rangesList[1].startFrom.toDouble(),
|
|
||||||
endValue: rangesList[2].startFrom.toDouble(),
|
|
||||||
color: rangesList[1].color.withOpacity(0.1),
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
endWidth: 0.3,
|
|
||||||
startWidth: 0.3
|
|
||||||
),
|
|
||||||
GaugeRange(
|
|
||||||
startValue: rangesList[2].startFrom.toDouble(),
|
|
||||||
endValue: card.max.toDouble(),
|
|
||||||
color: rangesList[2].color.withOpacity(0.1),
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
endWidth: 0.3,
|
|
||||||
startWidth: 0.3
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (ranges == null) {
|
|
||||||
currentColor = Theme.of(context).primaryColorDark;
|
|
||||||
ranges = <GaugeRange>[
|
|
||||||
GaugeRange(
|
|
||||||
startValue: card.min.toDouble(),
|
|
||||||
endValue: card.max.toDouble(),
|
|
||||||
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
endWidth: 0.3,
|
|
||||||
startWidth: 0.3,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return CardWrapper(
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: entityWrapper,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1.8,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
children: <Widget>[
|
|
||||||
SfRadialGauge(
|
|
||||||
axes: <RadialAxis>[
|
|
||||||
RadialAxis(
|
|
||||||
maximum: card.max.toDouble(),
|
|
||||||
minimum: card.min.toDouble(),
|
|
||||||
showLabels: false,
|
|
||||||
useRangeColorForAxis: true,
|
|
||||||
showTicks: false,
|
|
||||||
canScaleToFit: true,
|
|
||||||
ranges: ranges,
|
|
||||||
axisLineStyle: AxisLineStyle(
|
|
||||||
thickness: 0.3,
|
|
||||||
thicknessUnit: GaugeSizeUnit.factor,
|
|
||||||
color: Colors.transparent
|
|
||||||
),
|
|
||||||
startAngle: 180,
|
|
||||||
endAngle: 0,
|
|
||||||
pointers: <GaugePointer>[
|
|
||||||
RangePointer(
|
|
||||||
value: fixedValue,
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
width: 0.3,
|
|
||||||
color: currentColor,
|
|
||||||
enableAnimation: true,
|
|
||||||
animationType: AnimationType.bounceOut,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Flexible(
|
|
||||||
flex: 8,
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: Container()
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 6,
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: FractionallySizedBox(
|
|
||||||
widthFactor: 0.4,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: SimpleEntityState(
|
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
expanded: false,
|
|
||||||
maxLines: 1,
|
|
||||||
textAlign: TextAlign.center
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 3,
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
child: EntityName(
|
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
textStyle: Theme.of(context).textTheme.subhead
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
handleTap: true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class RangeContainer {
|
|
||||||
final int startFrom;
|
|
||||||
Color color;
|
|
||||||
|
|
||||||
RangeContainer(this.startFrom, this.color);
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class GlanceCard extends StatelessWidget {
|
|
||||||
final GlanceCardData card;
|
|
||||||
|
|
||||||
const GlanceCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
|
||||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
int length = entitiesToShow.length;
|
|
||||||
int rowsCount;
|
|
||||||
int columnsCount;
|
|
||||||
if (length == 0) {
|
|
||||||
columnsCount = 0;
|
|
||||||
rowsCount = 0;
|
|
||||||
} else {
|
|
||||||
columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
|
||||||
rowsCount = (length / columnsCount).round();
|
|
||||||
}
|
|
||||||
List<TableRow> rows = [];
|
|
||||||
for (int i = 0; i < rowsCount; i++) {
|
|
||||||
int start = i*columnsCount;
|
|
||||||
int end = start + math.min(columnsCount, length - start);
|
|
||||||
List<Widget> rowChildren = [];
|
|
||||||
rowChildren.addAll(entitiesToShow.sublist(
|
|
||||||
start, end
|
|
||||||
).map(
|
|
||||||
(EntityWrapper entity){
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: entity,
|
|
||||||
child: _buildEntityContainer(context, entity),
|
|
||||||
handleTap: true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
).toList()
|
|
||||||
);
|
|
||||||
while (rowChildren.length < columnsCount) {
|
|
||||||
rowChildren.add(
|
|
||||||
Container()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
rows.add(
|
|
||||||
TableRow(
|
|
||||||
children: rowChildren
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return CardWrapper(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
CardHeader(name: card.title),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
|
||||||
child: Table(
|
|
||||||
children: rows
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) {
|
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return MissedEntityWidget();
|
|
||||||
} else if (entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
|
||||||
return Container(width: 0.0, height: 0.0,);
|
|
||||||
}
|
|
||||||
List<Widget> result = [];
|
|
||||||
if (card.showName) {
|
|
||||||
result.add(_buildName(context));
|
|
||||||
}
|
|
||||||
result.add(
|
|
||||||
EntityIcon(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
size: Sizes.iconSize,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (card.showState) {
|
|
||||||
result.add(_buildState());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Center(
|
|
||||||
child: InkResponse(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: result,
|
|
||||||
),
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildName(BuildContext context) {
|
|
||||||
return EntityName(
|
|
||||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
wordsWrap: false,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
textStyle: Theme.of(context).textTheme.body1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildState() {
|
|
||||||
return SimpleEntityState(
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
expanded: false,
|
|
||||||
maxLines: 1,
|
|
||||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class HorizontalStackCard extends StatelessWidget {
|
|
||||||
final HorizontalStackCardData card;
|
|
||||||
|
|
||||||
const HorizontalStackCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.childCards.isNotEmpty) {
|
|
||||||
List<Widget> children = [];
|
|
||||||
children = card.childCards.map((childCard) => Flexible(
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: childCard.buildCardWidget()
|
|
||||||
)
|
|
||||||
).toList();
|
|
||||||
return IntrinsicHeight(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class MarkdownCard extends StatelessWidget {
|
|
||||||
final MarkdownCardData card;
|
|
||||||
|
|
||||||
const MarkdownCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.content == null) {
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
} else if (card.content == '***') {
|
|
||||||
return Container(height: Sizes.rowPadding, width: 0.0,);
|
|
||||||
}
|
|
||||||
return CardWrapper(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
CardHeader(name: card.title),
|
|
||||||
MarkdownBody(
|
|
||||||
data: card.content,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class MediaControlsCard extends StatelessWidget {
|
|
||||||
final MediaControlCardData card;
|
|
||||||
|
|
||||||
const MediaControlsCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: MissedEntityWidget(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return CardWrapper(
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
handleTap: null,
|
|
||||||
child: MediaPlayerWidget()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class UnsupportedCard extends StatelessWidget {
|
|
||||||
final CardData card;
|
|
||||||
|
|
||||||
const UnsupportedCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return CardWrapper(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Text("'${card.type}' card is not supported yet"),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class VerticalStackCard extends StatelessWidget {
|
|
||||||
final VerticalStackCardData card;
|
|
||||||
|
|
||||||
const VerticalStackCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.childCards.isNotEmpty) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: card.childCards.map<Widget>(
|
|
||||||
(childCard) => childCard.buildCardWidget()
|
|
||||||
).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -4,11 +4,9 @@ class CardHeader extends StatelessWidget {
|
|||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final Widget trailing;
|
final Widget trailing;
|
||||||
final Widget leading;
|
|
||||||
final Widget subtitle;
|
final Widget subtitle;
|
||||||
final double emptyPadding;
|
|
||||||
|
|
||||||
const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key);
|
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -16,15 +14,14 @@ class CardHeader extends StatelessWidget {
|
|||||||
if ((name != null) && (name.trim().length > 0)) {
|
if ((name != null) && (name.trim().length > 0)) {
|
||||||
result = new ListTile(
|
result = new ListTile(
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
leading: leading,
|
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
title: Text("$name",
|
title: Text("$name",
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.headline),
|
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = new Container(width: 0.0, height: emptyPadding);
|
result = new Container(width: 0.0, height: 0.0);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class CardWrapper extends StatelessWidget {
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
final EdgeInsets padding;
|
|
||||||
|
|
||||||
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: padding,
|
|
||||||
child: child
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
51
lib/cards/widgets/entity_button_card_body.widget.dart
Normal file
51
lib/cards/widgets/entity_button_card_body.widget.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class EntityButtonCardBody extends StatelessWidget {
|
||||||
|
|
||||||
|
EntityButtonCardBody({
|
||||||
|
Key key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
}
|
||||||
|
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
widthFactor: 1,
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
return EntityIcon(
|
||||||
|
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||||
|
size: constraints.maxWidth / 2.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_buildName()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildName() {
|
||||||
|
return EntityName(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 3,
|
||||||
|
wordsWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
fontSize: Sizes.nameFontSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
153
lib/cards/widgets/gauge_card_body.dart
Normal file
153
lib/cards/widgets/gauge_card_body.dart
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class GaugeCardBody extends StatefulWidget {
|
||||||
|
|
||||||
|
final int min;
|
||||||
|
final int max;
|
||||||
|
final Map severity;
|
||||||
|
|
||||||
|
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_GaugeCardBodyState createState() => _GaugeCardBodyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GaugeCardBodyState extends State<GaugeCardBody> {
|
||||||
|
|
||||||
|
List<charts.Series> seriesList;
|
||||||
|
|
||||||
|
List<charts.Series<GaugeSegment, String>> _createData(double value) {
|
||||||
|
double fixedValue;
|
||||||
|
if (value > widget.max) {
|
||||||
|
fixedValue = widget.max.toDouble();
|
||||||
|
} else if (value < widget.min) {
|
||||||
|
fixedValue = widget.min.toDouble();
|
||||||
|
} else {
|
||||||
|
fixedValue = value;
|
||||||
|
}
|
||||||
|
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
|
||||||
|
Color mainColor;
|
||||||
|
if (widget.severity != null) {
|
||||||
|
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
|
||||||
|
mainColor = Colors.red;
|
||||||
|
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
|
||||||
|
mainColor = Colors.amber;
|
||||||
|
} else {
|
||||||
|
mainColor = Colors.green;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mainColor = Colors.green;
|
||||||
|
}
|
||||||
|
final data = [
|
||||||
|
GaugeSegment('Main', toShow, mainColor),
|
||||||
|
GaugeSegment('Rest', 100 - toShow, Colors.black45),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
charts.Series<GaugeSegment, String>(
|
||||||
|
id: 'Segments',
|
||||||
|
domainFn: (GaugeSegment segment, _) => segment.segment,
|
||||||
|
measureFn: (GaugeSegment segment, _) => segment.value,
|
||||||
|
colorFn: (GaugeSegment segment, _) => segment.color,
|
||||||
|
// Set a label accessor to control the text of the arc label.
|
||||||
|
labelAccessorFn: (GaugeSegment segment, _) =>
|
||||||
|
segment.segment == 'Main' ? '${segment.value}' : null,
|
||||||
|
data: data,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1.5,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
overflow: Overflow.clip,
|
||||||
|
children: [
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double verticalOffset;
|
||||||
|
if(constraints.maxWidth > 150.0) {
|
||||||
|
verticalOffset = 0.2;
|
||||||
|
} else if (constraints.maxWidth > 100.0) {
|
||||||
|
verticalOffset = 0.3;
|
||||||
|
} else {
|
||||||
|
verticalOffset = 0.3;
|
||||||
|
}
|
||||||
|
return FractionallySizedBox(
|
||||||
|
heightFactor: 2,
|
||||||
|
widthFactor: 1,
|
||||||
|
alignment: FractionalOffset(0,verticalOffset),
|
||||||
|
child: charts.PieChart(
|
||||||
|
_createData(entityWrapper.entity.doubleState),
|
||||||
|
animate: false,
|
||||||
|
defaultRenderer: charts.ArcRendererConfig(
|
||||||
|
arcRatio: 0.4,
|
||||||
|
startAngle: pi,
|
||||||
|
arcLength: pi,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double fontSize = constraints.maxHeight / 7;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||||
|
child: SimpleEntityState(
|
||||||
|
//textAlign: TextAlign.center,
|
||||||
|
expanded: false,
|
||||||
|
maxLines: 1,
|
||||||
|
bold: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
fontSize: fontSize,
|
||||||
|
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double fontSize = constraints.maxHeight / 7;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: fontSize),
|
||||||
|
child: EntityName(
|
||||||
|
fontSize: fontSize,
|
||||||
|
maxLines: 1,
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GaugeSegment {
|
||||||
|
final String segment;
|
||||||
|
final double value;
|
||||||
|
final charts.Color color;
|
||||||
|
|
||||||
|
GaugeSegment(this.segment, this.value, Color color)
|
||||||
|
: this.color = charts.Color(
|
||||||
|
r: color.red, g: color.green, b: color.blue, a: color.alpha);
|
||||||
|
}
|
85
lib/cards/widgets/glance_card_entity_container.dart
Normal file
85
lib/cards/widgets/glance_card_entity_container.dart
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class GlanceCardEntityContainer extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool showName;
|
||||||
|
final bool showState;
|
||||||
|
final bool nameInTheBottom;
|
||||||
|
final double iconSize;
|
||||||
|
final double nameFontSize;
|
||||||
|
final bool wordsWrapInName;
|
||||||
|
|
||||||
|
GlanceCardEntityContainer({
|
||||||
|
Key key,
|
||||||
|
@required this.showName,
|
||||||
|
@required this.showState,
|
||||||
|
this.nameInTheBottom: false,
|
||||||
|
this.iconSize: Sizes.iconSize,
|
||||||
|
this.nameFontSize: Sizes.smallFontSize,
|
||||||
|
this.wordsWrapInName: false
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
}
|
||||||
|
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> result = [];
|
||||||
|
if (!nameInTheBottom) {
|
||||||
|
if (showName) {
|
||||||
|
result.add(_buildName());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (showState) {
|
||||||
|
result.add(_buildState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.add(
|
||||||
|
EntityIcon(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
size: iconSize,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!nameInTheBottom) {
|
||||||
|
if (showState) {
|
||||||
|
result.add(_buildState());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.add(_buildName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: InkResponse(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: result,
|
||||||
|
),
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildName() {
|
||||||
|
return EntityName(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
wordsWrap: wordsWrapInName,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
fontSize: nameFontSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildState() {
|
||||||
|
return SimpleEntityState(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
expanded: false,
|
||||||
|
maxLines: 1,
|
||||||
|
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
90
lib/cards/widgets/light_card_body.dart
Normal file
90
lib/cards/widgets/light_card_body.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class LightCardBody extends StatefulWidget {
|
||||||
|
|
||||||
|
final int min;
|
||||||
|
final int max;
|
||||||
|
final Map severity;
|
||||||
|
|
||||||
|
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LightCardBodyState createState() => _LightCardBodyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LightCardBodyState extends State<LightCardBody> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
LightEntity entity = entityWrapper.entity;
|
||||||
|
Logger.d("Light brightness: ${entity.brightness}");
|
||||||
|
|
||||||
|
return FractionallySizedBox(
|
||||||
|
widthFactor: 0.5,
|
||||||
|
child: Container(
|
||||||
|
//color: Colors.redAccent,
|
||||||
|
child: SingleCircularSlider(
|
||||||
|
255,
|
||||||
|
entity.brightness ?? 0,
|
||||||
|
baseColor: Colors.white,
|
||||||
|
handlerColor: Colors.blue[200],
|
||||||
|
selectionColor: Colors.blue[100],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1.5,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
overflow: Overflow.clip,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double fontSize = constraints.maxHeight / 7;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||||
|
child: SimpleEntityState(
|
||||||
|
//textAlign: TextAlign.center,
|
||||||
|
expanded: false,
|
||||||
|
maxLines: 1,
|
||||||
|
bold: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
fontSize: fontSize,
|
||||||
|
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double fontSize = constraints.maxHeight / 7;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: fontSize),
|
||||||
|
child: EntityName(
|
||||||
|
fontSize: fontSize,
|
||||||
|
maxLines: 1,
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -51,10 +51,6 @@ 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) {
|
||||||
@ -80,17 +76,6 @@ 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"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,12 +98,10 @@ class CardType {
|
|||||||
static const IFRAME = "iframe";
|
static const IFRAME = "iframe";
|
||||||
static const GAUGE = "gauge";
|
static const GAUGE = "gauge";
|
||||||
static const ENTITY_BUTTON = "entity-button";
|
static const ENTITY_BUTTON = "entity-button";
|
||||||
static const BUTTON = "button";
|
|
||||||
static const CONDITIONAL = "conditional";
|
static const CONDITIONAL = "conditional";
|
||||||
static const ALARM_PANEL = "alarm-panel";
|
static const ALARM_PANEL = "alarm-panel";
|
||||||
static const MARKDOWN = "markdown";
|
static const MARKDOWN = "markdown";
|
||||||
static const LIGHT = "light";
|
static const LIGHT = "light";
|
||||||
static const ENTITY_FILTER = "entity-filter";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Sizes {
|
class Sizes {
|
||||||
@ -128,10 +111,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;
|
||||||
|
@ -25,12 +25,9 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
|||||||
|
|
||||||
|
|
||||||
void _callService(AlarmControlPanelEntity entity, String service) {
|
void _callService(AlarmControlPanelEntity entity, String service) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain,
|
entity.domain, service, entity.entityId,
|
||||||
service: service,
|
{"code": "$code"}));
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"code": "$code"}
|
|
||||||
);
|
|
||||||
setState(() {
|
setState(() {
|
||||||
code = "";
|
code = "";
|
||||||
});
|
});
|
||||||
@ -61,11 +58,7 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
|||||||
FlatButton(
|
FlatButton(
|
||||||
child: new Text("Yes"),
|
child: new Text("Yes"),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null));
|
||||||
domain: entity.domain,
|
|
||||||
service: "alarm_trigger",
|
|
||||||
entityId: entity.entityId
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -248,9 +241,7 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
|||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
"TRIGGER",
|
"TRIGGER",
|
||||||
style: Theme.of(context).textTheme.subhead.copyWith(
|
style: TextStyle(color: Colors.redAccent)
|
||||||
color: Theme.of(context).errorColor
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
onPressed: () => _askToTrigger(entity),
|
onPressed: () => _askToTrigger(entity),
|
||||||
)
|
)
|
||||||
|
@ -7,7 +7,8 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
double iconSize = 26.0;
|
double iconSize = 26.0;
|
||||||
Widget badgeIcon;
|
Widget badgeIcon;
|
||||||
String onBadgeTextValue;
|
String onBadgeTextValue;
|
||||||
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
|
||||||
|
EntityColor.badgeColors["default"];
|
||||||
switch (entityModel.entityWrapper.entity.domain) {
|
switch (entityModel.entityWrapper.entity.domain) {
|
||||||
case "sun":
|
case "sun":
|
||||||
{
|
{
|
||||||
@ -29,7 +30,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
badgeIcon = EntityIcon(
|
badgeIcon = EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
color: Theme.of(context).textTheme.body1.color
|
color: Colors.black
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -39,7 +40,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
badgeIcon = EntityIcon(
|
badgeIcon = EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
color: Theme.of(context).textTheme.body1.color
|
color: Colors.black
|
||||||
);
|
);
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||||
break;
|
break;
|
||||||
@ -63,9 +64,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.body1.copyWith(
|
style: TextStyle(fontSize: stateFontSize),
|
||||||
fontSize: stateFontSize
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -78,9 +77,7 @@ 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: Theme.of(context).textTheme.overline.copyWith(
|
style: TextStyle(fontSize: 12.0, color: Colors.white),
|
||||||
color: HAClientTheme().getOnBadgeTextColor()
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.fade),
|
overflow: TextOverflow.fade),
|
||||||
@ -101,7 +98,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
decoration: new BoxDecoration(
|
decoration: new BoxDecoration(
|
||||||
// Circle shape
|
// Circle shape
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: Theme.of(context).cardColor,
|
color: Colors.white,
|
||||||
// The border you want
|
// The border you want
|
||||||
border: new Border.all(
|
border: new Border.all(
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
@ -134,7 +131,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
"${entityModel.entityWrapper.displayName}",
|
"${entityModel.entityWrapper.displayName}",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.caption,
|
style: TextStyle(fontSize: 12.0),
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
@ -3,16 +3,12 @@ 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,9 +2,7 @@ part of '../../../main.dart';
|
|||||||
|
|
||||||
class CameraStreamView extends StatefulWidget {
|
class CameraStreamView extends StatefulWidget {
|
||||||
|
|
||||||
final bool withControls;
|
CameraStreamView({Key key}) : super(key: key);
|
||||||
|
|
||||||
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_CameraStreamViewState createState() => _CameraStreamViewState();
|
_CameraStreamViewState createState() => _CameraStreamViewState();
|
||||||
@ -12,176 +10,45 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _loadResources() {
|
CameraEntity _entity;
|
||||||
if (_loading != null && !_loading.isCompleted) {
|
bool started = false;
|
||||||
Logger.d("[Camera Player] Resources loading is not finished yet");
|
String streamUrl = "";
|
||||||
return _loading.future;
|
|
||||||
}
|
|
||||||
Logger.d("[Camera Player] Loading resources");
|
|
||||||
_loading = Completer();
|
|
||||||
_entity = EntityModel
|
|
||||||
.of(context)
|
|
||||||
.entityWrapper
|
|
||||||
.entity;
|
|
||||||
if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
|
|
||||||
HomeAssistant().getCameraStream(_entity.entityId)
|
|
||||||
.then((data) {
|
|
||||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
|
||||||
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
|
|
||||||
_webViewHtml = Uri.dataFromString(
|
|
||||||
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
|
||||||
mimeType: 'text/html',
|
|
||||||
encoding: Encoding.getByName('utf-8')
|
|
||||||
).toString();
|
|
||||||
_loading.complete();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catchError((e) {
|
|
||||||
if (e == 'start_stream_failed') {
|
|
||||||
Logger.e("[Camera Player] Home Assistant failed starting stream. Forcing MJPEG: $e");
|
|
||||||
_loadMJPEG().then((_) {
|
|
||||||
_loading.complete();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_loading.completeError(e);
|
|
||||||
Logger.e("[Camera Player] Error loading stream: $e");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_loadMJPEG().then((_) {
|
|
||||||
_loading.complete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _loading.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _loadMJPEG() async {
|
launchStream() {
|
||||||
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
Launcher.launchURLInCustomTab(
|
||||||
.entityId}?token=${_entity.attributes['access_token']}';
|
context: context,
|
||||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
url: streamUrl
|
||||||
var file = await rootBundle.loadString('assets/html/cameraView.html');
|
|
||||||
_webViewHtml = Uri.dataFromString(
|
|
||||||
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
|
||||||
mimeType: 'text/html',
|
|
||||||
encoding: Encoding.getByName('utf-8')
|
|
||||||
).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildScreen() {
|
|
||||||
Widget screenWidget;
|
|
||||||
if (!_isLoaded) {
|
|
||||||
screenWidget = Center(
|
|
||||||
child: EntityPicture(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
screenWidget = WebView(
|
|
||||||
initialUrl: _webViewHtml,
|
|
||||||
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
|
|
||||||
debuggingEnabled: Logger.isInDebugMode,
|
|
||||||
gestureNavigationEnabled: false,
|
|
||||||
javascriptMode: JavascriptMode.unrestricted,
|
|
||||||
javascriptChannels: {
|
|
||||||
JavascriptChannel(
|
|
||||||
name: _jsMessageChannelName,
|
|
||||||
onMessageReceived: ((message) {
|
|
||||||
Logger.d('[Camera Player] Message from page: $message');
|
|
||||||
setState((){
|
|
||||||
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return AspectRatio(
|
|
||||||
aspectRatio: _aspectRatio,
|
|
||||||
child: screenWidget
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildControls() {
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.refresh),
|
|
||||||
iconSize: 40,
|
|
||||||
color: Theme.of(context).accentColor,
|
|
||||||
onPressed: _isLoaded ? () {
|
|
||||||
setState(() {
|
|
||||||
_isLoaded = false;
|
|
||||||
});
|
|
||||||
} : null,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Container(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.fullscreen),
|
|
||||||
iconSize: 40,
|
|
||||||
color: Theme.of(context).accentColor,
|
|
||||||
onPressed: _isLoaded ? () {
|
|
||||||
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 (!_isLoaded && (_loading == null || _loading.isCompleted)) {
|
if (!started) {
|
||||||
_loadResources().then((_) => setState((){ _isLoaded = true; }));
|
_entity = EntityModel
|
||||||
|
.of(context)
|
||||||
|
.entityWrapper
|
||||||
|
.entity;
|
||||||
|
started = true;
|
||||||
}
|
}
|
||||||
if (widget.withControls) {
|
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||||
return Card(
|
.entityId}?token=${_entity.attributes['access_token']}';
|
||||||
child: Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildScreen(),
|
Container(
|
||||||
_buildControls()
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
||||||
|
iconSize: 50.0,
|
||||||
|
onPressed: () => launchStream(),
|
||||||
|
)
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return _buildScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -10,8 +10,9 @@ class ClimateControlWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||||
|
|
||||||
bool _temperaturePending = false;
|
bool _showPending = 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;
|
||||||
@ -26,11 +27,9 @@ 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;
|
||||||
@ -39,6 +38,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
_tmpAuxHeat = entity.auxHeat;
|
_tmpAuxHeat = entity.auxHeat;
|
||||||
_tmpTargetHumidity = entity.targetHumidity;
|
_tmpTargetHumidity = entity.targetHumidity;
|
||||||
|
|
||||||
|
_showPending = false;
|
||||||
_changedHere = false;
|
_changedHere = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,44 +73,36 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _setTemperature(ClimateEntity entity) {
|
void _setTemperature(ClimateEntity entity) {
|
||||||
_tempThrottleTimer?.cancel();
|
if (_tempThrottleTimer!=null) {
|
||||||
|
_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;
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
|
||||||
ConnectionManager().callService(
|
_resetStateTimer(entity);
|
||||||
domain: entity.domain,
|
|
||||||
service: "set_temperature",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setTargetTemp(ClimateEntity entity) {
|
void _setTargetTemp(ClimateEntity entity) {
|
||||||
_targetTempThrottleTimer?.cancel();
|
if (_targetTempThrottleTimer!=null) {
|
||||||
|
_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;
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
|
||||||
ConnectionManager().callService(
|
_resetStateTimer(entity);
|
||||||
domain: entity.domain,
|
|
||||||
service: "set_temperature",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -119,12 +111,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpTargetHumidity = value.roundToDouble();
|
_tmpTargetHumidity = value.roundToDouble();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
|
||||||
domain: entity.domain,
|
_resetStateTimer(entity);
|
||||||
service: "set_humidity",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"humidity": "$_tmpTargetHumidity"}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,12 +120,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpHVACMode = value;
|
_tmpHVACMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"}));
|
||||||
domain: entity.domain,
|
_resetStateTimer(entity);
|
||||||
service: "set_hvac_mode",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"hvac_mode": "$_tmpHVACMode"}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,12 +129,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpSwingMode = value;
|
_tmpSwingMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
|
||||||
domain: entity.domain,
|
_resetStateTimer(entity);
|
||||||
service: "set_swing_mode",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"swing_mode": "$_tmpSwingMode"}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +138,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpFanMode = value;
|
_tmpFanMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"});
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +147,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpPresetMode = value;
|
_tmpPresetMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"});
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +165,18 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpAuxHeat = value;
|
_tmpAuxHeat = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"});
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetStateTimer(ClimateEntity entity) {
|
||||||
|
if (_resetTimer!=null) {
|
||||||
|
_resetTimer.cancel();
|
||||||
|
}
|
||||||
|
_resetTimer = Timer(Duration(seconds: 3), () {
|
||||||
|
setState(() {});
|
||||||
|
_resetVars(entity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,11 +184,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(
|
||||||
@ -204,20 +197,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
//_buildOnOffControl(entity),
|
//_buildOnOffControl(entity),
|
||||||
_buildTemperatureControls(entity, context),
|
_buildTemperatureControls(entity),
|
||||||
_buildTargetTemperatureControls(entity, context),
|
_buildTargetTemperatureControls(entity),
|
||||||
_buildHumidityControls(entity, context),
|
_buildHumidityControls(entity),
|
||||||
_buildOperationControl(entity, context),
|
_buildOperationControl(entity),
|
||||||
_buildFanControl(entity, context),
|
_buildFanControl(entity),
|
||||||
_buildSwingControl(entity, context),
|
_buildSwingControl(entity),
|
||||||
_buildPresetModeControl(entity, context),
|
_buildPresetModeControl(entity),
|
||||||
_buildAuxHeatControl(entity, context)
|
_buildAuxHeatControl(entity)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) {
|
Widget _buildPresetModeControl(ClimateEntity entity) {
|
||||||
if (entity.supportPresetMode) {
|
if (entity.supportPresetMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.presetModes,
|
options: entity.presetModes,
|
||||||
@ -242,7 +235,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) {
|
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
||||||
if (entity.supportAuxHeat ) {
|
if (entity.supportAuxHeat ) {
|
||||||
return ModeSwitchWidget(
|
return ModeSwitchWidget(
|
||||||
caption: "Aux heat",
|
caption: "Aux heat",
|
||||||
@ -254,7 +247,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOperationControl(ClimateEntity entity, BuildContext context) {
|
Widget _buildOperationControl(ClimateEntity entity) {
|
||||||
if (entity.hvacModes != null) {
|
if (entity.hvacModes != null) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setHVACMode(entity, mode),
|
onChange: (mode) => _setHVACMode(entity, mode),
|
||||||
@ -267,7 +260,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFanControl(ClimateEntity entity, BuildContext context) {
|
Widget _buildFanControl(ClimateEntity entity) {
|
||||||
if (entity.supportFanMode) {
|
if (entity.supportFanMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.fanModes,
|
options: entity.fanModes,
|
||||||
@ -280,7 +273,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSwingControl(ClimateEntity entity, BuildContext context) {
|
Widget _buildSwingControl(ClimateEntity entity) {
|
||||||
if (entity.supportSwingMode) {
|
if (entity.supportSwingMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setSwingMode(entity, mode),
|
onChange: (mode) => _setSwingMode(entity, mode),
|
||||||
@ -293,15 +286,17 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) {
|
Widget _buildTemperatureControls(ClimateEntity entity) {
|
||||||
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: Theme.of(context).textTheme.body1),
|
Text("Target temperature", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTemperature,
|
value: _tmpTemperature,
|
||||||
active: _temperaturePending,
|
fontColor: _showPending ? Colors.red : Colors.black,
|
||||||
onDec: () => _temperatureDown(entity),
|
onDec: () => _temperatureDown(entity),
|
||||||
onInc: () => _temperatureUp(entity),
|
onInc: () => _temperatureUp(entity),
|
||||||
)
|
)
|
||||||
@ -312,13 +307,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) {
|
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
||||||
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,
|
||||||
active: _temperaturePending,
|
fontColor: _showPending ? Colors.red : Colors.black,
|
||||||
onDec: () => _targetLowDown(entity),
|
onDec: () => _targetLowDown(entity),
|
||||||
onInc: () => _targetLowUp(entity),
|
onInc: () => _targetLowUp(entity),
|
||||||
),
|
),
|
||||||
@ -331,7 +326,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
controls.add(
|
controls.add(
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetHigh,
|
value: _tmpTargetHigh,
|
||||||
active: _temperaturePending,
|
fontColor: _showPending ? Colors.red : Colors.black,
|
||||||
onDec: () => _targetHighDown(entity),
|
onDec: () => _targetHighDown(entity),
|
||||||
onInc: () => _targetHighUp(entity),
|
onInc: () => _targetHighUp(entity),
|
||||||
)
|
)
|
||||||
@ -341,7 +336,9 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Target temperature range", style: Theme.of(context).textTheme.body1),
|
Text("Target temperature range", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
Row(
|
Row(
|
||||||
children: controls,
|
children: controls,
|
||||||
)
|
)
|
||||||
@ -352,13 +349,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) {
|
Widget _buildHumidityControls(ClimateEntity entity) {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (entity.supportTargetHumidity) {
|
if (entity.supportTargetHumidity) {
|
||||||
result.addAll(<Widget>[
|
result.addAll(<Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$_tmpTargetHumidity%",
|
"$_tmpTargetHumidity%",
|
||||||
style: Theme.of(context).textTheme.display1,
|
style: TextStyle(fontSize: Sizes.largeFontSize),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Slider(
|
child: Slider(
|
||||||
@ -383,7 +380,9 @@ 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: Theme.of(context).textTheme.body1),
|
child: Text("Target humidity", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@ -405,6 +404,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_resetTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,16 +33,23 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("$displayState",
|
Text("$displayState",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: Theme.of(context).textTheme.body2),
|
style: new TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
)),
|
||||||
Text(" $targetTemp",
|
Text(" $targetTemp",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: Theme.of(context).textTheme.body1)
|
style: new TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
entity.currentTemperature != null ?
|
entity.currentTemperature != null ?
|
||||||
Text("Currently: ${entity.currentTemperature}",
|
Text("Currently: ${entity.currentTemperature}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: Theme.of(context).textTheme.subtitle
|
style: new TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
color: Colors.black45)
|
||||||
) :
|
) :
|
||||||
Container(height: 0.0,)
|
Container(height: 0.0,)
|
||||||
],
|
],
|
||||||
|
@ -3,8 +3,10 @@ part of '../../../main.dart';
|
|||||||
class ModeSelectorWidget extends StatelessWidget {
|
class ModeSelectorWidget extends StatelessWidget {
|
||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
final List options;
|
final List<String> options;
|
||||||
final String value;
|
final String value;
|
||||||
|
final double captionFontSize;
|
||||||
|
final double valueFontSize;
|
||||||
final onChange;
|
final onChange;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
|
|
||||||
@ -14,6 +16,8 @@ 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);
|
||||||
|
|
||||||
@ -24,7 +28,9 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("$caption", style: Theme.of(context).textTheme.body1),
|
Text("$caption", style: TextStyle(
|
||||||
|
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||||
|
)),
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -34,12 +40,15 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
value: value,
|
value: value,
|
||||||
iconSize: 30.0,
|
iconSize: 30.0,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
style: Theme.of(context).textTheme.title,
|
style: TextStyle(
|
||||||
|
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
hint: Text("Select ${caption.toLowerCase()}"),
|
hint: Text("Select ${caption.toLowerCase()}"),
|
||||||
items: options.map((value) {
|
items: options.map((String 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,6 +4,7 @@ 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;
|
||||||
@ -12,6 +13,7 @@ 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)
|
||||||
@ -23,7 +25,7 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
padding: this.padding,
|
padding: this.padding,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildCaption(context),
|
_buildCaption(),
|
||||||
Switch(
|
Switch(
|
||||||
onChanged: (value) => onChange(value),
|
onChanged: (value) => onChange(value),
|
||||||
value: value ?? false,
|
value: value ?? false,
|
||||||
@ -33,10 +35,12 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCaption(BuildContext context) {
|
Widget _buildCaption() {
|
||||||
Widget captionWidget = Text(
|
Widget captionWidget = Text(
|
||||||
"$caption",
|
"$caption",
|
||||||
style: Theme.of(context).textTheme.body1,
|
style: TextStyle(
|
||||||
|
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
|
@ -2,7 +2,8 @@ part of '../../../main.dart';
|
|||||||
|
|
||||||
class TemperatureControlWidget extends StatelessWidget {
|
class TemperatureControlWidget extends StatelessWidget {
|
||||||
final double value;
|
final double value;
|
||||||
final bool active;
|
final double fontSize;
|
||||||
|
final Color fontColor;
|
||||||
final onInc;
|
final onInc;
|
||||||
final onDec;
|
final onDec;
|
||||||
|
|
||||||
@ -11,9 +12,8 @@ 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.active: false
|
this.fontColor})
|
||||||
})
|
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -23,7 +23,10 @@ class TemperatureControlWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$value",
|
"$value",
|
||||||
style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1,
|
style: TextStyle(
|
||||||
|
fontSize: fontSize ?? 24.0,
|
||||||
|
color: fontColor ?? Colors.black
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
@ -18,7 +18,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpPosition = position.roundToDouble();
|
_tmpPosition = position.roundToDouble();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(domain: entity.domain, service: "set_cover_position", entityId: entity.entityId, data: {"position": _tmpPosition.round()});
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpTiltPosition = position.roundToDouble();
|
_tmpTiltPosition = position.roundToDouble();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(domain: entity.domain, service: "set_cover_tilt_position", entityId: entity.entityId, data: {"tilt_position": _tmpTiltPosition.round()});
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +64,9 @@ 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"),
|
child: Text("Position", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
Slider(
|
Slider(
|
||||||
value: _tmpPosition,
|
value: _tmpPosition,
|
||||||
@ -116,7 +118,9 @@ 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"),
|
child: Text("Tilt position", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
));
|
));
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -131,18 +135,18 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
|
|
||||||
class CoverTiltControlsWidget extends StatelessWidget {
|
class CoverTiltControlsWidget extends StatelessWidget {
|
||||||
void _open(CoverEntity entity) {
|
void _open(CoverEntity entity) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain, service: "open_cover_tilt", entityId: entity.entityId);
|
entity.domain, "open_cover_tilt", entity.entityId, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _close(CoverEntity entity) {
|
void _close(CoverEntity entity) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain, service: "close_cover_tilt", entityId: entity.entityId);
|
entity.domain, "close_cover_tilt", entity.entityId, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stop(CoverEntity entity) {
|
void _stop(CoverEntity entity) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain, service: "stop_cover_tilt", entityId: entity.entityId);
|
entity.domain, "stop_cover_tilt", entity.entityId, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -2,27 +2,18 @@ part of '../../../main.dart';
|
|||||||
|
|
||||||
class CoverStateWidget extends StatelessWidget {
|
class CoverStateWidget extends StatelessWidget {
|
||||||
void _open(CoverEntity entity) {
|
void _open(CoverEntity entity) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain,
|
entity.domain, "open_cover", entity.entityId, null));
|
||||||
service: "open_cover",
|
|
||||||
entityId: entity.entityId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _close(CoverEntity entity) {
|
void _close(CoverEntity entity) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain,
|
entity.domain, "close_cover", entity.entityId, null));
|
||||||
service: "close_cover",
|
|
||||||
entityId: entity.entityId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stop(CoverEntity entity) {
|
void _stop(CoverEntity entity) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain,
|
entity.domain, "stop_cover", entity.entityId, null));
|
||||||
service: "stop_cover",
|
|
||||||
entityId: entity.entityId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -35,7 +35,8 @@ class DateTimeEntity extends Entity {
|
|||||||
return formattedState;
|
return formattedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setNewState(Map newValue) {
|
void setNewState(newValue) {
|
||||||
ConnectionManager().callService(domain: domain, service: "set_datetime", entityId: entityId, data: newValue);
|
eventBus
|
||||||
|
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,8 +9,10 @@ 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),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -11,23 +11,25 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
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: HAClientTheme().getLinkTextStyle(context).copyWith(
|
style: TextStyle(color: Colors.blue),
|
||||||
decoration: TextDecoration.none
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -59,11 +61,6 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
entityModel.entityWrapper.handleTap();
|
entityModel.entityWrapper.handleTap();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDoubleTap: () {
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
entityModel.entityWrapper.handleDoubleTap();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: result,
|
child: result,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink}
|
class StatelessEntityType {
|
||||||
|
static const NONE = 0;
|
||||||
|
static const MISSED = 1;
|
||||||
|
static const DIVIDER = 2;
|
||||||
|
static const SECTION = 3;
|
||||||
|
static const CALL_SERVICE = 4;
|
||||||
|
static const WEBLINK = 5;
|
||||||
|
}
|
||||||
|
|
||||||
class Entity {
|
class Entity {
|
||||||
|
|
||||||
@ -69,8 +76,8 @@ class Entity {
|
|||||||
String entityPicture;
|
String entityPicture;
|
||||||
String state;
|
String state;
|
||||||
String displayState;
|
String displayState;
|
||||||
DateTime lastUpdatedTimestamp;
|
DateTime _lastUpdated;
|
||||||
StatelessEntityType statelessType = StatelessEntityType.none;
|
int statelessType = 0;
|
||||||
|
|
||||||
List<Entity> childEntities = [];
|
List<Entity> childEntities = [];
|
||||||
String deviceClass;
|
String deviceClass;
|
||||||
@ -113,36 +120,31 @@ class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Entity.missed(String entityId) {
|
Entity.missed(String entityId) {
|
||||||
statelessType = StatelessEntityType.missed;
|
statelessType = StatelessEntityType.MISSED;
|
||||||
attributes = {"hidden": false};
|
attributes = {"hidden": false};
|
||||||
this.entityId = entityId;
|
this.entityId = entityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.divider() {
|
Entity.divider() {
|
||||||
statelessType = StatelessEntityType.divider;
|
statelessType = StatelessEntityType.DIVIDER;
|
||||||
attributes = {"hidden": false};
|
attributes = {"hidden": false};
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.section(String label) {
|
Entity.section(String label) {
|
||||||
statelessType = StatelessEntityType.section;
|
statelessType = StatelessEntityType.SECTION;
|
||||||
attributes = {"hidden": false, "friendly_name": "$label"};
|
attributes = {"hidden": false, "friendly_name": "$label"};
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.ghost(String name, String icon) {
|
|
||||||
statelessType = StatelessEntityType.ghost;
|
|
||||||
attributes = {"icon": icon, "hidden": false, "friendly_name": name};
|
|
||||||
}
|
|
||||||
|
|
||||||
Entity.callService({String icon, String name, String service, String actionName}) {
|
Entity.callService({String icon, String name, String service, String actionName}) {
|
||||||
statelessType = StatelessEntityType.callService;
|
statelessType = StatelessEntityType.CALL_SERVICE;
|
||||||
entityId = service;
|
entityId = service;
|
||||||
displayState = actionName?.toUpperCase() ?? "RUN";
|
displayState = actionName?.toUpperCase() ?? "RUN";
|
||||||
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.weblink({String url, String name, String icon}) {
|
Entity.weblink({String url, String name, String icon}) {
|
||||||
statelessType = StatelessEntityType.webLink;
|
statelessType = StatelessEntityType.WEBLINK;
|
||||||
entityId = "custom.custom";
|
entityId = "custom.custom"; //TODO wtf??
|
||||||
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,9 +153,9 @@ class Entity {
|
|||||||
domain = rawData["entity_id"].split(".")[0];
|
domain = rawData["entity_id"].split(".")[0];
|
||||||
entityId = rawData["entity_id"];
|
entityId = rawData["entity_id"];
|
||||||
deviceClass = attributes["device_class"];
|
deviceClass = attributes["device_class"];
|
||||||
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
|
state = rawData["state"];
|
||||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
||||||
lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]);
|
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||||
entityPicture = _getEntityPictureUrl(webHost);
|
entityPicture = _getEntityPictureUrl(webHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,17 +221,17 @@ class Entity {
|
|||||||
|
|
||||||
String getAttribute(String attributeName) {
|
String getAttribute(String attributeName) {
|
||||||
if (attributes != null) {
|
if (attributes != null) {
|
||||||
return attributes["$attributeName"].toString();
|
return attributes["$attributeName"];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getLastUpdatedFormatted() {
|
String _getLastUpdatedFormatted() {
|
||||||
if (lastUpdatedTimestamp == null) {
|
if (_lastUpdated == null) {
|
||||||
return "-";
|
return "-";
|
||||||
} else {
|
} else {
|
||||||
DateTime now = DateTime.now();
|
DateTime now = DateTime.now();
|
||||||
Duration d = now.difference(lastUpdatedTimestamp);
|
Duration d = now.difference(_lastUpdated);
|
||||||
String text;
|
String text;
|
||||||
int v;
|
int v;
|
||||||
if (d.inDays == 0) {
|
if (d.inDays == 0) {
|
||||||
|
77
lib/entities/entity_colors.class.dart
Normal file
77
lib/entities/entity_colors.class.dart
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -50,29 +50,24 @@ class EntityIcon extends StatelessWidget {
|
|||||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||||
data.entity.deviceClass, data.entity.state); //
|
data.entity.deviceClass, data.entity.state); //
|
||||||
}
|
}
|
||||||
return Icon(
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
|
||||||
|
child: Icon(
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
size: size,
|
size: size,
|
||||||
color: color,
|
color: color,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
Color iconColor;
|
|
||||||
if (color != null) {
|
|
||||||
iconColor = color;
|
|
||||||
} else if (entityWrapper.stateColor) {
|
|
||||||
iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context);
|
|
||||||
} else {
|
|
||||||
iconColor = HAClientTheme().getOffStateColor(context);
|
|
||||||
}
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: buildIcon(
|
child: buildIcon(
|
||||||
entityWrapper,
|
entityWrapper,
|
||||||
iconColor
|
color ?? EntityColor.stateColor(entityWrapper.entity.state)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget {
|
|||||||
final bool handleTap;
|
final bool handleTap;
|
||||||
|
|
||||||
static EntityModel of(BuildContext context) {
|
static EntityModel of(BuildContext context) {
|
||||||
return context.dependOnInheritedWidgetOfExactType<EntityModel>();
|
return context.inheritFromWidgetOfExactType(EntityModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(EntityModel oldWidget) {
|
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||||
return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,24 +5,18 @@ 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.textStyle, this.wordsWrap: true, this.textAlign: TextAlign.left}) : super(key: key);
|
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
TextStyle tStyle;
|
TextStyle textStyle = TextStyle(fontSize: fontSize);
|
||||||
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,
|
||||||
@ -31,7 +25,7 @@ class EntityName extends StatelessWidget {
|
|||||||
overflow: textOverflow,
|
overflow: textOverflow,
|
||||||
softWrap: wordsWrap,
|
softWrap: wordsWrap,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
style: tStyle,
|
style: textStyle,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -16,8 +16,8 @@ class EntityPageLayout extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
showClose ?
|
showClose ?
|
||||||
Container(
|
Container(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Colors.blue[300],
|
||||||
height: 40,
|
height: 36,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -25,15 +25,19 @@ class EntityPageLayout extends StatelessWidget {
|
|||||||
padding: EdgeInsets.only(left: 8),
|
padding: EdgeInsets.only(left: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
entity.displayName,
|
entity.displayName,
|
||||||
style: Theme.of(context).primaryTextTheme.headline
|
style: TextStyle(
|
||||||
|
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: Theme.of(context).primaryTextTheme.headline.color,
|
color: Colors.white,
|
||||||
iconSize: 36.0,
|
iconSize: 30.0,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
eventBus.fire(ShowEntityPageEvent());
|
eventBus.fire(ShowEntityPageEvent());
|
||||||
},
|
},
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
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,48 +2,47 @@ part of '../main.dart';
|
|||||||
|
|
||||||
class EntityWrapper {
|
class EntityWrapper {
|
||||||
|
|
||||||
String overrideName;
|
String displayName;
|
||||||
final String overrideIcon;
|
String icon;
|
||||||
final bool stateColor;
|
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,
|
||||||
this.overrideIcon,
|
String icon,
|
||||||
this.overrideName,
|
String displayName,
|
||||||
this.stateColor: true,
|
this.uiAction
|
||||||
this.uiAction,
|
|
||||||
this.stateFilter
|
|
||||||
}) {
|
}) {
|
||||||
if (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleTap() {
|
void handleTap() {
|
||||||
switch (uiAction.tapAction) {
|
switch (uiAction.tapAction) {
|
||||||
case EntityUIAction.toggle: {
|
case EntityUIAction.toggle: {
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
eventBus.fire(
|
||||||
|
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.callService: {
|
case EntityUIAction.callService: {
|
||||||
if (uiAction.tapService != null) {
|
if (uiAction.tapService != null) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(
|
||||||
domain: uiAction.tapService.split(".")[0],
|
ServiceCallEvent(uiAction.tapService.split(".")[0],
|
||||||
service: uiAction.tapService.split(".")[1],
|
uiAction.tapService.split(".")[1], null,
|
||||||
data: uiAction.tapServiceData
|
uiAction.tapServiceData));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -59,7 +58,7 @@ class EntityWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.navigate: {
|
case EntityUIAction.navigate: {
|
||||||
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
|
if (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 {
|
||||||
@ -77,17 +76,17 @@ class EntityWrapper {
|
|||||||
void handleHold() {
|
void handleHold() {
|
||||||
switch (uiAction.holdAction) {
|
switch (uiAction.holdAction) {
|
||||||
case EntityUIAction.toggle: {
|
case EntityUIAction.toggle: {
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
eventBus.fire(
|
||||||
|
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.callService: {
|
case EntityUIAction.callService: {
|
||||||
if (uiAction.holdService != null) {
|
if (uiAction.holdService != null) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(
|
||||||
domain: uiAction.holdService.split(".")[0],
|
ServiceCallEvent(uiAction.holdService.split(".")[0],
|
||||||
service: uiAction.holdService.split(".")[1],
|
uiAction.holdService.split(".")[1], null,
|
||||||
data: uiAction.holdServiceData
|
uiAction.holdServiceData));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -99,7 +98,7 @@ class EntityWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.navigate: {
|
case EntityUIAction.navigate: {
|
||||||
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
|
if (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 {
|
||||||
@ -114,44 +113,4 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -24,12 +24,9 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpOscillate = oscillate;
|
_tmpOscillate = oscillate;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: "fan",
|
"fan", "oscillate", entity.entityId,
|
||||||
service: "oscillate",
|
{"oscillating": oscillate}));
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"oscillating": oscillate}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,12 +34,9 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpDirectionForward = forward;
|
_tmpDirectionForward = forward;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: "fan",
|
"fan", "set_direction", entity.entityId,
|
||||||
service: "set_direction",
|
{"direction": forward ? "forward" : "reverse"}));
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"direction": forward ? "forward" : "reverse"}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,12 +44,9 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpSpeed = value;
|
_tmpSpeed = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: "fan",
|
"fan", "set_speed", entity.entityId,
|
||||||
service: "set_speed",
|
{"speed": value}));
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"speed": value}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ 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,
|
||||||
@ -13,16 +14,17 @@ 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() {
|
||||||
ConnectionManager().callService(domain: serviceDomain, service: serviceName, entityId: entityId);
|
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: Theme.of(context).textTheme.subhead.fontSize*2.5,
|
height: fontSize*2.5,
|
||||||
child: FlatButton(
|
child: FlatButton(
|
||||||
onPressed: (() {
|
onPressed: (() {
|
||||||
_setNewState();
|
_setNewState();
|
||||||
@ -30,7 +32,8 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: HAClientTheme().getActionTextStyle(context),
|
style:
|
||||||
|
new TextStyle(fontSize: fontSize, color: Colors.blue),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -28,12 +28,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpBrightness = value.round();
|
_tmpBrightness = value.round();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain,
|
entity.domain, "turn_on", entity.entityId,
|
||||||
service: "turn_on",
|
{"brightness": _tmpBrightness}));
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"brightness": _tmpBrightness}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,12 +38,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpWhiteValue = value.round();
|
_tmpWhiteValue = value.round();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain,
|
entity.domain, "turn_on", entity.entityId,
|
||||||
service: "turn_on",
|
{"white_value": _tmpWhiteValue}));
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"white_value": _tmpWhiteValue}
|
|
||||||
);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -55,12 +49,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpColorTemp = value.round();
|
_tmpColorTemp = value.round();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain,
|
entity.domain, "turn_on", entity.entityId,
|
||||||
service: "turn_on",
|
{"color_temp": _tmpColorTemp}));
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"color_temp": _tmpColorTemp}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,12 +59,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpColor = color;
|
_tmpColor = color;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
|
||||||
domain: entity.domain,
|
eventBus.fire(new ServiceCallEvent(
|
||||||
service: "turn_on",
|
entity.domain, "turn_on", entity.entityId,
|
||||||
entityId: entity.entityId,
|
{"hs_color": [color.hue, color.saturation*100]}));
|
||||||
data: {"hs_color": [color.hue, color.saturation*100]}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,12 +71,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpEffect = value;
|
_tmpEffect = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
if (_tmpEffect != null) {
|
if (_tmpEffect != null) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain,
|
entity.domain, "turn_on", entity.entityId,
|
||||||
service: "turn_on",
|
{"effect": "$value"}));
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"effect": "$value"}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -183,7 +169,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
}
|
}
|
||||||
return UniversalSlider(
|
return UniversalSlider(
|
||||||
title: "Color temperature",
|
title: "Color temperature",
|
||||||
leading: Text("Cold", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.lightBlue)),
|
leading: Text("Cold", style: TextStyle(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 +180,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpColorTemp = value.round();
|
_tmpColorTemp = value.round();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
closing: Text("Warm", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.amberAccent),),
|
closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0.0, height: 0.0);
|
return Container(width: 0.0, height: 0.0);
|
||||||
@ -224,7 +210,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
color: savedColor?.toColor() ?? Theme.of(context).backgroundColor,
|
color: savedColor?.toColor() ?? Colors.transparent,
|
||||||
child: Text('Paste color'),
|
child: Text('Paste color'),
|
||||||
onPressed: savedColor == null ? null : () {
|
onPressed: savedColor == null ? null : () {
|
||||||
_setColor(entity, savedColor);
|
_setColor(entity, savedColor);
|
||||||
@ -241,6 +227,8 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
Widget _buildEffectControl(LightEntity entity) {
|
Widget _buildEffectControl(LightEntity entity) {
|
||||||
if ((entity.supportEffect) && (entity.effectList != null)) {
|
if ((entity.supportEffect) && (entity.effectList != null)) {
|
||||||
|
Logger.d("[LIGHT] entity effects: ${entity.effectList}");
|
||||||
|
Logger.d("[LIGHT] current effect: $_tmpEffect");
|
||||||
List<String> list = List.from(entity.effectList);
|
List<String> list = List.from(entity.effectList);
|
||||||
if (_tmpEffect!= null && !list.contains(_tmpEffect)) {
|
if (_tmpEffect!= null && !list.contains(_tmpEffect)) {
|
||||||
list.insert(0, _tmpEffect);
|
list.insert(0, _tmpEffect);
|
||||||
|
@ -7,11 +7,11 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
|
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
|
||||||
|
|
||||||
void _lock(Entity entity) {
|
void _lock(Entity entity) {
|
||||||
ConnectionManager().callService(domain: "lock", service: "lock", entityId: entity.entityId);
|
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unlock(Entity entity) {
|
void _unlock(Entity entity) {
|
||||||
ConnectionManager().callService(domain: "lock", service: "unlock", entityId: entity.entityId);
|
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -28,7 +28,8 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
onPressed: () => _unlock(entity),
|
onPressed: () => _unlock(entity),
|
||||||
child: Text("UNLOCK",
|
child: Text("UNLOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: HAClientTheme().getActionTextStyle(context)
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -38,7 +39,8 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
onPressed: () => _lock(entity),
|
onPressed: () => _lock(entity),
|
||||||
child: Text("LOCK",
|
child: Text("LOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: HAClientTheme().getActionTextStyle(context),
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -54,7 +56,8 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
entity.isLocked ? "UNLOCK" : "LOCK",
|
entity.isLocked ? "UNLOCK" : "LOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: HAClientTheme().getActionTextStyle(context),
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -84,11 +84,12 @@ 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);
|
||||||
@ -100,6 +101,8 @@ 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 = 0;
|
double progress;
|
||||||
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>(HAClientTheme().getOnStateColor(context)),
|
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,12 @@ 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();
|
||||||
@ -47,14 +53,15 @@ 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: Theme.of(context).accentColor,
|
color: Colors.orange,
|
||||||
|
focusColor: Colors.white,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(ServiceCallEvent(
|
||||||
domain: "media_player",
|
"media_player",
|
||||||
service: "media_seek",
|
"media_seek",
|
||||||
entityId: entity.entityId,
|
"${entity.entityId}",
|
||||||
data: {"seek_position": _savedPosition}
|
{"seek_position": _savedPosition}
|
||||||
);
|
));
|
||||||
setState(() {
|
setState(() {
|
||||||
_savedPosition = 0;
|
_savedPosition = 0;
|
||||||
});
|
});
|
||||||
@ -72,13 +79,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("00:00"),
|
Text("00:00"),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle),
|
||||||
"${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]}")
|
||||||
],
|
],
|
||||||
@ -86,7 +87,8 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
Container(height: 10,),
|
Container(height: 10,),
|
||||||
Slider(
|
Slider(
|
||||||
min: 0,
|
min: 0,
|
||||||
activeColor: Theme.of(context).accentColor,
|
activeColor: Colors.amber,
|
||||||
|
inactiveColor: Colors.black26,
|
||||||
max: entity.durationSeconds.toDouble(),
|
max: entity.durationSeconds.toDouble(),
|
||||||
value: _currentPosition,
|
value: _currentPosition,
|
||||||
onChangeStart: (val) {
|
onChangeStart: (val) {
|
||||||
@ -101,12 +103,12 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
_seekStarted = false;
|
_seekStarted = false;
|
||||||
Timer(Duration(milliseconds: 500), () {
|
Timer(Duration(milliseconds: 500), () {
|
||||||
if (!_seekStarted) {
|
if (!_seekStarted) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(ServiceCallEvent(
|
||||||
domain: "media_player",
|
"media_player",
|
||||||
service: "media_seek",
|
"media_seek",
|
||||||
entityId: entity.entityId,
|
"${entity.entityId}",
|
||||||
data: {"seek_position": val}
|
{"seek_position": val}
|
||||||
);
|
));
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
_currentPosition = val;
|
_currentPosition = val;
|
||||||
|
@ -12,14 +12,14 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
Stack(
|
Stack(
|
||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildImage(entity, context),
|
_buildImage(entity),
|
||||||
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, context),
|
child: _buildState(entity),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -35,9 +35,12 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildState(MediaPlayerEntity entity, BuildContext context) {
|
Widget _buildState(MediaPlayerEntity entity) {
|
||||||
TextStyle style = Theme.of(context).textTheme.body1.copyWith(
|
TextStyle style = TextStyle(
|
||||||
color: Colors.white
|
fontSize: 14.0,
|
||||||
|
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));
|
||||||
@ -68,7 +71,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImage(MediaPlayerEntity entity, BuildContext context) {
|
Widget _buildImage(MediaPlayerEntity entity) {
|
||||||
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(
|
||||||
@ -94,7 +97,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
||||||
size: 150.0,
|
size: 150.0,
|
||||||
color: HAClientTheme().getColorByEntityState("$state", context),
|
color: EntityColor.stateColor("$state"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -115,28 +118,26 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
|||||||
|
|
||||||
|
|
||||||
void _setPower(MediaPlayerEntity entity) {
|
void _setPower(MediaPlayerEntity entity) {
|
||||||
|
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
|
||||||
if (entity.state == EntityState.off) {
|
if (entity.state == EntityState.off) {
|
||||||
ConnectionManager().callService(
|
Logger.d("${entity.entityId} turn_on");
|
||||||
domain: entity.domain,
|
eventBus.fire(new ServiceCallEvent(
|
||||||
service: "turn_on",
|
entity.domain, "turn_on", entity.entityId,
|
||||||
entityId: entity.entityId
|
null));
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
ConnectionManager().callService(
|
Logger.d("${entity.entityId} turn_off");
|
||||||
domain: entity.domain,
|
eventBus.fire(new ServiceCallEvent(
|
||||||
service: "turn_off",
|
entity.domain, "turn_off", entity.entityId,
|
||||||
entityId: entity.entityId
|
null));
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _callAction(MediaPlayerEntity entity, String action) {
|
void _callAction(MediaPlayerEntity entity, String action) {
|
||||||
Logger.d("${entity.entityId} $action");
|
Logger.d("${entity.entityId} $action");
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: entity.domain,
|
entity.domain, "$action", entity.entityId,
|
||||||
service: "$action",
|
null));
|
||||||
entityId: entity.entityId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -263,50 +264,27 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
_newVolumeLevel = value;
|
_newVolumeLevel = value;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(ServiceCallEvent("media_player", "volume_set", entityId, {"volume_level": value}));
|
||||||
domain: "media_player",
|
|
||||||
service: "volume_set",
|
|
||||||
entityId: entityId,
|
|
||||||
data: {"volume_level": value}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setVolumeMute(bool isMuted, String entityId) {
|
void _setVolumeMute(bool isMuted, String entityId) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(ServiceCallEvent("media_player", "volume_mute", entityId, {"is_volume_muted": isMuted}));
|
||||||
domain: "media_player",
|
|
||||||
service: "volume_mute",
|
|
||||||
entityId: entityId,
|
|
||||||
data: {"is_volume_muted": isMuted}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setVolumeUp(String entityId) {
|
void _setVolumeUp(String entityId) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(ServiceCallEvent("media_player", "volume_up", entityId, null));
|
||||||
domain: "media_player",
|
|
||||||
service: "volume_up",
|
|
||||||
entityId: entityId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setVolumeDown(String entityId) {
|
void _setVolumeDown(String entityId) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(ServiceCallEvent("media_player", "volume_down", entityId, null));
|
||||||
domain: "media_player",
|
|
||||||
service: "volume_down",
|
|
||||||
entityId: entityId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setSoundMode(String value, String entityId) {
|
void _setSoundMode(String value, String entityId) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_newSoundMode = value;
|
_newSoundMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(ServiceCallEvent("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"}));
|
||||||
domain: "media_player",
|
|
||||||
service: "select_sound_mode",
|
|
||||||
entityId: entityId,
|
|
||||||
data: {"sound_mode": "$value"}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,12 +292,7 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_newSource = source;
|
_newSource = source;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
ConnectionManager().callService(
|
eventBus.fire(ServiceCallEvent("media_player", "select_source", entityId, {"source": "$source"}));
|
||||||
domain: "media_player",
|
|
||||||
service: "select_source",
|
|
||||||
entityId: entityId,
|
|
||||||
data: {"source": "$source"}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,13 +326,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:minus")),
|
|
||||||
onPressed: () => _setVolumeDown(entity.entityId)
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||||
onPressed: () => _setVolumeUp(entity.entityId)
|
onPressed: () => _setVolumeUp(entity.entityId)
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||||
|
onPressed: () => _setVolumeDown(entity.entityId)
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -457,16 +430,16 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _duplicateTo(entity) {
|
void _duplicateTo(entity) {
|
||||||
if (entity.canCalculateActualPosition()) {
|
|
||||||
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
|
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
|
||||||
|
if (MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
|
||||||
|
Navigator.of(context).popAndPushNamed("/play-media", arguments: {"url": entity.attributes["media_content_id"], "type": entity.attributes["media_content_type"]});
|
||||||
} else {
|
} 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"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _switchTo(entity) {
|
void _switchTo(entity) {
|
||||||
HomeAssistant().sendFromPlayerId = entity.entityId;
|
HomeAssistant().sendFromPlayerId = entity.entityId;
|
||||||
|
@ -11,12 +11,8 @@ class SelectStateWidget extends StatefulWidget {
|
|||||||
class _SelectStateWidgetState extends State<SelectStateWidget> {
|
class _SelectStateWidgetState extends State<SelectStateWidget> {
|
||||||
|
|
||||||
void setNewState(domain, entityId, newValue) {
|
void setNewState(domain, entityId, newValue) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
|
||||||
domain: domain,
|
{"option": "$newValue"}));
|
||||||
service: "select_option",
|
|
||||||
entityId: entityId,
|
|
||||||
data: {"option": "$newValue"}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -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 TextStyle textStyle;
|
final double fontSize;
|
||||||
//final bool bold;
|
final bool bold;
|
||||||
|
|
||||||
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);
|
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);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -22,19 +22,16 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
state = customValue;
|
state = customValue;
|
||||||
}
|
}
|
||||||
TextStyle tStyle;
|
TextStyle textStyle = TextStyle(
|
||||||
if (textStyle != null) {
|
fontSize: this.fontSize,
|
||||||
tStyle = textStyle;
|
fontWeight: FontWeight.normal
|
||||||
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) {
|
|
||||||
tStyle = Theme.of(context).textTheme.subhead.copyWith(
|
|
||||||
color: Colors.blue
|
|
||||||
);
|
);
|
||||||
} else {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||||
tStyle = Theme.of(context).textTheme.body1;
|
textStyle = textStyle.apply(color: Colors.blue);
|
||||||
}
|
}
|
||||||
/*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(" ", " ");
|
||||||
}
|
}
|
||||||
@ -46,7 +43,7 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
style: tStyle
|
style: textStyle
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
|
@ -18,12 +18,8 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
|||||||
_newValue = newValue;
|
_newValue = newValue;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
});
|
});
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
||||||
domain: domain,
|
{"value": "${newValue.toString()}"}));
|
||||||
service: "set_value",
|
|
||||||
entityId: entityId,
|
|
||||||
data: {"value": "${newValue.toString()}"}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -62,7 +58,8 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$_newValue",
|
"$_newValue",
|
||||||
style: Theme.of(context).textTheme.display1.copyWith(
|
style: TextStyle(
|
||||||
|
fontSize: Sizes.largeFontSize,
|
||||||
color: Colors.blue
|
color: Colors.blue
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -38,11 +38,8 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
|
|||||||
} else {
|
} else {
|
||||||
domain = entity.domain;
|
domain = entity.domain;
|
||||||
}
|
}
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(
|
||||||
domain: domain,
|
domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
|
||||||
service: (newValue as bool) ? "turn_on" : "turn_off",
|
|
||||||
entityId: entity.entityId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -26,12 +26,8 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
|||||||
|
|
||||||
void setNewState(newValue, domain, entityId) {
|
void setNewState(newValue, domain, entityId) {
|
||||||
if (validate(newValue, _minLength, _maxLength)) {
|
if (validate(newValue, _minLength, _maxLength)) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
||||||
domain: domain,
|
{"value": "$newValue"}));
|
||||||
service: "set_value",
|
|
||||||
entityId: entityId,
|
|
||||||
data: {"value": "$newValue"}
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tmpValue = _entityState;
|
_tmpValue = _entityState;
|
||||||
|
@ -27,7 +27,7 @@ class _TimerStateState extends State<TimerState> {
|
|||||||
try {
|
try {
|
||||||
int passed = DateTime
|
int passed = DateTime
|
||||||
.now()
|
.now()
|
||||||
.difference(entity.lastUpdatedTimestamp)
|
.difference(entity._lastUpdated)
|
||||||
.inSeconds;
|
.inSeconds;
|
||||||
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class UniversalSlider extends StatefulWidget {
|
class UniversalSlider extends StatelessWidget {
|
||||||
|
|
||||||
final onChanged;
|
final onChanged;
|
||||||
final onChangeEnd;
|
final onChangeEnd;
|
||||||
@ -14,69 +14,36 @@ class UniversalSlider extends StatefulWidget {
|
|||||||
|
|
||||||
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return UniversalSliderState();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class UniversalSliderState extends State<UniversalSlider> {
|
|
||||||
|
|
||||||
double _value;
|
|
||||||
bool _changeStarted = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_value = widget.value;
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List <Widget> row = [];
|
List <Widget> row = [];
|
||||||
if (widget.leading != null) {
|
if (leading != null) {
|
||||||
row.add(widget.leading);
|
row.add(leading);
|
||||||
}
|
}
|
||||||
row.add(
|
row.add(
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Slider(
|
child: Slider(
|
||||||
value: _value,
|
value: value,
|
||||||
min: widget.min,
|
min: min,
|
||||||
max: widget.max,
|
max: max,
|
||||||
onChangeStart: (_) {
|
onChanged: (value) => onChanged(value),
|
||||||
_changeStarted = true;
|
onChangeEnd: (value) => onChangeEnd(value),
|
||||||
},
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_value = value;
|
|
||||||
});
|
|
||||||
widget.onChanged(value);
|
|
||||||
},
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
_changeStarted = false;
|
|
||||||
Timer(Duration(milliseconds: 500), () {
|
|
||||||
if (!_changeStarted) {
|
|
||||||
setState(() {
|
|
||||||
_value = value;
|
|
||||||
});
|
|
||||||
widget.onChangeEnd(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (widget.closing != null) {
|
if (closing != null) {
|
||||||
row.add(widget.closing);
|
row.add(closing);
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: widget.padding,
|
padding: padding,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Text('${widget.title}'),
|
Text(
|
||||||
|
"$title",
|
||||||
|
style: TextStyle(fontSize: Sizes.stateFontSize),
|
||||||
|
),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -87,4 +54,5 @@ class UniversalSliderState extends State<UniversalSlider> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -10,7 +10,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildStatusAndBattery(entity, context),
|
_buildStatusAndBattery(entity),
|
||||||
_buildCommands(entity),
|
_buildCommands(entity),
|
||||||
_buildFanSpeed(entity),
|
_buildFanSpeed(entity),
|
||||||
_buildAdditionalInfo(entity)
|
_buildAdditionalInfo(entity)
|
||||||
@ -19,12 +19,12 @@ class VacuumControls extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatusAndBattery(VacuumEntity entity, BuildContext context) {
|
Widget _buildStatusAndBattery(VacuumEntity entity) {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (entity.supportStatus) {
|
if (entity.supportStatus) {
|
||||||
result.addAll(
|
result.addAll(
|
||||||
<Widget>[
|
<Widget>[
|
||||||
Text("Status:"),
|
Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),),
|
||||||
Container(width: 6,),
|
Container(width: 6,),
|
||||||
Expanded(
|
Expanded(
|
||||||
//flex: 1,
|
//flex: 1,
|
||||||
@ -33,7 +33,10 @@ class VacuumControls extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.body2,
|
style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -45,7 +48,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 %")
|
Text("$batteryLevel %", style: TextStyle(fontSize: Sizes.stateFontSize))
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -169,7 +172,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:"),
|
Text("Vacuum cleaner commands:", style: TextStyle(fontSize: Sizes.stateFontSize)),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
@ -194,7 +197,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
domain: "vacuum",
|
domain: "vacuum",
|
||||||
entityId: entity.entityId,
|
entityId: entity.entityId,
|
||||||
service: "set_fan_speed",
|
service: "set_fan_speed",
|
||||||
data: {"fan_speed": val}
|
additionalServiceData: {"fan_speed": val}
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -27,7 +27,10 @@ class VacuumStateButton extends StatelessWidget {
|
|||||||
text: "RETURN TO DOCK"
|
text: "RETURN TO DOCK"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = Text(entity.state.toUpperCase(), style: Theme.of(context).textTheme.subhead);
|
result = Text(entity.state.toUpperCase(), style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(right: 15),
|
padding: EdgeInsets.only(right: 15),
|
||||||
|
@ -152,12 +152,39 @@ class EntityCollection {
|
|||||||
return _allEntities[entityId] != null;
|
return _allEntities[entityId] != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
|
List<Entity> getByDomains({List<String> domains, List<String> stateFiler}) {
|
||||||
return _allEntities.values.where((entity) {
|
return _allEntities.values.where((entity) {
|
||||||
return
|
return domains.contains(entity.domain) &&
|
||||||
(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,8 +2,6 @@ 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() {
|
||||||
@ -13,73 +11,69 @@ class HomeAssistant {
|
|||||||
EntityCollection entities;
|
EntityCollection entities;
|
||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
|
Map services;
|
||||||
String _userName;
|
String _userName;
|
||||||
String _lovelaceDashbordUrl;
|
bool childMode;
|
||||||
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 (!autoUi) {
|
if (ConnectionManager().useLovelace) {
|
||||||
return ui?.title ?? "Home";
|
return ui?.title ?? "";
|
||||||
} else {
|
} else {
|
||||||
return _instanceConfig["location_name"] ?? "Home";
|
return _instanceConfig["location_name"] ?? "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String get userName => _userName ?? locationName;
|
String get userName => _userName ?? locationName;
|
||||||
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||||
bool get isNoEntities => entities == null || entities.isEmpty;
|
bool get isNoEntities => entities == null || entities.isEmpty;
|
||||||
bool get isNoViews => ui == null || ui.isEmpty;
|
bool get isNoViews => ui == null || ui.isEmpty;
|
||||||
|
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
||||||
|
|
||||||
HomeAssistant._internal() {
|
HomeAssistant._internal() {
|
||||||
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||||
ConnectionManager().onLovelaceUpdatedCallback = _handleLovelaceUpdate;
|
|
||||||
DeviceInfoManager().loadDeviceInfo();
|
DeviceInfoManager().loadDeviceInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Completer _fetchCompleter;
|
Completer _fetchCompleter;
|
||||||
|
|
||||||
Future fetchData(bool uiOnly) {
|
Future fetchData() {
|
||||||
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 = [];
|
||||||
if (!uiOnly) {
|
futures.add(_getStates());
|
||||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
if (ConnectionManager().useLovelace) {
|
||||||
futures.add(_getStates(null));
|
futures.add(_getLovelace());
|
||||||
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 (isComponentEnabled('mobile_app')) {
|
if (isMobileAppEnabled) {
|
||||||
_createUI();
|
if (!childMode) _createUI();
|
||||||
_fetchCompleter.complete();
|
_fetchCompleter.complete();
|
||||||
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
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.app/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.homemade.systems/docs#mobile-app-integration")]));
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_fetchCompleter.completeError(e);
|
_fetchCompleter.completeError(e);
|
||||||
@ -87,48 +81,6 @@ 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 (isComponentEnabled('mobile_app')) {
|
|
||||||
_createUI();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Logger.d('Didnt get cached data: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveCache() async {
|
|
||||||
Logger.d('Saving data to cache...');
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
try {
|
|
||||||
await prefs.setString('cached_states', json.encode(_rawStates));
|
|
||||||
await prefs.setString('cached_lovelace', json.encode(_rawLovelaceData));
|
|
||||||
await prefs.setString('cached_user', json.encode(_rawUserInfo));
|
|
||||||
await prefs.setString('cached_config', json.encode(_instanceConfig));
|
|
||||||
await prefs.setString('cached_panels', json.encode(_rawPanels));
|
|
||||||
await prefs.setString('cached_services', json.encode(services));
|
|
||||||
await prefs.setBool('cached', true);
|
|
||||||
} catch (e) {
|
|
||||||
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((_) {
|
||||||
@ -138,186 +90,71 @@ class HomeAssistant {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getConfig(SharedPreferences sharedPrefs) async {
|
Future _getConfig() async {
|
||||||
if (sharedPrefs != null) {
|
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||||
try {
|
_instanceConfig = Map.from(data);
|
||||||
var data = json.decode(sharedPrefs.getString('cached_config'));
|
}).catchError((e) {
|
||||||
_parseConfig(data);
|
throw HAError("Error getting config: ${e}");
|
||||||
} 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");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void _parseConfig(data) {
|
Future _getStates() async {
|
||||||
_instanceConfig = Map.from(data);
|
|
||||||
Logger.d('stream: ${_instanceConfig['components'].contains('stream')}');
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => _parseStates(data)
|
(data) => entities.parse(data)
|
||||||
).catchError((e) {
|
).catchError((e) {
|
||||||
throw HAError("Error getting states: $e");
|
throw HAError("Error getting states: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void _parseStates(data) {
|
Future _getLovelace() async {
|
||||||
_rawStates = data;
|
await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
||||||
entities.parse(data);
|
throw HAError("Error getting lovelace config: $e");
|
||||||
}
|
|
||||||
|
|
||||||
Future _getLovelace(SharedPreferences sharedPrefs) {
|
|
||||||
if (sharedPrefs != null) {
|
|
||||||
try {
|
|
||||||
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
|
|
||||||
_rawLovelaceData = data;
|
|
||||||
} catch (e) {
|
|
||||||
autoUi = true;
|
|
||||||
}
|
|
||||||
return Future.value();
|
|
||||||
} else {
|
|
||||||
Completer completer = Completer();
|
|
||||||
var additionalData;
|
|
||||||
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
|
|
||||||
additionalData = {
|
|
||||||
'url_path': _lovelaceDashbordUrl
|
|
||||||
};
|
|
||||||
}
|
|
||||||
ConnectionManager().sendSocketMessage(
|
|
||||||
type: 'lovelace/config',
|
|
||||||
additionalData: additionalData
|
|
||||||
).then((data) {
|
|
||||||
_rawLovelaceData = data;
|
|
||||||
completer.complete();
|
|
||||||
}).catchError((e) {
|
|
||||||
if ("$e" == "config_not_found") {
|
|
||||||
autoUi = true;
|
|
||||||
_rawLovelaceData = null;
|
|
||||||
completer.complete();
|
|
||||||
} else {
|
|
||||||
completer.completeError(HAError("Error getting lovelace config: $e"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getServices(SharedPreferences prefs) async {
|
|
||||||
if (prefs != null) {
|
|
||||||
try {
|
|
||||||
var data = json.decode(prefs.getString('cached_services'));
|
|
||||||
_parseServices(data);
|
|
||||||
} catch (e) {
|
|
||||||
Logger.w("Can't get services: $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
|
|
||||||
Logger.w("Can't get services: $e");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseServices(data) {
|
Future _getUserInfo() async {
|
||||||
services = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getUserInfo(SharedPreferences sharedPrefs) async {
|
|
||||||
_userName = null;
|
_userName = null;
|
||||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
|
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");
|
Logger.w("Can't get user info: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseUserInfo(data) {
|
Future _getServices() async {
|
||||||
_rawUserInfo = data;
|
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
|
||||||
_userName = data["name"];
|
Logger.d("Got ${data.length} services");
|
||||||
}
|
Logger.d("Media extractor: ${data["media_extractor"]}");
|
||||||
|
services = data;
|
||||||
Future _getPanels(SharedPreferences sharedPrefs) async {
|
}).catchError((e) {
|
||||||
if (sharedPrefs != null) {
|
Logger.w("Can't get services: $e");
|
||||||
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");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void _parsePanels(data) {
|
Future _getPanels() async {
|
||||||
_rawPanels = data;
|
|
||||||
panels.clear();
|
panels.clear();
|
||||||
List<Panel> dashboards = [];
|
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
|
||||||
data.forEach((k,v) {
|
data.forEach((k,v) {
|
||||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
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') {
|
panels.add(Panel(
|
||||||
dashboards.add(
|
|
||||||
Panel(
|
|
||||||
id: k,
|
id: k,
|
||||||
componentName: v['component_name'],
|
type: v["component_name"],
|
||||||
title: title,
|
title: title,
|
||||||
urlPath: v['url_path'],
|
urlPath: v["url_path"],
|
||||||
config: v['config'],
|
config: v["config"],
|
||||||
icon: (v['icon'] == null || v['icon'] == 'hass:view-dashboard') ? 'mdi:view-dashboard' : v['icon']
|
icon: 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) {
|
}).catchError((e) {
|
||||||
completer.completeError(e);
|
throw HAError("Error getting panels list: $e");
|
||||||
});
|
});
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isComponentEnabled(String name) {
|
|
||||||
return _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("$name");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 != null && _fetchCompleter.isCompleted) {
|
if (_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"],
|
||||||
@ -326,26 +163,203 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isServiceExist(String service) {
|
void _parseLovelace() {
|
||||||
return services != null &&
|
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
||||||
services.isNotEmpty &&
|
ui.title = _rawLovelaceData["title"];
|
||||||
services.containsKey(service);
|
int viewCounter = 0;
|
||||||
|
Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
|
||||||
|
_rawLovelaceData["views"].forEach((rawView){
|
||||||
|
Logger.d("----view id: ${rawView['id']}");
|
||||||
|
HAView view = HAView(
|
||||||
|
count: viewCounter,
|
||||||
|
id: "${rawView['id']}",
|
||||||
|
name: rawView['title'],
|
||||||
|
iconName: rawView['icon'],
|
||||||
|
panel: rawView['panel'] ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rawView['badges'] != null && rawView['badges'] is List) {
|
||||||
|
rawView['badges'].forEach((entity) {
|
||||||
|
if (entities.isExist(entity)) {
|
||||||
|
Entity e = entities.get(entity);
|
||||||
|
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: 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");
|
||||||
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
|
_parseLovelace();
|
||||||
/*if (isServiceExist('zha_map')) {
|
} else {
|
||||||
panels.add(
|
Logger.d("Creating group-based UI");
|
||||||
Panel(
|
int viewCounter = 0;
|
||||||
id: 'haclient_zha',
|
if (!entities.hasDefaultView) {
|
||||||
componentName: 'haclient_zha',
|
HAView view = HAView(
|
||||||
title: 'ZHA',
|
count: viewCounter,
|
||||||
urlPath: '/haclient_zha',
|
id: "group.default_view",
|
||||||
icon: 'mdi:zigbee'
|
name: "Home",
|
||||||
)
|
childEntities: entities.filterEntitiesForDefaultView()
|
||||||
);
|
);
|
||||||
}*/
|
ui.views.add(
|
||||||
|
view
|
||||||
|
);
|
||||||
|
viewCounter += 1;
|
||||||
|
}
|
||||||
|
entities.viewEntities.forEach((viewEntity) {
|
||||||
|
HAView view = HAView(
|
||||||
|
count: viewCounter,
|
||||||
|
id: viewEntity.entityId,
|
||||||
|
name: viewEntity.displayName,
|
||||||
|
childEntities: viewEntity.childEntities
|
||||||
|
);
|
||||||
|
view.linkedEntity = viewEntity;
|
||||||
|
ui.views.add(
|
||||||
|
view
|
||||||
|
);
|
||||||
|
viewCounter += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildViews(BuildContext context, TabController tabController) {
|
||||||
|
return ui.build(context, tabController);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
187
lib/main.dart
187
lib/main.dart
@ -1,7 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
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';
|
||||||
@ -14,6 +13,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:date_format/date_format.dart';
|
import 'package:date_format/date_format.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:charts_flutter/flutter.dart' as charts;
|
import 'package:charts_flutter/flutter.dart' as charts;
|
||||||
|
import 'package:progress_indicators/progress_indicators.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
@ -21,19 +21,17 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
import 'package:device_info/device_info.dart';
|
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 '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';
|
||||||
@ -62,7 +60,8 @@ part 'entities/badge.widget.dart';
|
|||||||
part 'entities/entity_model.widget.dart';
|
part 'entities/entity_model.widget.dart';
|
||||||
part 'entities/default_entity_container.widget.dart';
|
part 'entities/default_entity_container.widget.dart';
|
||||||
part 'entities/missed_entity.widget.dart';
|
part 'entities/missed_entity.widget.dart';
|
||||||
part 'cards/entity_button_card.dart';
|
part 'cards/widgets/glance_card_entity_container.dart';
|
||||||
|
part 'cards/widgets/entity_button_card_body.widget.dart';
|
||||||
part 'pages/widgets/entity_attributes_list.dart';
|
part 'pages/widgets/entity_attributes_list.dart';
|
||||||
part 'entities/entity_icon.widget.dart';
|
part 'entities/entity_icon.widget.dart';
|
||||||
part 'entities/entity_name.widget.dart';
|
part 'entities/entity_name.widget.dart';
|
||||||
@ -73,6 +72,7 @@ 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,7 +84,6 @@ 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';
|
||||||
@ -100,18 +99,14 @@ part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.d
|
|||||||
part 'entities/vacuum/vacuum_entity.class.dart';
|
part 'entities/vacuum/vacuum_entity.class.dart';
|
||||||
part 'entities/vacuum/widgets/vacuum_controls.dart';
|
part 'entities/vacuum/widgets/vacuum_controls.dart';
|
||||||
part 'entities/vacuum/widgets/vacuum_state_button.dart';
|
part 'entities/vacuum/widgets/vacuum_state_button.dart';
|
||||||
part 'pages/settings/connection_settings.part.dart';
|
part 'pages/settings.page.dart';
|
||||||
part 'pages/purchase.page.dart';
|
part 'pages/purchase.page.dart';
|
||||||
part 'pages/widgets/product_purchase.widget.dart';
|
part 'pages/widgets/product_purchase.widget.dart';
|
||||||
part 'pages/widgets/page_loading_indicator.dart';
|
part 'pages/widgets/page_loading_indicator.dart';
|
||||||
part 'pages/widgets/bottom_info_bar.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/main.page.dart';
|
part 'pages/main.page.dart';
|
||||||
part 'pages/settings/integration_settings.part.dart';
|
part 'pages/integration_settings.page.dart';
|
||||||
part 'pages/settings/app_settings.page.dart';
|
|
||||||
part 'pages/settings/lookandfeel_settings.part.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';
|
||||||
@ -123,190 +118,80 @@ 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';
|
||||||
part 'panels/panel_class.dart';
|
part 'panels/panel_class.dart';
|
||||||
part 'viewWidget.widget.dart';
|
part 'viewWidget.widget.dart';
|
||||||
|
part 'cards/card_widget.dart';
|
||||||
part 'cards/widgets/card_header.widget.dart';
|
part 'cards/widgets/card_header.widget.dart';
|
||||||
part 'panels/config_panel_widget.dart';
|
part 'panels/config_panel_widget.dart';
|
||||||
part 'panels/widgets/link_to_web_config.dart';
|
part 'panels/widgets/link_to_web_config.dart';
|
||||||
part 'types/ha_error.dart';
|
part 'types/ha_error.dart';
|
||||||
part 'types/event_bus_events.dart';
|
part 'types/event_bus_events.dart';
|
||||||
part 'cards/gauge_card.dart';
|
part 'cards/widgets/gauge_card_body.dart';
|
||||||
part 'cards/widgets/card_wrapper.widget.dart';
|
part 'cards/widgets/light_card_body.dart';
|
||||||
part 'cards/entities_card.dart';
|
|
||||||
part 'cards/alarm_panel_card.dart';
|
|
||||||
part 'cards/horizontal_srack_card.dart';
|
|
||||||
part 'cards/markdown_card.dart';
|
|
||||||
part 'cards/media_control_card.dart';
|
|
||||||
part 'cards/unsupported_card.dart';
|
|
||||||
part 'cards/vertical_stack_card.dart';
|
|
||||||
part 'cards/glance_card.dart';
|
|
||||||
part 'pages/play_media.page.dart';
|
part 'pages/play_media.page.dart';
|
||||||
part 'entities/entity_page_layout.widget.dart';
|
part 'entities/entity_page_layout.widget.dart';
|
||||||
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
|
part 'entities/media_player/widgets/media_player_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.8.5";
|
const appVersionNumber = "0.7.0";
|
||||||
const appVersionAdd = "";
|
const appVersionAdd = "";
|
||||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
const appVersion = "$appVersionNumber-$appVersionAdd";
|
||||||
|
|
||||||
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
|
||||||
// Print the exception to the console.
|
|
||||||
if (Logger.isInDebugMode) {
|
|
||||||
Logger.e('Caught error: $error');
|
|
||||||
Logger.p(stackTrace);
|
|
||||||
}
|
|
||||||
Crashlytics.instance.recordError(error, stackTrace);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
Crashlytics.instance.enableInDevMode = true;
|
FlutterError.onError = (errorDetails) {
|
||||||
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
|
Logger.e( "${errorDetails.exception}");
|
||||||
|
|
||||||
FlutterError.onError = (FlutterErrorDetails details) {
|
|
||||||
Logger.e("Caut Flutter runtime error: ${details.exception}");
|
|
||||||
if (Logger.isInDebugMode) {
|
if (Logger.isInDebugMode) {
|
||||||
FlutterError.dumpErrorToConsole(details);
|
FlutterError.dumpErrorToConsole(errorDetails);
|
||||||
}
|
}
|
||||||
Crashlytics.instance.recordFlutterError(details);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
|
|
||||||
|
|
||||||
runZoned(() {
|
runZoned(() {
|
||||||
runApp(new HAClientApp(
|
|
||||||
theme: theme,
|
|
||||||
));
|
|
||||||
}, onError: (error, stack) {
|
|
||||||
_reportError(error, stack);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class HAClientApp extends StatefulWidget {
|
|
||||||
|
|
||||||
final AppTheme theme;
|
|
||||||
|
|
||||||
const HAClientApp({Key key, this.theme: AppTheme.defaultTheme}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_HAClientAppState createState() => new _HAClientAppState();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HAClientAppState extends State<HAClientApp> {
|
|
||||||
StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription;
|
|
||||||
StreamSubscription _themeChangeSubscription;
|
|
||||||
AppTheme _currentTheme = AppTheme.defaultTheme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
InAppPurchaseConnection.enablePendingPurchases();
|
|
||||||
final Stream purchaseUpdates =
|
|
||||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
|
||||||
_purchaseUpdateSubscription = purchaseUpdates.listen((purchases) {
|
|
||||||
_handlePurchaseUpdates(purchases);
|
|
||||||
});
|
|
||||||
_currentTheme = widget.theme;
|
|
||||||
_themeChangeSubscription = eventBus.on<ChangeThemeEvent>().listen((event){
|
|
||||||
setState(() {
|
|
||||||
_currentTheme = event.theme;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
workManager.Workmanager.initialize(
|
workManager.Workmanager.initialize(
|
||||||
updateDeviceLocationIsolate,
|
updateDeviceLocationIsolate,
|
||||||
isInDebugMode: false
|
isInDebugMode: false
|
||||||
);
|
);
|
||||||
super.initState();
|
runApp(new HAClientApp());
|
||||||
|
|
||||||
|
}, onError: (error, stack) {
|
||||||
|
Logger.e("$error");
|
||||||
|
Logger.e("$stack");
|
||||||
|
if (Logger.isInDebugMode) {
|
||||||
|
debugPrint("$stack");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handlePurchaseUpdates(purchase) {
|
class HAClientApp extends StatelessWidget {
|
||||||
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: HAClientTheme().getThemeData(_currentTheme),
|
theme: new ThemeData(
|
||||||
darkTheme: HAClientTheme().darkTheme,
|
primarySwatch: Colors.blue,
|
||||||
debugShowCheckedModeBanner: false,
|
),
|
||||||
initialRoute: "/",
|
initialRoute: "/",
|
||||||
routes: {
|
routes: {
|
||||||
"/": (context) => MainPage(title: 'HA Client'),
|
"/": (context) => MainPage(title: 'HA Client'),
|
||||||
"/app-settings": (context) => AppSettingsPage(),
|
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||||
"/connection-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.connectionSettings),
|
"/integration-settings": (context) => IntegrationSettingsPage(title: "Integration settings"),
|
||||||
"/integration-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.integrationSettings),
|
|
||||||
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
||||||
"/play-media": (context) => PlayMediaPage(
|
"/play-media": (context) => PlayMediaPage(
|
||||||
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
|
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
|
||||||
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
|
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"),
|
||||||
"/webview": (context) => standaloneWebview.WebviewScaffold(
|
"/whats-new": (context) => WhatsNewPage()
|
||||||
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() {
|
|
||||||
_purchaseUpdateSubscription.cancel();
|
|
||||||
_themeChangeSubscription.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -9,37 +9,46 @@ class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AuthManager._internal();
|
AuthManager._internal();
|
||||||
|
StreamSubscription deepLinksSubscription;
|
||||||
|
|
||||||
Future start({String oauthUrl}) {
|
Future start({String oauthUrl}) {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
final flutterWebviewPlugin = new standaloneWebview.FlutterWebviewPlugin();
|
deepLinksSubscription?.cancel();
|
||||||
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
deepLinksSubscription = getUriLinksStream().listen((Uri uri) {
|
||||||
if (url.startsWith("https://ha-client.app/service/auth_callback.html")) {
|
Logger.d("[LINKED AUTH] We got something private");
|
||||||
Logger.d("url=$url");
|
_getTempToken(oauthUrl, uri.queryParameters["code"])
|
||||||
String authCode = url.split("=")[1];
|
.then((tempToken) => completer.complete(tempToken))
|
||||||
Logger.d("authCode=$authCode");
|
.catchError((_){
|
||||||
Logger.d("We have auth code. Getting temporary access token...");
|
completer.completeError(HAError("Auth error"));
|
||||||
ConnectionManager().sendHTTPPost(
|
});
|
||||||
endPoint: "/auth/token",
|
}, onError: (err) {
|
||||||
contentType: "application/x-www-form-urlencoded",
|
Logger.e("[LINKED AUTH] Error handling linked auth: $e");
|
||||||
includeAuthHeader: false,
|
completer.completeError(HAError("Auth error"));
|
||||||
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,8 +19,8 @@ class ConnectionManager {
|
|||||||
String _tempToken;
|
String _tempToken;
|
||||||
String oauthUrl;
|
String oauthUrl;
|
||||||
String webhookId;
|
String webhookId;
|
||||||
|
bool useLovelace = true;
|
||||||
bool settingsLoaded = false;
|
bool settingsLoaded = false;
|
||||||
int appIntegrationVersion;
|
|
||||||
bool get isAuthenticated => _token != null;
|
bool get isAuthenticated => _token != null;
|
||||||
StreamSubscription _socketSubscription;
|
StreamSubscription _socketSubscription;
|
||||||
Duration connectTimeout = Duration(seconds: 15);
|
Duration connectTimeout = Duration(seconds: 15);
|
||||||
@ -28,7 +28,6 @@ class ConnectionManager {
|
|||||||
bool isConnected = false;
|
bool isConnected = false;
|
||||||
|
|
||||||
var onStateChangeCallback;
|
var onStateChangeCallback;
|
||||||
var onLovelaceUpdatedCallback;
|
|
||||||
|
|
||||||
IOWebSocketChannel _socket;
|
IOWebSocketChannel _socket;
|
||||||
|
|
||||||
@ -39,12 +38,12 @@ class ConnectionManager {
|
|||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
bool stopInit = false;
|
bool stopInit = false;
|
||||||
if (loadSettings) {
|
if (loadSettings) {
|
||||||
Logger.d("Loading settings...");
|
Logger.e("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');
|
||||||
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
|
|
||||||
displayHostname = "$_domain:$_port";
|
displayHostname = "$_domain:$_port";
|
||||||
_webSocketAPIEndpoint =
|
_webSocketAPIEndpoint =
|
||||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||||
@ -60,9 +59,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(
|
||||||
'https://ha-client.app')}&redirect_uri=${Uri
|
'http://ha-client.homemade.systems')}&redirect_uri=${Uri
|
||||||
.encodeComponent(
|
.encodeComponent(
|
||||||
'https://ha-client.app/service/auth_callback.html')}";
|
'haclient://auth')}";
|
||||||
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()]));
|
||||||
@ -99,23 +98,16 @@ class ConnectionManager {
|
|||||||
|
|
||||||
void _doConnect({Completer completer, bool forceReconnect}) {
|
void _doConnect({Completer completer, bool forceReconnect}) {
|
||||||
if (forceReconnect || !isConnected) {
|
if (forceReconnect || !isConnected) {
|
||||||
|
_connect().timeout(connectTimeout, onTimeout: () {
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
_connect().timeout(connectTimeout).then((_) {
|
if (completer != null && !completer.isCompleted) {
|
||||||
|
completer.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();
|
||||||
@ -132,7 +124,6 @@ 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(
|
||||||
@ -149,21 +140,11 @@ 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()]));
|
||||||
@ -177,9 +158,6 @@ 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;
|
||||||
}
|
}
|
||||||
@ -211,19 +189,18 @@ class ConnectionManager {
|
|||||||
//Logger.d("[Received] <== Request id ${data['id']} was successful");
|
//Logger.d("[Received] <== Request id ${data['id']} was successful");
|
||||||
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
||||||
} else if (data["id"] != null) {
|
} else if (data["id"] != null) {
|
||||||
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
//Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
||||||
_messageResolver["${data["id"]}"]?.completeError("${data["error"]["code"]}");
|
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
|
||||||
}
|
}
|
||||||
_messageResolver.remove("${data["id"]}");
|
_messageResolver.remove("${data["id"]}");
|
||||||
} else if (data["type"] == "event") {
|
} else if (data["type"] == "event") {
|
||||||
if (data["event"] != null) {
|
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||||
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"]["event_type"] == "lovelace_updated") {
|
} else if (data["event"] != null) {
|
||||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: $data");
|
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
|
||||||
onLovelaceUpdatedCallback();
|
} else {
|
||||||
}
|
Logger.e("Event is null: $data");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.d("[Received unhandled] <== ${data.toString()}");
|
Logger.d("[Received unhandled] <== ${data.toString()}");
|
||||||
@ -232,24 +209,38 @@ 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("Disconnected", actions: [HAErrorAction.reconnect()]));
|
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
|
||||||
}
|
} else {
|
||||||
|
_disconnect().then((_) {
|
||||||
|
Timer(Duration(seconds: 5), () {
|
||||||
|
Logger.d("Trying to reconnect...");
|
||||||
|
_connect().catchError((e) {
|
||||||
|
isConnected = false;
|
||||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _authenticate() {
|
Future _authenticate() {
|
||||||
@ -338,13 +329,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).then((_) {
|
_connect().timeout(connectTimeout, onTimeout: (){
|
||||||
|
_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) {
|
||||||
if (!_completer.isCompleted) {
|
_completer.completeError(e);
|
||||||
_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}");
|
||||||
@ -357,27 +348,25 @@ class ConnectionManager {
|
|||||||
_currentMessageId += 1;
|
_currentMessageId += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future callService({@required String domain, @required String service, entityId, Map data}) {
|
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
|
||||||
eventBus.fire(NotifyServiceCallEvent(domain, service, entityId));
|
|
||||||
Logger.d("Service call: $domain.$service, $entityId, $data");
|
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
Map serviceData = {};
|
Map serviceData = {};
|
||||||
if (entityId != null) {
|
if (entityId != null) {
|
||||||
serviceData["entity_id"] = entityId;
|
serviceData["entity_id"] = entityId;
|
||||||
}
|
}
|
||||||
if (data != null && data.isNotEmpty) {
|
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
|
||||||
serviceData.addAll(data);
|
serviceData.addAll(additionalServiceData);
|
||||||
}
|
}
|
||||||
if (serviceData.isNotEmpty)
|
if (serviceData.isNotEmpty)
|
||||||
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.toString())));
|
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));
|
||||||
//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.toString())));
|
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));;
|
||||||
//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;
|
||||||
}
|
}
|
||||||
@ -418,12 +407,11 @@ class ConnectionManager {
|
|||||||
headers: headers,
|
headers: headers,
|
||||||
body: data
|
body: data
|
||||||
).then((response) {
|
).then((response) {
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
|
||||||
Logger.d("[Received] <== HTTP ${response.statusCode}");
|
Logger.d("[Received] <== HTTP ${response.statusCode}");
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
||||||
completer.complete(response.body);
|
completer.complete(response.body);
|
||||||
} else {
|
} else {
|
||||||
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);
|
||||||
|
@ -14,7 +14,7 @@ class LocationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final int defaultUpdateIntervalMinutes = 20;
|
final int defaultUpdateIntervalMinutes = 20;
|
||||||
final String backgroundTaskId = "haclocationtask0";
|
final String backgroundTaskId = "haclocationtask4352";
|
||||||
final String backgroundTaskTag = "haclocation";
|
final String backgroundTaskTag = "haclocation";
|
||||||
Duration _updateInterval;
|
Duration _updateInterval;
|
||||||
bool _isRunning;
|
bool _isRunning;
|
||||||
@ -57,110 +57,76 @@ class LocationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_startLocationService() async {
|
_startLocationService() async {
|
||||||
|
Logger.d("Scheduling location update for every ${_updateInterval
|
||||||
|
.inMinutes} minutes...");
|
||||||
String webhookId = ConnectionManager().webhookId;
|
String webhookId = ConnectionManager().webhookId;
|
||||||
String httpWebHost = ConnectionManager().httpWebHost;
|
String httpWebHost = ConnectionManager().httpWebHost;
|
||||||
if (webhookId != null && webhookId.isNotEmpty) {
|
if (webhookId != null && webhookId.isNotEmpty) {
|
||||||
Duration interval;
|
|
||||||
int delayFactor;
|
|
||||||
int taskCount;
|
|
||||||
Logger.d("Starting location update for every ${_updateInterval
|
|
||||||
.inMinutes} minutes...");
|
|
||||||
if (_updateInterval.inMinutes == 10) {
|
|
||||||
interval = Duration(minutes: 20);
|
|
||||||
taskCount = 2;
|
|
||||||
delayFactor = 10;
|
|
||||||
} else if (_updateInterval.inMinutes == 5) {
|
|
||||||
interval = Duration(minutes: 15);
|
|
||||||
taskCount = 3;
|
|
||||||
delayFactor = 5;
|
|
||||||
} else {
|
|
||||||
interval = _updateInterval;
|
|
||||||
taskCount = 1;
|
|
||||||
delayFactor = 0;
|
|
||||||
}
|
|
||||||
for (int i = 1; i <= taskCount; i++) {
|
|
||||||
int delay = i*delayFactor;
|
|
||||||
Logger.d("Scheduling location update task #$i for every ${interval.inMinutes} minutes in $delay minutes...");
|
|
||||||
await workManager.Workmanager.registerPeriodicTask(
|
await workManager.Workmanager.registerPeriodicTask(
|
||||||
"$backgroundTaskId$i",
|
backgroundTaskId,
|
||||||
"haClientLocationTracking-0$i",
|
"haClientLocationTracking",
|
||||||
tag: backgroundTaskTag,
|
tag: backgroundTaskTag,
|
||||||
inputData: {
|
inputData: {
|
||||||
"webhookId": webhookId,
|
"webhookId": webhookId,
|
||||||
"httpWebHost": httpWebHost
|
"httpWebHost": httpWebHost
|
||||||
},
|
},
|
||||||
frequency: interval,
|
frequency: _updateInterval,
|
||||||
initialDelay: Duration(minutes: delay),
|
|
||||||
existingWorkPolicy: workManager.ExistingWorkPolicy.keep,
|
existingWorkPolicy: workManager.ExistingWorkPolicy.keep,
|
||||||
backoffPolicy: workManager.BackoffPolicy.linear,
|
backoffPolicy: workManager.BackoffPolicy.linear,
|
||||||
backoffPolicyDelay: interval,
|
backoffPolicyDelay: _updateInterval,
|
||||||
constraints: workManager.Constraints(
|
constraints: workManager.Constraints(
|
||||||
networkType: workManager.NetworkType.connected,
|
networkType: workManager.NetworkType.connected
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_stopLocationService() async {
|
_stopLocationService() async {
|
||||||
Logger.d("Canceling previous schedule if any...");
|
Logger.d("Canceling previous schedule if any...");
|
||||||
await workManager.Workmanager.cancelAll();
|
await workManager.Workmanager.cancelByTag(backgroundTaskTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDeviceLocation() async {
|
updateDeviceLocation() async {
|
||||||
Logger.d("[Foreground location] Started");
|
if (ConnectionManager().webhookId != null &&
|
||||||
Geolocator geolocator = Geolocator();
|
ConnectionManager().webhookId.isNotEmpty) {
|
||||||
var battery = Battery();
|
String url = "${ConnectionManager()
|
||||||
String webhookId = ConnectionManager().webhookId;
|
.httpWebHost}/api/webhook/${ConnectionManager().webhookId}";
|
||||||
String httpWebHost = ConnectionManager().httpWebHost;
|
Map<String, String> headers = {};
|
||||||
if (webhookId != null && webhookId.isNotEmpty) {
|
Logger.d("[Location] Getting device location...");
|
||||||
Logger.d("[Foreground location] Getting battery level...");
|
Position location = await Geolocator().getCurrentPosition(
|
||||||
int batteryLevel = await battery.batteryLevel;
|
desiredAccuracy: LocationAccuracy.medium);
|
||||||
Logger.d("[Foreground location] Getting device location...");
|
Logger.d("[Location] Got location: ${location.latitude} ${location
|
||||||
Position position = await geolocator.getCurrentPosition(
|
.longitude}. Sending home...");
|
||||||
desiredAccuracy: LocationAccuracy.high,
|
int battery = await Battery().batteryLevel;
|
||||||
locationPermissionLevel: GeolocationPermission.locationAlways
|
var data = {
|
||||||
);
|
|
||||||
if (position != null) {
|
|
||||||
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
|
|
||||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
|
||||||
Map data = {
|
|
||||||
"type": "update_location",
|
"type": "update_location",
|
||||||
"data": {
|
"data": {
|
||||||
"gps": [position.latitude, position.longitude],
|
"gps": [location.latitude, location.longitude],
|
||||||
"gps_accuracy": position.accuracy,
|
"gps_accuracy": location.accuracy,
|
||||||
"battery": batteryLevel ?? 100
|
"battery": battery
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Logger.d("[Foreground location] Sending data home...");
|
headers["Content-Type"] = "application/json";
|
||||||
var response = await http.post(
|
await http.post(
|
||||||
url,
|
url,
|
||||||
headers: {"Content-Type": "application/json"},
|
headers: headers,
|
||||||
body: json.encode(data)
|
body: json.encode(data)
|
||||||
);
|
);
|
||||||
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
|
Logger.d("[Location] ...done.");
|
||||||
} else {
|
|
||||||
Logger.d("[Foreground location] No location. Aborting.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateDeviceLocationIsolate() {
|
void updateDeviceLocationIsolate() {
|
||||||
workManager.Workmanager.executeTask((backgroundTask, data) async {
|
workManager.Workmanager.executeTask((backgroundTask, data) {
|
||||||
//print("[Background $backgroundTask] Started");
|
//print("[Background $backgroundTask] Started");
|
||||||
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";
|
||||||
@ -169,69 +135,42 @@ void updateDeviceLocationIsolate() {
|
|||||||
"data": {
|
"data": {
|
||||||
"gps": [],
|
"gps": [],
|
||||||
"gps_accuracy": 0,
|
"gps_accuracy": 0,
|
||||||
"battery": 100
|
"battery": batteryLevel
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
//print("[Background $backgroundTask] Getting battery level...");
|
//print("[Background $backgroundTask] Getting battery level...");
|
||||||
int batteryLevel;
|
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){
|
||||||
try {
|
//print("[Background $backgroundTask] Getting device location...");
|
||||||
batteryLevel = await battery.batteryLevel;
|
Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.medium).then((location) {
|
||||||
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
|
//print("[Background $backgroundTask] Got location: ${location.latitude} ${location.longitude}");
|
||||||
} catch(e) {
|
if (location != null) {
|
||||||
//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;
|
||||||
try {
|
//print("[Background $backgroundTask] Sending data home...");
|
||||||
http.Response response = await http.post(
|
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';
|
|
||||||
}
|
}
|
||||||
}/* else {
|
}).catchError((e) {
|
||||||
logData += ' || Location: error, location is null';
|
//print("[Background $backgroundTask] Error getting current location: ${e.toString()}. Trying last known...");
|
||||||
}*/
|
Geolocator().getLastKnownPosition(desiredAccuracy: LocationAccuracy.medium).then((location){
|
||||||
} catch (e) {
|
//print("[Background $backgroundTask] Got last known location: ${location.latitude} ${location.longitude}");
|
||||||
//print("[Background $backgroundTask] Location error: $e");
|
if (location != null) {
|
||||||
//logData += ' || Location: error, $e';
|
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||||
|
data["data"]["gps_accuracy"] = location.accuracy;
|
||||||
|
//print("[Background $backgroundTask] Sending data home...");
|
||||||
|
http.post(
|
||||||
|
url,
|
||||||
|
headers: headers,
|
||||||
|
body: json.encode(data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}/* else {
|
});
|
||||||
logData += 'Not configured';
|
});
|
||||||
}*/
|
});
|
||||||
//print("[Background $backgroundTask] Writing log data...");
|
}
|
||||||
/*try {
|
return Future.value(true);
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -2,11 +2,9 @@ part of '../main.dart';
|
|||||||
|
|
||||||
class MobileAppIntegrationManager {
|
class MobileAppIntegrationManager {
|
||||||
|
|
||||||
static const INTEGRATION_VERSION = 3;
|
|
||||||
|
|
||||||
static final _appRegistrationData = {
|
static final _appRegistrationData = {
|
||||||
"device_name": "",
|
|
||||||
"app_version": "$appVersion",
|
"app_version": "$appVersion",
|
||||||
|
"device_name": "",
|
||||||
"manufacturer": DeviceInfoManager().manufacturer,
|
"manufacturer": DeviceInfoManager().manufacturer,
|
||||||
"model": DeviceInfoManager().model,
|
"model": DeviceInfoManager().model,
|
||||||
"os_version": DeviceInfoManager().osVersion,
|
"os_version": DeviceInfoManager().osVersion,
|
||||||
@ -24,7 +22,6 @@ class MobileAppIntegrationManager {
|
|||||||
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
||||||
var registrationData = Map.from(_appRegistrationData);
|
var registrationData = Map.from(_appRegistrationData);
|
||||||
registrationData.addAll({
|
registrationData.addAll({
|
||||||
"device_id": "${DeviceInfoManager().unicDeviceId}",
|
|
||||||
"app_id": "ha_client",
|
"app_id": "ha_client",
|
||||||
"app_name": "$appName",
|
"app_name": "$appName",
|
||||||
"os_name": DeviceInfoManager().osName,
|
"os_name": DeviceInfoManager().osName,
|
||||||
@ -37,11 +34,9 @@ class MobileAppIntegrationManager {
|
|||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("Processing registration responce...");
|
Logger.d("Processing registration responce...");
|
||||||
var responseObject = json.decode(response);
|
var responseObject = json.decode(response);
|
||||||
ConnectionManager().webhookId = responseObject["webhook_id"];
|
|
||||||
ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
||||||
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
|
ConnectionManager().webhookId = responseObject["webhook_id"];
|
||||||
|
|
||||||
completer.complete();
|
completer.complete();
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
@ -50,7 +45,7 @@ class MobileAppIntegrationManager {
|
|||||||
positiveText: "Restart now",
|
positiveText: "Restart now",
|
||||||
negativeText: "Later",
|
negativeText: "Later",
|
||||||
onPositive: () {
|
onPositive: () {
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "restart");
|
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
@ -70,21 +65,11 @@ class MobileAppIntegrationManager {
|
|||||||
includeAuthHeader: false,
|
includeAuthHeader: false,
|
||||||
data: json.encode(updateData)
|
data: json.encode(updateData)
|
||||||
).then((response) {
|
).then((response) {
|
||||||
var registrationData;
|
if (response == null || response.isEmpty) {
|
||||||
try {
|
Logger.d("No registration data in response. MobileApp integration was removed");
|
||||||
registrationData = json.decode(response);
|
|
||||||
} catch (e) {
|
|
||||||
registrationData = null;
|
|
||||||
}
|
|
||||||
if (registrationData == null || registrationData.isEmpty) {
|
|
||||||
Logger.d("No registration data in response. MobileApp integration was removed or broken");
|
|
||||||
_askToRegisterApp();
|
_askToRegisterApp();
|
||||||
} else {
|
} else {
|
||||||
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
|
Logger.d("App registration works fine");
|
||||||
Logger.d('App registration needs to be updated');
|
|
||||||
_askToRemoveAndRegisterApp();
|
|
||||||
} else {
|
|
||||||
Logger.d('App registration works fine');
|
|
||||||
if (showOkDialog) {
|
if (showOkDialog) {
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
title: "All good",
|
title: "All good",
|
||||||
@ -94,10 +79,9 @@ class MobileAppIntegrationManager {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
completer.complete();
|
completer.complete();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
if (e is http.Response && e.statusCode == 410) {
|
if (e['code'] != null && e['code'] == 410) {
|
||||||
Logger.e("MobileApp integration was removed");
|
Logger.e("MobileApp integration was removed");
|
||||||
_askToRegisterApp();
|
_askToRegisterApp();
|
||||||
} else {
|
} else {
|
||||||
@ -121,22 +105,10 @@ class MobileAppIntegrationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _askToRemoveAndRegisterApp() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Mobile app integration needs to be updated",
|
|
||||||
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
|
|
||||||
positiveText: "Ok",
|
|
||||||
negativeText: "Report an issue",
|
|
||||||
onNegative: () {
|
|
||||||
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _askToRegisterApp() {
|
static void _askToRegisterApp() {
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
title: "App integration is broken",
|
title: "App integration was removed",
|
||||||
body: "Looks like app integration was removed from your Home Assistant or it needs to be updated. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant before registering and restart Home Assistant. Then go back here.",
|
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
|
||||||
positiveText: "Register now",
|
positiveText: "Register now",
|
||||||
negativeText: "Cancel",
|
negativeText: "Cancel",
|
||||||
onPositive: () {
|
onPositive: () {
|
||||||
|
@ -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-894";
|
static final _whatsNewMessageKey = "user-message-shown-whats-new-706";
|
||||||
|
|
||||||
void checkMessagesToShow() async {
|
void checkMessagesToShow() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
@ -1,272 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
enum AppTheme {darkTheme, defaultTheme, haTheme}
|
|
||||||
|
|
||||||
class HAClientTheme {
|
|
||||||
|
|
||||||
static const TextTheme textTheme = TextTheme(
|
|
||||||
display1: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
|
|
||||||
display2: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
|
|
||||||
headline: TextStyle(fontSize: 24, fontWeight: FontWeight.normal),
|
|
||||||
title: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
|
|
||||||
subhead: TextStyle(fontSize: 16, fontWeight: FontWeight.normal),
|
|
||||||
body1: TextStyle(fontSize: 15, fontWeight: FontWeight.normal),
|
|
||||||
body2: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
|
||||||
subtitle: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
|
||||||
caption: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
|
|
||||||
overline: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
letterSpacing: 1,
|
|
||||||
),
|
|
||||||
button: TextStyle(fontSize: 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();
|
|
||||||
|
|
||||||
ThemeData getThemeData(AppTheme theme) {
|
|
||||||
switch (theme) {
|
|
||||||
case AppTheme.darkTheme:
|
|
||||||
return darkTheme;
|
|
||||||
break;
|
|
||||||
case AppTheme.defaultTheme:
|
|
||||||
return defaultTheme;
|
|
||||||
break;
|
|
||||||
case AppTheme.haTheme:
|
|
||||||
return homeAssistantTheme;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return defaultTheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final ThemeData defaultTheme = ThemeData.from(
|
|
||||||
colorScheme: ColorScheme(
|
|
||||||
primary: Color.fromRGBO(112, 154, 193, 1),
|
|
||||||
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
|
||||||
secondary: Color.fromRGBO(253, 216, 53, 1),
|
|
||||||
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
|
||||||
background: Color.fromRGBO(250, 250, 250, 1),
|
|
||||||
surface: Colors.white,
|
|
||||||
error: Colors.red,
|
|
||||||
onPrimary: Colors.white,
|
|
||||||
onSecondary: Colors.black87,
|
|
||||||
onBackground: Colors.black87,
|
|
||||||
onSurface: Colors.black87,
|
|
||||||
onError: Colors.white,
|
|
||||||
brightness: Brightness.light
|
|
||||||
),
|
|
||||||
textTheme: ThemeData.light().textTheme.copyWith(
|
|
||||||
display1: textTheme.display1.copyWith(color: Colors.black54),
|
|
||||||
display2: textTheme.display2.copyWith(color: Colors.redAccent),
|
|
||||||
headline: textTheme.headline.copyWith(color: Colors.black87),
|
|
||||||
title: textTheme.title.copyWith(color: Colors.black87),
|
|
||||||
subhead: textTheme.subhead.copyWith(color: Colors.black54),
|
|
||||||
body1: textTheme.body1.copyWith(color: Colors.black87),
|
|
||||||
body2: textTheme.body2.copyWith(color: Colors.black87),
|
|
||||||
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
|
|
||||||
caption: textTheme.caption.copyWith(color: Colors.black45),
|
|
||||||
overline: textTheme.overline.copyWith(color: Colors.black26),
|
|
||||||
button: textTheme.button.copyWith(color: Colors.white),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
final ThemeData homeAssistantTheme = ThemeData.from(
|
|
||||||
colorScheme: ColorScheme(
|
|
||||||
primary: Color.fromRGBO(2, 165, 238, 1),
|
|
||||||
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
|
||||||
secondary: Color.fromRGBO(253, 216, 53, 1),
|
|
||||||
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
|
||||||
background: Color.fromRGBO(250, 250, 250, 1),
|
|
||||||
surface: Colors.white,
|
|
||||||
error: Colors.red,
|
|
||||||
onPrimary: Colors.white,
|
|
||||||
onSecondary: Colors.black87,
|
|
||||||
onBackground: Colors.black87,
|
|
||||||
onSurface: Colors.black87,
|
|
||||||
onError: Colors.white,
|
|
||||||
brightness: Brightness.light
|
|
||||||
),
|
|
||||||
textTheme: ThemeData.light().textTheme.copyWith(
|
|
||||||
display1: textTheme.display1.copyWith(color: Colors.black54),
|
|
||||||
display2: textTheme.display2.copyWith(color: Colors.redAccent),
|
|
||||||
headline: textTheme.headline.copyWith(color: Colors.black87),
|
|
||||||
title: textTheme.title.copyWith(color: Colors.black87),
|
|
||||||
subhead: textTheme.subhead.copyWith(color: Colors.black54),
|
|
||||||
body1: textTheme.body1.copyWith(color: Colors.black87),
|
|
||||||
body2: textTheme.body2.copyWith(color: Colors.black87),
|
|
||||||
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
|
|
||||||
caption: textTheme.caption.copyWith(color: Colors.black45),
|
|
||||||
overline: textTheme.overline.copyWith(color: Colors.black26),
|
|
||||||
button: textTheme.button.copyWith(color: Colors.white),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
final ThemeData darkTheme = ThemeData.from(
|
|
||||||
colorScheme: ColorScheme(
|
|
||||||
primary: Color.fromRGBO(112, 154, 193, 1),
|
|
||||||
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
|
||||||
secondary: Color.fromRGBO(253, 216, 53, 1),
|
|
||||||
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
|
||||||
background: Color.fromRGBO(47, 49, 54, 1),
|
|
||||||
surface: Color.fromRGBO(54, 57, 63, 1),
|
|
||||||
error: Color.fromRGBO(183, 109, 109, 1),
|
|
||||||
onPrimary: Colors.white,
|
|
||||||
onSecondary: Colors.black87,
|
|
||||||
onBackground: Color.fromRGBO(220, 221, 222, 1),
|
|
||||||
onSurface: Colors.white,
|
|
||||||
onError: Colors.white,
|
|
||||||
brightness: Brightness.dark
|
|
||||||
),
|
|
||||||
textTheme: textTheme
|
|
||||||
);
|
|
||||||
|
|
||||||
Color getOnStateColor(BuildContext context) {
|
|
||||||
return Theme.of(context).colorScheme.secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getOffStateColor(BuildContext context) {
|
|
||||||
return Theme.of(context).colorScheme.primaryVariant;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getDisabledStateColor(BuildContext context) {
|
|
||||||
return Theme.of(context).disabledColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getAlertStateColor(BuildContext context) {
|
|
||||||
return Theme.of(context).colorScheme.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getColorByEntityState(String state, BuildContext context) {
|
|
||||||
if (onEntityStates.contains(state)) {
|
|
||||||
return getOnStateColor(context);
|
|
||||||
} else if (disabledEntityStates.contains(state)) {
|
|
||||||
return getDisabledStateColor(context);
|
|
||||||
} else if (alarmEntityStates.contains(state)) {
|
|
||||||
return getAlertStateColor(context);
|
|
||||||
} else {
|
|
||||||
return getOffStateColor(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getGreenGaugeColor() {
|
|
||||||
return Colors.green;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getYellowGaugeColor() {
|
|
||||||
return Colors.yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getRedGaugeColor() {
|
|
||||||
return Colors.red;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextStyle getLinkTextStyle(BuildContext context) {
|
|
||||||
ThemeData theme = Theme.of(context);
|
|
||||||
return theme.textTheme.body1.copyWith(
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextStyle getActionTextStyle(BuildContext context) {
|
|
||||||
ThemeData theme = Theme.of(context);
|
|
||||||
return theme.textTheme.subhead.copyWith(
|
|
||||||
color: Colors.blue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getBadgeColor(String entityDomain) {
|
|
||||||
return badgeColors[entityDomain] ??
|
|
||||||
badgeColors["default"];
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getOnBadgeTextColor() {
|
|
||||||
return Colors.white;
|
|
||||||
}
|
|
||||||
|
|
||||||
charts.Color chartHistoryStateColor(String state, int id, BuildContext context) {
|
|
||||||
Color c = getColorByEntityState(state, context);
|
|
||||||
if (c != null) {
|
|
||||||
return charts.Color(
|
|
||||||
r: c.red,
|
|
||||||
g: c.green,
|
|
||||||
b: c.blue,
|
|
||||||
a: c.alpha
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
double r = id.toDouble() % 10;
|
|
||||||
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color historyStateColor(String state, int id, BuildContext context) {
|
|
||||||
Color c = getColorByEntityState(state, context);
|
|
||||||
if (c != null) {
|
|
||||||
return c;
|
|
||||||
} else {
|
|
||||||
if (id > -1) {
|
|
||||||
double r = id.toDouble() % 10;
|
|
||||||
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
|
||||||
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
|
|
||||||
} else {
|
|
||||||
return getOnStateColor(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -12,52 +12,61 @@ class EntityViewPage extends StatefulWidget {
|
|||||||
class _EntityViewPageState extends State<EntityViewPage> {
|
class _EntityViewPageState extends State<EntityViewPage> {
|
||||||
StreamSubscription _refreshDataSubscription;
|
StreamSubscription _refreshDataSubscription;
|
||||||
StreamSubscription _stateSubscription;
|
StreamSubscription _stateSubscription;
|
||||||
Entity _entity;
|
Entity entity;
|
||||||
|
Entity forwardToMainPage;
|
||||||
|
bool _popScheduled = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
if (event.entityId == widget.entityId) {
|
if (event.entityId == widget.entityId) {
|
||||||
|
entity = HomeAssistant().entities.get(widget.entityId);
|
||||||
Logger.d("[Entity page] State change event handled: ${event.entityId}");
|
Logger.d("[Entity page] State change event handled: ${event.entityId}");
|
||||||
setState(() {
|
setState(() {});
|
||||||
_getEntity();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||||
Logger.d("[Entity page] Refresh data event handled");
|
entity = HomeAssistant().entities.get(widget.entityId);
|
||||||
setState(() {
|
setState(() {});
|
||||||
_getEntity();
|
|
||||||
});
|
});
|
||||||
});
|
entity = HomeAssistant().entities.get(widget.entityId);
|
||||||
_getEntity();
|
|
||||||
}
|
|
||||||
|
|
||||||
_getEntity() {
|
|
||||||
_entity = HomeAssistant().entities.get(widget.entityId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String entityNameToDisplay = '${(_entity?.displayName ?? widget.entityId) ?? ''}';
|
Widget body;
|
||||||
|
if (MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
|
||||||
|
if (!_popScheduled) {
|
||||||
|
_popScheduled = true;
|
||||||
|
_popAfterBuild();
|
||||||
|
}
|
||||||
|
body = PageLoadingIndicator();
|
||||||
|
} else {
|
||||||
|
body = EntityPageLayout(entity: entity);
|
||||||
|
}
|
||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}),
|
}),
|
||||||
title: new Text(entityNameToDisplay),
|
title: new Text("${entity.displayName}"),
|
||||||
),
|
),
|
||||||
body: _entity == null ? PageLoadingError(
|
body: body,
|
||||||
errorText: 'Entity is not available $entityNameToDisplay',
|
|
||||||
) : EntityPageLayout(entity: _entity),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_popAfterBuild() async {
|
||||||
|
forwardToMainPage = entity;
|
||||||
|
await Future.delayed(Duration(milliseconds: 300));
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose(){
|
void dispose(){
|
||||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
||||||
|
eventBus.fire(ShowEntityPageEvent(entity: forwardToMainPage));
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,18 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
197
lib/pages/integration_settings.page.dart
Normal file
197
lib/pages/integration_settings.page.dart
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class IntegrationSettingsPage extends StatefulWidget {
|
||||||
|
IntegrationSettingsPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_IntegrationSettingsPageState createState() => new _IntegrationSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||||
|
|
||||||
|
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
|
||||||
|
bool _locationTrackingEnabled = false;
|
||||||
|
bool _wait = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSettings() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.reload();
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
setState(() {
|
||||||
|
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||||
|
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void incLocationInterval() {
|
||||||
|
if (_locationInterval < 720) {
|
||||||
|
setState(() {
|
||||||
|
_locationInterval = _locationInterval + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void decLocationInterval() {
|
||||||
|
if (_locationInterval > 1) {
|
||||||
|
setState(() {
|
||||||
|
_locationInterval = _locationInterval - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Are you sure you want to restart Home Assistant?",
|
||||||
|
body: "This will restart your Home Assistant server.",
|
||||||
|
positiveText: "Sure. Make it so",
|
||||||
|
negativeText: "What?? No!",
|
||||||
|
onPositive: () {
|
||||||
|
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Are you sure you want to STOP Home Assistant?",
|
||||||
|
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
||||||
|
positiveText: "Sure. Make it so",
|
||||||
|
negativeText: "What?? No!",
|
||||||
|
onPositive: () {
|
||||||
|
ConnectionManager().callService(domain: "homeassistant", service: "stop", entityId: null);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRegistration() {
|
||||||
|
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetRegistration() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Waaaait",
|
||||||
|
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
|
||||||
|
positiveText: "Done it already",
|
||||||
|
negativeText: "Ok, I will",
|
||||||
|
onPositive: () {
|
||||||
|
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
_switchLocationTrackingState(bool state) async {
|
||||||
|
if (state) {
|
||||||
|
await LocationManager().updateDeviceLocation();
|
||||||
|
}
|
||||||
|
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||||
|
setState(() {
|
||||||
|
_wait = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
title: new Text(widget.title),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
InkWell(
|
||||||
|
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#location-tracking"),
|
||||||
|
child: Text(
|
||||||
|
"Please read documentation!",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontSize: 16,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Enable device location tracking"),
|
||||||
|
Switch(
|
||||||
|
value: _locationTrackingEnabled,
|
||||||
|
onChanged: _wait ? null : (value) {
|
||||||
|
setState(() {
|
||||||
|
_locationTrackingEnabled = value;
|
||||||
|
_wait = true;
|
||||||
|
});
|
||||||
|
_switchLocationTrackingState(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Text("Location update interval in minutes:"),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
//Expanded(child: Container(),),
|
||||||
|
FlatButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
child: Text("-", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||||
|
onPressed: () => decLocationInterval(),
|
||||||
|
),
|
||||||
|
Text("$_locationInterval", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||||
|
FlatButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
child: Text("+", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||||
|
onPressed: () => incLocationInterval(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(),
|
||||||
|
Text("Integration status", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Text("${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}"),
|
||||||
|
Container(height: 6.0,),
|
||||||
|
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
|
||||||
|
//Divider(),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
RaisedButton(
|
||||||
|
color: Colors.blue,
|
||||||
|
onPressed: () => updateRegistration(),
|
||||||
|
child: Text("Check integration", style: TextStyle(color: Colors.white))
|
||||||
|
),
|
||||||
|
Container(width: 10.0,),
|
||||||
|
RaisedButton(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
onPressed: () => resetRegistration(),
|
||||||
|
child: Text("Reset integration", style: TextStyle(color: Colors.white))
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -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 State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
class _MainPageState extends ReceiveShareState<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;
|
||||||
@ -21,16 +21,22 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
StreamSubscription _showPopupDialogSubscription;
|
StreamSubscription _showPopupDialogSubscription;
|
||||||
StreamSubscription _showPopupMessageSubscription;
|
StreamSubscription _showPopupMessageSubscription;
|
||||||
StreamSubscription _reloadUISubscription;
|
StreamSubscription _reloadUISubscription;
|
||||||
StreamSubscription _fullReloadSubscription;
|
|
||||||
StreamSubscription _showPageSubscription;
|
StreamSubscription _showPageSubscription;
|
||||||
BottomInfoBarController _bottomInfoBarController;
|
|
||||||
int _previousViewCount;
|
int _previousViewCount;
|
||||||
bool _showLoginButton = false;
|
bool _showLoginButton = false;
|
||||||
bool _preventAppRefresh = false;
|
bool _preventAppRefresh = false;
|
||||||
|
String _savedSharedText;
|
||||||
|
String _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(
|
||||||
@ -48,8 +54,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
_bottomInfoBarController = BottomInfoBarController();
|
|
||||||
|
|
||||||
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
||||||
|
|
||||||
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
||||||
@ -73,6 +77,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_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);
|
||||||
@ -94,42 +104,44 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fullLoad() {
|
void _fullLoad() async {
|
||||||
_bottomInfoBarController.showInfoBottomBar(progress: true,);
|
_showInfoBottomBar(progress: true,);
|
||||||
_subscribe().then((_) {
|
_subscribe().then((_) {
|
||||||
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
_fetchData();
|
||||||
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({bool uiOnly: false}) {
|
void _quickLoad() {
|
||||||
_bottomInfoBarController.showInfoBottomBar(progress: true,);
|
_hideBottomBar();
|
||||||
|
_showInfoBottomBar(progress: true,);
|
||||||
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
||||||
_fetchData(useCache: false, uiOnly: uiOnly);
|
_fetchData();
|
||||||
|
//StartupUserMessagesManager().checkMessagesToShow();
|
||||||
}, onError: (e) {
|
}, onError: (e) {
|
||||||
_setErrorState(e);
|
_setErrorState(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_fetchData({useCache: false, uiOnly: false}) async {
|
_fetchData() async {
|
||||||
if (useCache && !uiOnly) {
|
if (_savedSharedText != null && !HomeAssistant().isNoEntities) {
|
||||||
HomeAssistant().fetchDataFromCache().then((_) {
|
Logger.d("Got shared text: $_savedSharedText");
|
||||||
setState((){});
|
Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText});
|
||||||
});
|
_savedSharedText = null;
|
||||||
|
}
|
||||||
|
await HomeAssistant().fetchData().then((_) {
|
||||||
|
_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;
|
||||||
}
|
}
|
||||||
await HomeAssistant().fetchData(uiOnly).then((_) {
|
|
||||||
setState((){
|
|
||||||
_bottomInfoBarController.hideBottomBar();
|
|
||||||
});
|
|
||||||
HomeAssistant().saveCache();
|
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
if (e is HAError) {
|
if (e is HAError) {
|
||||||
_setErrorState(e);
|
_setErrorState(e);
|
||||||
@ -145,37 +157,40 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
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("Need to rebuild UI");
|
Logger.d("New entity. 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(uiOnly: true);
|
_quickLoad();
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_fullReloadSubscription == null) {
|
|
||||||
_fullReloadSubscription = eventBus.on<FullReloadEvent>().listen((event){
|
|
||||||
_fullLoad();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (_showPopupDialogSubscription == null) {
|
if (_showPopupDialogSubscription == null) {
|
||||||
@ -203,8 +218,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
if (_serviceCallSubscription == null) {
|
if (_serviceCallSubscription == null) {
|
||||||
_serviceCallSubscription =
|
_serviceCallSubscription =
|
||||||
eventBus.on<NotifyServiceCallEvent>().listen((event) {
|
eventBus.on<ServiceCallEvent>().listen((event) {
|
||||||
_notifyServiceCalled(event.domain, event.service, event.entityId);
|
_callService(event.domain, event.service, event.entityId,
|
||||||
|
event.additionalParams);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +240,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
if (_showErrorSubscription == null) {
|
if (_showErrorSubscription == null) {
|
||||||
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
||||||
_bottomInfoBarController.showErrorBottomBar(event.error);
|
_showErrorBottomBar(event.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +253,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_showOAuth();
|
_showOAuth();
|
||||||
} else {
|
} else {
|
||||||
_preventAppRefresh = false;
|
_preventAppRefresh = false;
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -251,16 +266,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
void _showOAuth() {
|
void _showOAuth() {
|
||||||
_preventAppRefresh = true;
|
_preventAppRefresh = true;
|
||||||
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
|
Launcher.launchURLInCustomTab(
|
||||||
|
url: ConnectionManager().oauthUrl
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setErrorState(HAError e) {
|
_setErrorState(HAError e) {
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
_bottomInfoBarController.showErrorBottomBar(
|
_showErrorBottomBar(
|
||||||
HAError("Unknown error")
|
HAError("Unknown error")
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_bottomInfoBarController.showErrorBottomBar(e);
|
_showErrorBottomBar(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,28 +318,27 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _notifyServiceCalled(String domain, String service, entityId) {
|
//TODO remove this shit.... maybe
|
||||||
_bottomInfoBarController.showInfoBottomBar(
|
void _callService(String domain, String service, String entityId, Map additionalParams) {
|
||||||
|
_showInfoBottomBar(
|
||||||
message: "Calling $domain.$service",
|
message: "Calling $domain.$service",
|
||||||
duration: Duration(seconds: 4)
|
duration: Duration(seconds: 3)
|
||||||
);
|
);
|
||||||
|
ConnectionManager().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showEntityPage(String entityId) {
|
void _showEntityPage(String entityId) {
|
||||||
|
setState(() {
|
||||||
|
_entityToShow = entityId;
|
||||||
|
});
|
||||||
|
if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => EntityViewPage(entityId: entityId),
|
builder: (context) => EntityViewPage(entityId: entityId),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
/*if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
|
}
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => EntityViewPage(entityId: entityId),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showPage(String path, bool goBackFirst) {
|
void _showPage(String path, bool goBackFirst) {
|
||||||
@ -352,12 +368,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
menuItems.add(
|
menuItems.add(
|
||||||
UserAccountsDrawerHeader(
|
UserAccountsDrawerHeader(
|
||||||
accountName: Text(HomeAssistant().userName),
|
accountName: Text(HomeAssistant().userName),
|
||||||
accountEmail: Text(HomeAssistant().locationName ?? ""),
|
accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"),
|
||||||
|
onDetailsPressed: () {
|
||||||
|
Launcher.launchURLInCustomTab(
|
||||||
|
url: "${ConnectionManager().httpWebHost}/profile?external_auth=1"
|
||||||
|
);
|
||||||
|
},
|
||||||
currentAccountPicture: CircleAvatar(
|
currentAccountPicture: CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).backgroundColor,
|
|
||||||
child: Text(
|
child: Text(
|
||||||
HomeAssistant().userAvatarText,
|
HomeAssistant().userAvatarText,
|
||||||
style: Theme.of(context).textTheme.display1
|
style: TextStyle(
|
||||||
|
fontSize: 32.0
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -366,7 +388,21 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
HomeAssistant().panels.forEach((Panel panel) {
|
HomeAssistant().panels.forEach((Panel panel) {
|
||||||
if (!panel.isHidden) {
|
if (!panel.isHidden) {
|
||||||
menuItems.add(
|
menuItems.add(
|
||||||
panel.getMenuItemWidget(context)
|
new ListTile(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -374,11 +410,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
menuItems.addAll([
|
menuItems.addAll([
|
||||||
Divider(),
|
Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:cellphone-settings-variant")),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:server-network")),
|
||||||
title: Text("App settings"),
|
title: Text("Connection settings"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pushNamed('/app-settings');
|
Navigator.of(context).pushNamed('/connection-settings');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:cellphone-settings-variant")),
|
||||||
|
title: Text("Integration settings"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pushNamed('/integration-settings');
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
@ -415,36 +459,29 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
title: Text("Help"),
|
title: Text("Help"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURL("http://ha-client.app/docs");
|
Launcher.launchURL("http://ha-client.homemade.systems/docs");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
||||||
title: Text("Contacts/Discussion"),
|
title: Text("Join Discord channel"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURL("https://discord.gg/nd6FZQ");
|
Launcher.launchURL("https://discord.gg/AUzEvwn");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
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.app/");
|
Launcher.launchURL("http://ha-client.homemade.systems/");
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"ha-client.app",
|
"ha-client.homemade.systems",
|
||||||
style: Theme.of(context).textTheme.body1.copyWith(
|
style: TextStyle(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -454,13 +491,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/terms_and_conditions");
|
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"Terms and Conditions",
|
"Terms and Conditions",
|
||||||
style: Theme.of(context).textTheme.body1.copyWith(
|
style: TextStyle(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -470,13 +507,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/privacy_policy");
|
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"Privacy Policy",
|
"Privacy Policy",
|
||||||
style: Theme.of(context).textTheme.body1.copyWith(
|
style: TextStyle(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -492,19 +529,118 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _hideBottomBar() {
|
||||||
|
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||||
|
setState(() {
|
||||||
|
_showBottomBar = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _bottomBarAction;
|
||||||
|
bool _showBottomBar = false;
|
||||||
|
String _bottomBarText;
|
||||||
|
bool _bottomBarProgress;
|
||||||
|
Color _bottomBarColor;
|
||||||
|
Timer _bottomBarTimer;
|
||||||
|
|
||||||
|
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
||||||
|
_bottomBarTimer?.cancel();
|
||||||
|
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
||||||
|
_bottomBarColor = Colors.grey.shade50;
|
||||||
|
setState(() {
|
||||||
|
_bottomBarText = message;
|
||||||
|
_bottomBarProgress = progress;
|
||||||
|
_showBottomBar = true;
|
||||||
|
});
|
||||||
|
if (duration != null) {
|
||||||
|
_bottomBarTimer = Timer(duration, () {
|
||||||
|
_hideBottomBar();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorBottomBar(HAError error) {
|
||||||
|
TextStyle textStyle = TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontSize: Sizes.nameFontSize
|
||||||
|
);
|
||||||
|
_bottomBarColor = Colors.red.shade100;
|
||||||
|
List<Widget> actions = [];
|
||||||
|
error.actions.forEach((HAErrorAction action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case HAErrorActionType.FULL_RELOAD: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
_fullLoad();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.QUICK_RELOAD: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
_quickLoad();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.RELOGIN: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
ConnectionManager().logout().then((_) => _fullLoad());
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.URL: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, '/connection-settings');
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (actions.isNotEmpty) {
|
||||||
|
_bottomBarAction = Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: actions,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_bottomBarProgress = false;
|
||||||
|
_bottomBarText = "${error.message}";
|
||||||
|
_showBottomBar = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
Widget _buildScaffoldBody(bool empty) {
|
Widget _buildScaffoldBody(bool empty) {
|
||||||
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",
|
||||||
@ -520,15 +656,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
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(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(domains: ["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: Theme.of(context).textTheme.body1.copyWith(
|
style: TextStyle(
|
||||||
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
color: EntityColor.stateColor(entity.state)
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
value: "${entity.entityId}",
|
value: "${entity.entityId}",
|
||||||
)).toList()
|
)).toList()
|
||||||
@ -557,12 +693,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text("$playersCount", style: TextStyle(fontSize: 12)),
|
||||||
"$playersCount",
|
|
||||||
style: Theme.of(context).textTheme.caption.copyWith(
|
|
||||||
color: Colors.white
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -580,7 +711,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text("Login with Home Assistant", style: Theme.of(context).textTheme.button),
|
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onPressed: () => _fullLoad(),
|
onPressed: () => _fullLoad(),
|
||||||
)
|
)
|
||||||
@ -598,7 +729,28 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
|
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
|
||||||
|
Entity entity = HomeAssistant().entities.get(_entityToShow);
|
||||||
|
mainScrollBody = Flex(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: HomeAssistant().buildViews(context, _viewsTabController),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: Sizes.mainPageScreenSeparatorWidth,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth),
|
||||||
|
child: EntityPageLayout(entity: entity, showClose: true,),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_entityToShow = null;
|
||||||
|
mainScrollBody = HomeAssistant().buildViews(context, _viewsTabController);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NestedScrollView(
|
return NestedScrollView(
|
||||||
@ -635,9 +787,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
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((_) {
|
||||||
@ -663,7 +813,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
body: mainScrollBody,
|
body: mainScrollBody
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -671,14 +821,61 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Widget bottomBar;
|
||||||
|
if (_showBottomBar) {
|
||||||
|
List<Widget> bottomBarChildren = [];
|
||||||
|
if (_bottomBarText != null) {
|
||||||
|
bottomBarChildren.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
|
||||||
|
Sizes.rowPadding),
|
||||||
|
child: Text(
|
||||||
|
"$_bottomBarText",
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_bottomBarProgress) {
|
||||||
|
bottomBarChildren.add(
|
||||||
|
CollectionScaleTransition(
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
|
||||||
|
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
|
||||||
|
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (bottomBarChildren.isNotEmpty) {
|
||||||
|
bottomBar = Container(
|
||||||
|
color: _bottomBarColor,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: bottomBarChildren,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_bottomBarAction
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This method is rerun every time setState is called.
|
||||||
if (HomeAssistant().isNoViews) {
|
if (HomeAssistant().isNoViews) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
primary: false,
|
primary: false,
|
||||||
drawer: _buildAppDrawer(),
|
drawer: _buildAppDrawer(),
|
||||||
bottomNavigationBar: BottomInfoBar(
|
bottomNavigationBar: bottomBar,
|
||||||
controller: _bottomInfoBarController,
|
|
||||||
),
|
|
||||||
body: _buildScaffoldBody(true)
|
body: _buildScaffoldBody(true)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -686,9 +883,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
drawer: _buildAppDrawer(),
|
drawer: _buildAppDrawer(),
|
||||||
primary: false,
|
primary: false,
|
||||||
bottomNavigationBar: BottomInfoBar(
|
bottomNavigationBar: bottomBar,
|
||||||
controller: _bottomInfoBarController,
|
|
||||||
),
|
|
||||||
body: _buildScaffoldBody(false)
|
body: _buildScaffoldBody(false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -697,11 +892,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
@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();
|
||||||
@ -709,9 +901,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_showEntityPageSubscription?.cancel();
|
_showEntityPageSubscription?.cancel();
|
||||||
_showErrorSubscription?.cancel();
|
_showErrorSubscription?.cancel();
|
||||||
_startAuthSubscription?.cancel();
|
_startAuthSubscription?.cancel();
|
||||||
|
_subscription?.cancel();
|
||||||
_showPageSubscription?.cancel();
|
_showPageSubscription?.cancel();
|
||||||
_fullReloadSubscription?.cancel();
|
|
||||||
_reloadUISubscription?.cancel();
|
_reloadUISubscription?.cancel();
|
||||||
|
//TODO disconnect
|
||||||
|
//widget.homeAssistant?.disconnect();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -57,8 +57,9 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
_loaded = false;
|
_loaded = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_isMediaExtractorExist = HomeAssistant().isComponentEnabled("media_extractor");
|
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
|
||||||
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
|
//_useMediaExtractor = _isMediaExtractorExist;
|
||||||
|
_players = HomeAssistant().entities.getByDomains(domains: ["media_player"]);
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_players.isNotEmpty) {
|
if (_players.isNotEmpty) {
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
@ -89,20 +90,16 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ConnectionManager().callService(
|
ConnectionManager().callService(
|
||||||
domain: serviceDomain,
|
domain: serviceDomain,
|
||||||
service: "play_media",
|
|
||||||
entityId: entity.entityId,
|
entityId: entity.entityId,
|
||||||
data: {
|
service: "play_media",
|
||||||
|
additionalServiceData: {
|
||||||
"media_content_id": _mediaUrl,
|
"media_content_id": _mediaUrl,
|
||||||
"media_content_type": _contentType
|
"media_content_type": _contentType
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
HomeAssistant().sendToPlayerId = entity.entityId;
|
HomeAssistant().sendToPlayerId = entity.entityId;
|
||||||
if (HomeAssistant().sendFromPlayerId != null && HomeAssistant().sendFromPlayerId != HomeAssistant().sendToPlayerId) {
|
if (HomeAssistant().sendFromPlayerId != null && HomeAssistant().sendFromPlayerId != HomeAssistant().sendToPlayerId) {
|
||||||
ConnectionManager().callService(
|
eventBus.fire(ServiceCallEvent(HomeAssistant().sendFromPlayerId.split(".")[0], "turn_off", HomeAssistant().sendFromPlayerId, null));
|
||||||
domain: HomeAssistant().sendFromPlayerId.split(".")[0],
|
|
||||||
service: "turn_off",
|
|
||||||
entityId: HomeAssistant().sendFromPlayerId
|
|
||||||
);
|
|
||||||
HomeAssistant().sendFromPlayerId = null;
|
HomeAssistant().sendFromPlayerId = null;
|
||||||
}
|
}
|
||||||
eventBus.fire(ShowEntityPageEvent(entity: entity));
|
eventBus.fire(ShowEntityPageEvent(entity: entity));
|
||||||
@ -134,9 +131,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
if (_validationMessage.isNotEmpty) {
|
if (_validationMessage.isNotEmpty) {
|
||||||
children.add(Text(
|
children.add(Text(
|
||||||
"$_validationMessage",
|
"$_validationMessage",
|
||||||
style: Theme.of(context).textTheme.body1.copyWith(
|
style: TextStyle(color: Colors.red)
|
||||||
color: Theme.of(context).errorColor
|
|
||||||
)
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
children.addAll(<Widget>[
|
children.addAll(<Widget>[
|
||||||
@ -194,7 +189,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"How?",
|
"How?",
|
||||||
style: Theme.of(context).textTheme.body1.copyWith(
|
style: TextStyle(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline
|
||||||
),
|
),
|
||||||
|
@ -62,7 +62,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildProducts() {
|
Widget _buildProducts() {
|
||||||
List<Widget> productWidgets = [];
|
List<Widget> productWidgets = [];
|
||||||
for (ProductDetails product in _products) {
|
for (ProductDetails product in _products) {
|
||||||
productWidgets.add(
|
productWidgets.add(
|
||||||
@ -72,7 +72,10 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
|
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return productWidgets;
|
return ListView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
children: productWidgets
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _buyProduct(ProductDetails product) {
|
void _buyProduct(ProductDetails product) {
|
||||||
@ -84,28 +87,12 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<Widget> body;
|
Widget body;
|
||||||
if (!_loaded) {
|
if (!_loaded) {
|
||||||
body = [_error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error)];
|
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
||||||
} else {
|
} else {
|
||||||
body = _buildProducts();
|
body = _buildProducts();
|
||||||
}
|
}
|
||||||
body.add(
|
|
||||||
Card(
|
|
||||||
child: Container(
|
|
||||||
height: 80,
|
|
||||||
child: InkWell(
|
|
||||||
child: Image.network('https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif'),
|
|
||||||
onTap: () {
|
|
||||||
Launcher.launchURLInCustomTab(
|
|
||||||
context: context,
|
|
||||||
url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARWGETZD2D83Q&source=url'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
@ -113,10 +100,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
}),
|
}),
|
||||||
title: new Text(widget.title),
|
title: new Text(widget.title),
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: body,
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
children: body
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
228
lib/pages/settings.page.dart
Normal file
228
lib/pages/settings.page.dart
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ConnectionSettingsPage extends StatefulWidget {
|
||||||
|
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||||
|
String _hassioDomain = "";
|
||||||
|
String _newHassioDomain = "";
|
||||||
|
String _hassioPort = "";
|
||||||
|
String _newHassioPort = "";
|
||||||
|
String _socketProtocol = "wss";
|
||||||
|
String _newSocketProtocol = "wss";
|
||||||
|
String _longLivedToken = "";
|
||||||
|
String _newLongLivedToken = "";
|
||||||
|
bool _useLovelace = true;
|
||||||
|
bool _newUseLovelace = true;
|
||||||
|
|
||||||
|
String oauthUrl;
|
||||||
|
bool useOAuth = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSettings() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
final storage = new FlutterSecureStorage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
useOAuth = prefs.getBool("oauth-used") ?? true;
|
||||||
|
} catch (e) {
|
||||||
|
useOAuth = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useOAuth) {
|
||||||
|
try {
|
||||||
|
_longLivedToken = _newLongLivedToken =
|
||||||
|
await storage.read(key: "hacl_llt");
|
||||||
|
} catch (e) {
|
||||||
|
_longLivedToken = _newLongLivedToken = "";
|
||||||
|
await storage.delete(key: "hacl_llt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
||||||
|
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
||||||
|
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
||||||
|
try {
|
||||||
|
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
|
||||||
|
} catch (e) {
|
||||||
|
_useLovelace = _newUseLovelace = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _checkConfigChanged() {
|
||||||
|
return (
|
||||||
|
(_newHassioPort != _hassioPort) ||
|
||||||
|
(_newHassioDomain != _hassioDomain) ||
|
||||||
|
(_newSocketProtocol != _socketProtocol) ||
|
||||||
|
(_newUseLovelace != _useLovelace) ||
|
||||||
|
(_newLongLivedToken != _longLivedToken));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveSettings() async {
|
||||||
|
_newHassioDomain = _newHassioDomain.trim();
|
||||||
|
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||||
|
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||||
|
}
|
||||||
|
_newHassioDomain = _newHassioDomain.split("/")[0];
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
final storage = new FlutterSecureStorage();
|
||||||
|
if (_newLongLivedToken.isNotEmpty) {
|
||||||
|
_newLongLivedToken = _newLongLivedToken.trim();
|
||||||
|
prefs.setBool("oauth-used", false);
|
||||||
|
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
||||||
|
} else if (!useOAuth) {
|
||||||
|
await storage.delete(key: "hacl_llt");
|
||||||
|
}
|
||||||
|
prefs.setString("hassio-domain", _newHassioDomain);
|
||||||
|
if (_newHassioPort == null || _newHassioPort.isEmpty) {
|
||||||
|
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
|
||||||
|
} else {
|
||||||
|
_newHassioPort = _newHassioPort.trim();
|
||||||
|
}
|
||||||
|
prefs.setString("hassio-port", _newHassioPort);
|
||||||
|
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||||
|
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||||
|
prefs.setBool("use-lovelace", _newUseLovelace);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
title: new Text(widget.title),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.check),
|
||||||
|
onPressed: (){
|
||||||
|
if (_checkConfigChanged()) {
|
||||||
|
Logger.d("Settings changed. Saving...");
|
||||||
|
_saveSettings().then((r) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
eventBus.fire(SettingsChangedEvent(true));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Logger.d("Settings was not changed");
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
"Connection settings",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black45,
|
||||||
|
fontSize: 20.0
|
||||||
|
),
|
||||||
|
),
|
||||||
|
new Row(
|
||||||
|
children: [
|
||||||
|
Text("Use ssl (HTTPS)"),
|
||||||
|
Switch(
|
||||||
|
value: (_newSocketProtocol == "wss"),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_newSocketProtocol = value ? "wss" : "ws";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Home Assistant domain or ip address"
|
||||||
|
),
|
||||||
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioDomain = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Home Assistant port (default is 8123)"
|
||||||
|
),
|
||||||
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioPort = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
new Text(
|
||||||
|
"Try ports 80 and 443 if default is not working and you don't know why.",
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 20.0),
|
||||||
|
child: Text(
|
||||||
|
"UI",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black45,
|
||||||
|
fontSize: 20.0
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
new Row(
|
||||||
|
children: [
|
||||||
|
Text("Use Lovelace UI"),
|
||||||
|
Switch(
|
||||||
|
value: _newUseLovelace,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_newUseLovelace = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Authentication settings",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black45,
|
||||||
|
fontSize: 20.0
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(height: 10.0,),
|
||||||
|
Text(
|
||||||
|
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
|
||||||
|
style: TextStyle(color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Long-lived token"
|
||||||
|
),
|
||||||
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newLongLivedToken = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -1,104 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
enum AppSettingsSection {menu, connectionSettings, integrationSettings, lookAndFeel}
|
|
||||||
|
|
||||||
class AppSettingsPage extends StatefulWidget {
|
|
||||||
final AppSettingsSection showSection;
|
|
||||||
|
|
||||||
AppSettingsPage({Key key, this.showSection: AppSettingsSection.menu}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_AppSettingsPageState createState() => new _AppSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppSettingsPageState extends State<AppSettingsPage> {
|
|
||||||
|
|
||||||
var _currentSection;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_currentSection = widget.showSection;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMenuItem(BuildContext context, IconData icon,String title, AppSettingsSection section) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(title, style: Theme.of(context).textTheme.subhead),
|
|
||||||
leading: Icon(icon),
|
|
||||||
trailing: Icon(Icons.keyboard_arrow_right),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_currentSection = section;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMenu(BuildContext context) {
|
|
||||||
return ListView(
|
|
||||||
children: <Widget>[
|
|
||||||
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
|
|
||||||
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings),
|
|
||||||
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Widget section;
|
|
||||||
String title;
|
|
||||||
switch (_currentSection) {
|
|
||||||
case AppSettingsSection.menu: {
|
|
||||||
section = _buildMenu(context);
|
|
||||||
title = 'App settings';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AppSettingsSection.connectionSettings: {
|
|
||||||
section = ConnectionSettingsPage();
|
|
||||||
title = 'App settings - Connection';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AppSettingsSection.integrationSettings: {
|
|
||||||
section = IntegrationSettingsPage();
|
|
||||||
title = 'App settings - Integration';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AppSettingsSection.lookAndFeel: {
|
|
||||||
section = LookAndFeelSettingsPage();
|
|
||||||
title = 'App settings - Look&Feel';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
title = ':(';
|
|
||||||
section = PageLoadingIndicator();
|
|
||||||
}
|
|
||||||
return WillPopScope(
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
|
||||||
if (_currentSection == AppSettingsSection.menu) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_currentSection = AppSettingsSection.menu;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
title: Text(title),
|
|
||||||
),
|
|
||||||
body: section
|
|
||||||
),
|
|
||||||
onWillPop: () {
|
|
||||||
if (_currentSection == AppSettingsSection.menu) {
|
|
||||||
return Future.value(true);
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_currentSection = AppSettingsSection.menu;
|
|
||||||
});
|
|
||||||
return Future.value(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,192 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class ConnectionSettingsPage extends StatefulWidget {
|
|
||||||
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|
||||||
String _hassioDomain = "";
|
|
||||||
String _newHassioDomain = "";
|
|
||||||
String _hassioPort = "";
|
|
||||||
String _newHassioPort = "";
|
|
||||||
String _socketProtocol = "wss";
|
|
||||||
String _newSocketProtocol = "wss";
|
|
||||||
String _longLivedToken = "";
|
|
||||||
String _newLongLivedToken = "";
|
|
||||||
|
|
||||||
String oauthUrl;
|
|
||||||
bool useOAuth = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadSettings();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadSettings() async {
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
final storage = new FlutterSecureStorage();
|
|
||||||
|
|
||||||
try {
|
|
||||||
useOAuth = prefs.getBool("oauth-used") ?? true;
|
|
||||||
} catch (e) {
|
|
||||||
useOAuth = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!useOAuth) {
|
|
||||||
try {
|
|
||||||
_longLivedToken = _newLongLivedToken =
|
|
||||||
await storage.read(key: "hacl_llt");
|
|
||||||
} catch (e) {
|
|
||||||
_longLivedToken = _newLongLivedToken = "";
|
|
||||||
await storage.delete(key: "hacl_llt");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
|
||||||
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
|
||||||
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _checkConfigChanged() {
|
|
||||||
return (
|
|
||||||
(_newHassioPort != _hassioPort) ||
|
|
||||||
(_newHassioDomain != _hassioDomain) ||
|
|
||||||
(_newSocketProtocol != _socketProtocol) ||
|
|
||||||
(_newLongLivedToken != _longLivedToken));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
_saveSettings() async {
|
|
||||||
_newHassioDomain = _newHassioDomain.trim();
|
|
||||||
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
|
|
||||||
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
|
|
||||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
|
||||||
}
|
|
||||||
_newHassioDomain = _newHassioDomain.split("/")[0];
|
|
||||||
if (_newHassioDomain.contains(":")) {
|
|
||||||
List<String> domainAndPort = _newHassioDomain.split(":");
|
|
||||||
_newHassioDomain = domainAndPort[0];
|
|
||||||
_newHassioPort = domainAndPort[1];
|
|
||||||
}
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
final storage = new FlutterSecureStorage();
|
|
||||||
if (_newLongLivedToken.isNotEmpty) {
|
|
||||||
_newLongLivedToken = _newLongLivedToken.trim();
|
|
||||||
prefs.setBool("oauth-used", false);
|
|
||||||
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
|
||||||
} else if (!useOAuth) {
|
|
||||||
await storage.delete(key: "hacl_llt");
|
|
||||||
}
|
|
||||||
prefs.setString("hassio-domain", _newHassioDomain);
|
|
||||||
if (_newHassioPort == null || _newHassioPort.isEmpty) {
|
|
||||||
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
|
|
||||||
} else {
|
|
||||||
_newHassioPort = _newHassioPort.trim();
|
|
||||||
}
|
|
||||||
prefs.setString("hassio-port", _newHassioPort);
|
|
||||||
prefs.setString("hassio-protocol", _newSocketProtocol);
|
|
||||||
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListView(
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
"Connection settings",
|
|
||||||
style: Theme.of(context).textTheme.headline,
|
|
||||||
),
|
|
||||||
new Row(
|
|
||||||
children: [
|
|
||||||
Text("Use ssl (HTTPS)"),
|
|
||||||
Switch(
|
|
||||||
value: (_newSocketProtocol == "wss"),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_newSocketProtocol = value ? "wss" : "ws";
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Home Assistant domain or ip address"
|
|
||||||
),
|
|
||||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
|
|
||||||
onChanged: (value) {
|
|
||||||
_newHassioDomain = value;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Home Assistant port (default is 8123)"
|
|
||||||
),
|
|
||||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
|
|
||||||
onChanged: (value) {
|
|
||||||
_newHassioPort = value;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
new Text(
|
|
||||||
"Try ports 80 and 443 if default is not working and you don't know why.",
|
|
||||||
style: Theme.of(context).textTheme.caption,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Authentication settings",
|
|
||||||
style: Theme.of(context).textTheme.headline,
|
|
||||||
),
|
|
||||||
Container(height: 10.0,),
|
|
||||||
Text(
|
|
||||||
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
|
|
||||||
style: Theme.of(context).textTheme.body1.copyWith(
|
|
||||||
color: Colors.redAccent
|
|
||||||
),
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Long-lived token"
|
|
||||||
),
|
|
||||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
|
|
||||||
onChanged: (value) {
|
|
||||||
_newLongLivedToken = value;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: Sizes.rowPadding,
|
|
||||||
),
|
|
||||||
RaisedButton(
|
|
||||||
child: Text('Apply', style: Theme.of(context).textTheme.button),
|
|
||||||
color: Theme.of(context).primaryColorDark,
|
|
||||||
onPressed: () {
|
|
||||||
if (_checkConfigChanged()) {
|
|
||||||
Logger.d("Settings changed. Saving...");
|
|
||||||
_saveSettings().then((r) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
eventBus.fire(SettingsChangedEvent(true));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Logger.d("Settings was not changed");
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,194 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class IntegrationSettingsPage extends StatefulWidget {
|
|
||||||
IntegrationSettingsPage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_IntegrationSettingsPageState createState() => new _IntegrationSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|
||||||
|
|
||||||
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
|
|
||||||
bool _locationTrackingEnabled = false;
|
|
||||||
bool _wait = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadSettings();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadSettings() async {
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.reload();
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
setState(() {
|
|
||||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
|
||||||
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
|
|
||||||
if (_locationInterval % 5 != 0) {
|
|
||||||
_locationInterval = 5 * (_locationInterval ~/ 5);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void incLocationInterval() {
|
|
||||||
if (_locationInterval < 720) {
|
|
||||||
setState(() {
|
|
||||||
_locationInterval = _locationInterval + 5;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void decLocationInterval() {
|
|
||||||
if (_locationInterval > 5) {
|
|
||||||
setState(() {
|
|
||||||
_locationInterval = _locationInterval - 5;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
restart() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Are you sure you want to restart Home Assistant?",
|
|
||||||
body: "This will restart your Home Assistant server.",
|
|
||||||
positiveText: "Sure. Make it so",
|
|
||||||
negativeText: "What?? No!",
|
|
||||||
onPositive: () {
|
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "restart");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Are you sure you want to STOP Home Assistant?",
|
|
||||||
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
|
||||||
positiveText: "Sure. Make it so",
|
|
||||||
negativeText: "What?? No!",
|
|
||||||
onPositive: () {
|
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "stop");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRegistration() {
|
|
||||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetRegistration() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Waaaait",
|
|
||||||
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
|
|
||||||
positiveText: "Done it already",
|
|
||||||
negativeText: "Ok, I will",
|
|
||||||
onPositive: () {
|
|
||||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
_switchLocationTrackingState(bool state) async {
|
|
||||||
if (state) {
|
|
||||||
await LocationManager().updateDeviceLocation();
|
|
||||||
}
|
|
||||||
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
|
||||||
setState(() {
|
|
||||||
_wait = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListView(
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
children: <Widget>[
|
|
||||||
Text("Location tracking", style: Theme.of(context).textTheme.title),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
InkWell(
|
|
||||||
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
|
|
||||||
child: Text(
|
|
||||||
"Please read documentation!",
|
|
||||||
style: Theme.of(context).textTheme.subhead.copyWith(
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Text("Enable device location tracking"),
|
|
||||||
Switch(
|
|
||||||
value: _locationTrackingEnabled,
|
|
||||||
onChanged: _wait ? null : (value) {
|
|
||||||
setState(() {
|
|
||||||
_locationTrackingEnabled = value;
|
|
||||||
_wait = true;
|
|
||||||
});
|
|
||||||
_switchLocationTrackingState(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text("Location update interval in minutes:"),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
//Expanded(child: Container(),),
|
|
||||||
FlatButton(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
child: Text("-", style: Theme.of(context).textTheme.title),
|
|
||||||
onPressed: () => decLocationInterval(),
|
|
||||||
),
|
|
||||||
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
|
|
||||||
FlatButton(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
child: Text("+", style: Theme.of(context).textTheme.title),
|
|
||||||
onPressed: () => incLocationInterval(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
Text("Integration status", style: Theme.of(context).textTheme.title),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text(
|
|
||||||
"${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}",
|
|
||||||
style: Theme.of(context).textTheme.subtitle,
|
|
||||||
),
|
|
||||||
Container(height: 6.0,),
|
|
||||||
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
|
|
||||||
//Divider(),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
RaisedButton(
|
|
||||||
color: Colors.blue,
|
|
||||||
onPressed: () => updateRegistration(),
|
|
||||||
child: Text("Check integration", style: Theme.of(context).textTheme.button)
|
|
||||||
),
|
|
||||||
Container(width: 10.0,),
|
|
||||||
RaisedButton(
|
|
||||||
color: Colors.redAccent,
|
|
||||||
onPressed: () => resetRegistration(),
|
|
||||||
child: Text("Reset integration", style: Theme.of(context).textTheme.button)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class LookAndFeelSettingsPage extends StatefulWidget {
|
|
||||||
LookAndFeelSettingsPage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LookAndFeelSettingsPageState createState() => new _LookAndFeelSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
|
||||||
|
|
||||||
AppTheme _currentTheme;
|
|
||||||
bool _changed = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadSettings() async {
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.reload();
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
setState(() {
|
|
||||||
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_saveSettings(AppTheme theme) {
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
prefs.setInt('app-theme', theme.index);
|
|
||||||
setState(() {
|
|
||||||
_currentTheme = theme;
|
|
||||||
eventBus.fire(ChangeThemeEvent(_currentTheme));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Map appThemeName = {
|
|
||||||
AppTheme.defaultTheme: 'Default',
|
|
||||||
AppTheme.haTheme: 'Home Assistant theme',
|
|
||||||
AppTheme.darkTheme: 'Dark theme'
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListView(
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
children: <Widget>[
|
|
||||||
Text("Application theme:", style: Theme.of(context).textTheme.body2),
|
|
||||||
Container(height: Sizes.rowPadding),
|
|
||||||
DropdownButton<AppTheme>(
|
|
||||||
value: _currentTheme,
|
|
||||||
iconSize: 30.0,
|
|
||||||
isExpanded: true,
|
|
||||||
style: Theme.of(context).textTheme.title,
|
|
||||||
//hint: Text("Select ${caption.toLowerCase()}"),
|
|
||||||
items: AppTheme.values.map((value) {
|
|
||||||
return new DropdownMenuItem<AppTheme>(
|
|
||||||
value: value,
|
|
||||||
child: Text('${appThemeName[value]}'),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
onChanged: (theme) => _saveSettings(theme),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
|
|||||||
error = "";
|
error = "";
|
||||||
});
|
});
|
||||||
http.Response response;
|
http.Response response;
|
||||||
response = await http.get("http://ha-client.app/service/whats_new_0.8.5.md");
|
response = await http.get("http://ha-client.homemade.systems/service/whats_new_$appVersionNumber.md");
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
setState(() {
|
setState(() {
|
||||||
data = response.body;
|
data = response.body;
|
||||||
|
@ -1,210 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class BottomInfoBarController {
|
|
||||||
|
|
||||||
Function show;
|
|
||||||
Function hide;
|
|
||||||
|
|
||||||
String bottomBarText;
|
|
||||||
bool bottomBarProgress;
|
|
||||||
bool bottomBarErrorColor;
|
|
||||||
Timer _bottomBarTimer;
|
|
||||||
bool initialState = false;
|
|
||||||
|
|
||||||
List<HAErrorAction> actions = [];
|
|
||||||
|
|
||||||
void hideBottomBar() {
|
|
||||||
_bottomBarTimer?.cancel();
|
|
||||||
if (hide == null) {
|
|
||||||
initialState = false;
|
|
||||||
} else {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
|
||||||
_bottomBarTimer?.cancel();
|
|
||||||
actions.clear();
|
|
||||||
bottomBarErrorColor = false;
|
|
||||||
bottomBarText = message;
|
|
||||||
bottomBarProgress = progress;
|
|
||||||
if (show == null) {
|
|
||||||
initialState = true;
|
|
||||||
} else {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
if (duration != null) {
|
|
||||||
_bottomBarTimer = Timer(duration, () {
|
|
||||||
hideBottomBar();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void showErrorBottomBar(HAError error) {
|
|
||||||
actions.clear();
|
|
||||||
actions.addAll(error.actions);
|
|
||||||
bottomBarErrorColor = true;
|
|
||||||
bottomBarProgress = false;
|
|
||||||
bottomBarText = "${error.message}";
|
|
||||||
if (show == null) {
|
|
||||||
initialState = true;
|
|
||||||
} else {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class BottomInfoBar extends StatefulWidget {
|
|
||||||
|
|
||||||
final BottomInfoBarController controller;
|
|
||||||
|
|
||||||
const BottomInfoBar({Key key, this.controller}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return new _BottomInfoBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BottomInfoBarState extends State<BottomInfoBar> {
|
|
||||||
|
|
||||||
bool _show;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_show = widget.controller.initialState;
|
|
||||||
widget.controller.show = () {
|
|
||||||
setState(() {
|
|
||||||
_show = true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
widget.controller.hide = () {
|
|
||||||
setState(() {
|
|
||||||
_show = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (!_show) {
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
} else {
|
|
||||||
Widget bottomBar;
|
|
||||||
List<Widget> bottomBarChildren = [];
|
|
||||||
Widget actionsWidget;
|
|
||||||
TextStyle textStyle = Theme.of(context).textTheme.button.copyWith(
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
);
|
|
||||||
List<Widget> actions = [];
|
|
||||||
widget.controller.actions.forEach((HAErrorAction action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case HAErrorActionType.FULL_RELOAD: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
eventBus.fire(FullReloadEvent());
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.QUICK_RELOAD: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
eventBus.fire(ReloadUIEvent());
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.RELOGIN: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
ConnectionManager().logout().then((_) => eventBus.fire(FullReloadEvent()));
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.URL: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pushNamed(context, '/connection-settings');
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (actions.isNotEmpty) {
|
|
||||||
actionsWidget = Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: actions,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
actionsWidget = Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.controller.bottomBarText != null) {
|
|
||||||
bottomBarChildren.add(
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
|
|
||||||
Sizes.rowPadding),
|
|
||||||
child: Text(
|
|
||||||
"${widget.controller.bottomBarText}",
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (widget.controller.bottomBarProgress) {
|
|
||||||
bottomBarChildren.add(
|
|
||||||
LinearProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (bottomBarChildren.isNotEmpty) {
|
|
||||||
bottomBar = Container(
|
|
||||||
color: widget.controller.bottomBarErrorColor ? Theme.of(context).errorColor : Theme.of(context).primaryColorLight,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: widget.controller.bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: bottomBarChildren,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actionsWidget
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
bottomBar = Container(height: 0,);
|
|
||||||
}
|
|
||||||
return bottomBar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,7 +10,8 @@ class LastUpdatedWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'${entityModel.entityWrapper.entity.lastUpdated}',
|
'${entityModel.entityWrapper.entity.lastUpdated}',
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: Theme.of(context).textTheme.caption
|
style: TextStyle(
|
||||||
|
fontSize: Sizes.smallFontSize, color: Colors.black26),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user