Compare commits
94 Commits
beta/0.7.6
...
beta/0.8.0
Author | SHA1 | Date | |
---|---|---|---|
a7c3b46061 | |||
dfbaaeb06b | |||
f6ab20c6e8 | |||
7625099d74 | |||
32c8e76855 | |||
0aa2c974d5 | |||
9524c8587b | |||
c075db8b1a | |||
d0b7cc1929 | |||
d8df32f140 | |||
293b5e0242 | |||
2f517a3ad5 | |||
56d8e389db | |||
1377843350 | |||
8e31eaf8bb | |||
5ced01463f | |||
a3548455eb | |||
c40fceea4f | |||
6ad3938a91 | |||
bc642f81ad | |||
14ce608bbb | |||
c4c67747c5 | |||
5b3ceecb0e | |||
bf53e4b9df | |||
7e09d92fdf | |||
1ba9106d0b | |||
d727a29991 | |||
c5d617477f | |||
244a1984cc | |||
b00b745f27 | |||
959ff21b9b | |||
e6a7fd2dfe | |||
216276e5f3 | |||
3e6229cf3e | |||
fc4cb80b74 | |||
b907ff1e82 | |||
7536a52771 | |||
73a8c111d1 | |||
86a19eeec2 | |||
fba4459977 | |||
06f994a827 | |||
35d8607484 | |||
2f4c06e9b5 | |||
92e008a380 | |||
14c272af92 | |||
710de9f2b8 | |||
d9ad3b3083 | |||
b2686cb105 | |||
959e89de2b | |||
6e448d3458 | |||
6695756727 | |||
ed732e9b77 | |||
f495a6affc | |||
c8d7e1a95f | |||
e1ca2638e3 | |||
01226cb9eb | |||
8a80d0c5d1 | |||
f26f3e87c7 | |||
b750417415 | |||
2c35dd7c21 | |||
cff4a4feed | |||
62174b0651 | |||
d3ea4210c1 | |||
1c782bf64d | |||
bc96dab339 | |||
0f7179b944 | |||
1e3bfa8ff7 | |||
2bce86f905 | |||
0be00acc3a | |||
4e61adaeb1 | |||
49a8f08153 | |||
ce15658462 | |||
16d73ba7dd | |||
9f3e3c1917 | |||
f29e382a19 | |||
073562373a | |||
4298ebcd66 | |||
a121295bef | |||
9303e4c0a5 | |||
831fc98ab1 | |||
2003005e56 | |||
fda8fb7182 | |||
cf6039b279 | |||
41e552dce5 | |||
90043b5806 | |||
9eb74b5a8d | |||
9cc60a136b | |||
78eb1e779c | |||
8db2d8508e | |||
3f1ece26ec | |||
d1912a44c6 | |||
36a05eb390 | |||
4f39ea1ad8 | |||
a241cc1d61 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,6 +15,7 @@ build/
|
||||
.settings/
|
||||
|
||||
flutter_export_environment.sh
|
||||
.flutter-plugins-dependencies
|
||||
|
||||
key.properties
|
||||
premium_features_manager.class.dart
|
||||
|
@ -4,9 +4,5 @@ ENV ANDROID_HOME=/workspace/android-sdk \
|
||||
FLUTTER_ROOT=/workspace/flutter \
|
||||
FLUTTER_HOME=/workspace/flutter
|
||||
|
||||
USER root
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install build-essential libkrb5-dev gcc make gradle openjdk-8-jdk && \
|
||||
apt-get clean && \
|
||||
apt-get -y autoremove
|
||||
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
|
||||
&& sdk install java 8.0.242.j9-adpt"
|
@ -8,7 +8,7 @@ tasks:
|
||||
touch /home/gitpod/.android/repositories.cfg
|
||||
init: |
|
||||
echo "Installing Flutter SDK..."
|
||||
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.9.1+hotfix.4-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
|
||||
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.12.13+hotfix.7-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
|
||||
echo "Installing Android SDK..."
|
||||
mkdir -p /workspace/android-sdk && cd /workspace/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip
|
||||
/workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
|
||||
@ -18,7 +18,6 @@ tasks:
|
||||
flutter doctor --android-licenses
|
||||
flutter pub get
|
||||
command: |
|
||||
flutter pub upgrade
|
||||
echo "Ready to go!"
|
||||
flutter doctor
|
||||
vscode:
|
||||
|
BIN
.gradle/6.0.1/fileChanges/last-build.bin
Normal file
BIN
.gradle/6.0.1/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/6.0.1/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/6.0.1/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
0
.gradle/6.0.1/gc.properties
Normal file
0
.gradle/6.0.1/gc.properties
Normal file
@ -1,13 +1,14 @@
|
||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||
# HA Client
|
||||
## Native Android client for Home Assistant
|
||||
### With notifications and Lovelace UI support
|
||||
|
||||
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
||||
Visit [ha-client.app](http://ha-client.app/) for more info.
|
||||
|
||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||
|
||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn)
|
||||
Discuss it on [Spectrum.chat](https://spectrum.chat/ha-client) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||
|
||||
#### Pre-release CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
||||
|
@ -78,10 +78,11 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-core:16.0.8'
|
||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
}
|
||||
|
||||
apply plugin: 'io.fabric'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
@ -6,9 +6,9 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<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
|
||||
@ -17,11 +17,14 @@
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="ha_notify" />
|
||||
@ -33,13 +36,12 @@
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- This keeps the window background of the activity showing
|
||||
until Flutter renders its first frame. It can be removed if
|
||||
there is no splash screen (such as the default splash screen
|
||||
defined in @style/LaunchTheme).
|
||||
<meta-data
|
||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||
android:value="true" />-->
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background" />
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@ -48,14 +50,6 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="haclient"
|
||||
android:host="auth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
|
@ -1,20 +0,0 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import io.flutter.app.FlutterApplication;
|
||||
import io.flutter.plugin.common.PluginRegistry;
|
||||
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
import be.tramckrijte.workmanager.WorkmanagerPlugin;
|
||||
|
||||
public class Application extends FlutterApplication implements PluginRegistrantCallback {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
WorkmanagerPlugin.setPluginRegistrantCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWith(PluginRegistry registry) {
|
||||
GeneratedPluginRegistrant.registerWith(registry);
|
||||
}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.os.Bundle;
|
||||
import io.flutter.app.FlutterActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
import io.flutter.plugins.share.FlutterShareReceiverActivity;
|
||||
|
||||
public class MainActivity extends FlutterShareReceiverActivity {
|
||||
public class MainActivity extends FlutterActivity {
|
||||
|
||||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
GeneratedPluginRegistrant.registerWith(this);
|
||||
}
|
||||
}
|
||||
|
@ -5,4 +5,7 @@
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
@ -2,11 +2,15 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
classpath 'io.fabric.tools:gradle:1.26.1'
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +18,9 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,3 +3,4 @@ org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableR8=true
|
||||
|
1
android/settings_aar.gradle
Normal file
1
android/settings_aar.gradle
Normal file
@ -0,0 +1 @@
|
||||
include ':app'
|
28
assets/html/cameraView.html
Normal file
28
assets/html/cameraView.html
Normal file
@ -0,0 +1,28 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
widows: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var messageChannel = '{{message_channel}}';
|
||||
window.onload = function() {
|
||||
var img = document.getElementById('screen');
|
||||
if (img) {
|
||||
window[messageChannel].postMessage(document.body.clientWidth / img.offsetHeight);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<img id="screen" src="{{stream_url}}">
|
||||
</body>
|
||||
</html>
|
@ -14,3 +14,22 @@ window.externalApp.getExternalAuth = function(options) {
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
window.externalApp.externalBus = function(message) {
|
||||
console.log("External bus message: " + message);
|
||||
var messageObj = JSON.parse(message);
|
||||
if (messageObj.type == "config/get") {
|
||||
var responseData = {
|
||||
id: messageObj.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: {
|
||||
hasSettingsScreen: true
|
||||
}
|
||||
};
|
||||
setTimeout(function(){
|
||||
window.externalBus(responseData);
|
||||
}, 500);
|
||||
} else if (messageObj.type == "config_screen/show") {
|
||||
HAClient.postMessage('show-settings');
|
||||
}
|
||||
};
|
@ -10,6 +10,7 @@ class HACard {
|
||||
bool showName;
|
||||
bool showState;
|
||||
bool showEmpty;
|
||||
bool showHeaderToggle;
|
||||
int columnsCount;
|
||||
List stateFilter;
|
||||
List states;
|
||||
@ -26,6 +27,7 @@ class HACard {
|
||||
this.linkedEntityWrapper,
|
||||
this.columnsCount: 4,
|
||||
this.showName: true,
|
||||
this.showHeaderToggle: true,
|
||||
this.showState: true,
|
||||
this.stateFilter: const [],
|
||||
this.showEmpty: true,
|
||||
@ -45,13 +47,70 @@ class HACard {
|
||||
|
||||
List<EntityWrapper> getEntitiesToShow() {
|
||||
return entities.where((entityWrapper) {
|
||||
if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) {
|
||||
if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
|
||||
return false;
|
||||
}
|
||||
if (stateFilter.isNotEmpty) {
|
||||
return stateFilter.contains(entityWrapper.entity.state);
|
||||
List currentStateFilter;
|
||||
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
|
||||
currentStateFilter = entityWrapper.stateFilter;
|
||||
} else {
|
||||
currentStateFilter = stateFilter;
|
||||
}
|
||||
return true;
|
||||
bool showByFilter = currentStateFilter.isEmpty;
|
||||
for (var allowedState in currentStateFilter) {
|
||||
if (allowedState is String && allowedState == entityWrapper.entity.state) {
|
||||
showByFilter = true;
|
||||
break;
|
||||
} else if (allowedState is Map) {
|
||||
try {
|
||||
var tmpVal = allowedState['attribute'] != null ? entityWrapper.entity.getAttribute(allowedState['attribute']) : entityWrapper.entity.state;
|
||||
var valToCompareWith = allowedState['value'];
|
||||
var valToCompare;
|
||||
if (valToCompareWith is! String && tmpVal is String) {
|
||||
valToCompare = double.tryParse(tmpVal);
|
||||
} else {
|
||||
valToCompare = tmpVal;
|
||||
}
|
||||
if (valToCompare != null) {
|
||||
bool result;
|
||||
switch (allowedState['operator']) {
|
||||
case '<=': { result = valToCompare <= valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '<': { result = valToCompare < valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '>=': { result = valToCompare >= valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '>': { result = valToCompare > valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '!=': { result = valToCompare != valToCompareWith;}
|
||||
break;
|
||||
|
||||
case 'regex': {
|
||||
RegExp regExp = RegExp(valToCompareWith.toString());
|
||||
result = regExp.hasMatch(valToCompare.toString());
|
||||
}
|
||||
break;
|
||||
|
||||
default: {
|
||||
result = valToCompare == valToCompareWith;
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
showByFilter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState');
|
||||
Logger.e('$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
return showByFilter;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
@ -132,7 +132,33 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeader(name: card.name));
|
||||
Widget headerSwitch;
|
||||
if (card.showHeaderToggle) {
|
||||
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
|
||||
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
|
||||
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
|
||||
}).map((EntityWrapper en) {
|
||||
return en.entity.entityId;
|
||||
}).toList();
|
||||
headerSwitch = Switch(
|
||||
value: headerToggleVal,
|
||||
onChanged: (val) {
|
||||
if (entitiesToToggle.isNotEmpty) {
|
||||
ConnectionManager().callService(
|
||||
domain: "homeassistant",
|
||||
service: val ? "turn_on" : "turn_off",
|
||||
entityId: entitiesToToggle
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
body.add(
|
||||
CardHeader(
|
||||
name: card.name,
|
||||
trailing: headerSwitch
|
||||
)
|
||||
);
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
body.add(
|
||||
Padding(
|
||||
@ -280,7 +306,7 @@ class CardWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildEntityButtonCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
||||
card.linkedEntityWrapper.overrideName = card.name?.toUpperCase() ??
|
||||
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
@ -294,9 +320,9 @@ class CardWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildGaugeCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name ??
|
||||
card.linkedEntityWrapper.overrideName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
|
||||
card.linkedEntityWrapper.unitOfMeasurementOverride = card.unit ??
|
||||
card.linkedEntityWrapper.unitOfMeasurement;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
@ -312,7 +338,7 @@ class CardWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildLightCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name ??
|
||||
card.linkedEntityWrapper.overrideName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
@ -329,7 +355,11 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildUnsupportedCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeader(name: card.name ?? ""));
|
||||
body.add(
|
||||
CardHeader(
|
||||
name: card.name ?? ""
|
||||
)
|
||||
);
|
||||
List<Widget> result = [];
|
||||
if (card.linkedEntityWrapper != null) {
|
||||
result.addAll(<Widget>[
|
||||
|
@ -21,6 +21,7 @@ class EntityButtonCardBody extends StatelessWidget {
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: Column(
|
||||
|
@ -64,6 +64,7 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
|
@ -60,6 +60,7 @@ class GlanceCardEntityContainer extends StatelessWidget {
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ class _LightCardBodyState extends State<LightCardBody> {
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
|
@ -51,6 +51,10 @@ class EntityUIAction {
|
||||
String holdNavigationPath;
|
||||
String holdService;
|
||||
Map<String, dynamic> holdServiceData;
|
||||
String doubleTapAction = EntityUIAction.none;
|
||||
String doubleTapNavigationPath;
|
||||
String doubleTapService;
|
||||
Map<String, dynamic> doubleTapServiceData;
|
||||
|
||||
EntityUIAction({rawEntityData}) {
|
||||
if (rawEntityData != null) {
|
||||
@ -76,6 +80,17 @@ class EntityUIAction {
|
||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
if (rawEntityData["double_tap_action"] != null) {
|
||||
if (rawEntityData["double_tap_action"] is String) {
|
||||
doubleTapAction = rawEntityData["double_tap_action"];
|
||||
} else {
|
||||
doubleTapAction =
|
||||
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
|
||||
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
|
||||
doubleTapService = rawEntityData["double_tap_action"]["service"];
|
||||
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,12 +3,16 @@ part of '../../main.dart';
|
||||
class CameraEntity extends Entity {
|
||||
|
||||
static const SUPPORT_ON_OFF = 1;
|
||||
static const SUPPORT_STREAM = 2;
|
||||
|
||||
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportOnOff => ((supportedFeatures &
|
||||
CameraEntity.SUPPORT_ON_OFF) ==
|
||||
CameraEntity.SUPPORT_ON_OFF);
|
||||
bool get supportStream => ((supportedFeatures &
|
||||
CameraEntity.SUPPORT_STREAM) ==
|
||||
CameraEntity.SUPPORT_STREAM);
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
|
@ -2,7 +2,9 @@ part of '../../../main.dart';
|
||||
|
||||
class CameraStreamView extends StatefulWidget {
|
||||
|
||||
CameraStreamView({Key key}) : super(key: key);
|
||||
final bool withControls;
|
||||
|
||||
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CameraStreamViewState createState() => _CameraStreamViewState();
|
||||
@ -10,49 +12,235 @@ class CameraStreamView extends StatefulWidget {
|
||||
|
||||
class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
|
||||
CameraEntity _entity;
|
||||
String _streamUrl = "";
|
||||
VideoPlayerController _videoPlayerController;
|
||||
Timer _monitorTimer;
|
||||
bool _isLoaded = false;
|
||||
double _aspectRatio = 1.33;
|
||||
String _webViewHtml;
|
||||
String _jsMessageChannelName = 'unknown';
|
||||
Completer _loading;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
CameraEntity _entity;
|
||||
bool started = false;
|
||||
String streamUrl = "";
|
||||
Future _loadResources() {
|
||||
if (_loading != null && !_loading.isCompleted) {
|
||||
Logger.d("[Camera Player] Resources loading is not finished yet");
|
||||
return _loading.future;
|
||||
}
|
||||
Logger.d("[Camera Player] Loading resources");
|
||||
_loading = Completer();
|
||||
_entity = EntityModel
|
||||
.of(context)
|
||||
.entityWrapper
|
||||
.entity;
|
||||
if (_entity.supportStream) {
|
||||
HomeAssistant().getCameraStream(_entity.entityId)
|
||||
.then((data) {
|
||||
if (_videoPlayerController != null) {
|
||||
_videoPlayerController.dispose().then((_) => createPlayer(data));
|
||||
} else {
|
||||
createPlayer(data);
|
||||
}
|
||||
})
|
||||
.catchError((e) {
|
||||
_loading.completeError(e);
|
||||
Logger.e("[Camera Player] $e");
|
||||
});
|
||||
} else {
|
||||
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
.entityId}?token=${_entity.attributes['access_token']}';
|
||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||
rootBundle.loadString('assets/html/cameraView.html').then((file) {
|
||||
_webViewHtml = Uri.dataFromString(
|
||||
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||
mimeType: 'text/html',
|
||||
encoding: Encoding.getByName('utf-8')
|
||||
).toString();
|
||||
_loading.complete();
|
||||
});
|
||||
}
|
||||
return _loading.future;
|
||||
}
|
||||
|
||||
launchStream() {
|
||||
Launcher.launchURLInCustomTab(
|
||||
context: context,
|
||||
url: streamUrl
|
||||
void createPlayer(data) {
|
||||
_videoPlayerController = VideoPlayerController.network("${ConnectionManager().httpWebHost}${data["url"]}");
|
||||
_videoPlayerController.initialize().then((_) {
|
||||
setState((){
|
||||
_aspectRatio = _videoPlayerController.value.aspectRatio;
|
||||
});
|
||||
_loading.complete();
|
||||
autoPlay();
|
||||
startMonitor();
|
||||
}).catchError((e) {
|
||||
_loading.completeError(e);
|
||||
Logger.e("[Camera Player] Error player init. Retrying");
|
||||
_loadResources();
|
||||
});
|
||||
}
|
||||
|
||||
void autoPlay() {
|
||||
if (!_videoPlayerController.value.isPlaying) {
|
||||
_videoPlayerController.play();
|
||||
}
|
||||
}
|
||||
|
||||
void startMonitor() {
|
||||
_monitorTimer?.cancel();
|
||||
_monitorTimer = Timer.periodic(Duration(milliseconds: 500), (timer) {
|
||||
if (_videoPlayerController.value.hasError) {
|
||||
timer.cancel();
|
||||
setState(() {
|
||||
_isLoaded = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildScreen() {
|
||||
Widget screenWidget;
|
||||
if (!_isLoaded) {
|
||||
screenWidget = Center(
|
||||
child: EntityPicture(
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
);
|
||||
} else if (_entity.supportStream) {
|
||||
if (_videoPlayerController.value.initialized) {
|
||||
screenWidget = VideoPlayer(_videoPlayerController);
|
||||
} else {
|
||||
screenWidget = Center(
|
||||
child: EntityPicture(
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
screenWidget = WebView(
|
||||
initialUrl: _webViewHtml,
|
||||
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
|
||||
debuggingEnabled: Logger.isInDebugMode,
|
||||
gestureNavigationEnabled: false,
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
javascriptChannels: {
|
||||
JavascriptChannel(
|
||||
name: _jsMessageChannelName,
|
||||
onMessageReceived: ((message) {
|
||||
setState((){
|
||||
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
||||
});
|
||||
})
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
return AspectRatio(
|
||||
aspectRatio: _aspectRatio,
|
||||
child: screenWidget
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControls() {
|
||||
Widget playControl;
|
||||
if (_entity.supportStream) {
|
||||
playControl = Center(
|
||||
child: IconButton(
|
||||
icon: Icon((_videoPlayerController != null && _videoPlayerController.value.isPlaying) ? Icons.pause_circle_outline : Icons.play_circle_outline),
|
||||
iconSize: 60,
|
||||
color: Colors.amberAccent,
|
||||
onPressed: (_videoPlayerController == null || _videoPlayerController.value.hasError || !_isLoaded) ? null :
|
||||
() {
|
||||
setState(() {
|
||||
if (_videoPlayerController != null && _videoPlayerController.value.isPlaying) {
|
||||
_videoPlayerController.pause();
|
||||
} else {
|
||||
_videoPlayerController.play();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
playControl = Container();
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
iconSize: 40,
|
||||
color: Colors.amberAccent,
|
||||
onPressed: _isLoaded ? () {
|
||||
setState(() {
|
||||
_isLoaded = false;
|
||||
});
|
||||
} : null,
|
||||
),
|
||||
Expanded(
|
||||
child: playControl,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.fullscreen),
|
||||
iconSize: 40,
|
||||
color: Colors.amberAccent,
|
||||
onPressed: _isLoaded ? () {
|
||||
_videoPlayerController?.pause();
|
||||
eventBus.fire(ShowEntityPageEvent());
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (conext) => FullScreenPage(
|
||||
child: EntityModel(
|
||||
child: CameraStreamView(
|
||||
withControls: false
|
||||
),
|
||||
handleTap: false,
|
||||
entityWrapper: EntityWrapper(
|
||||
entity: _entity
|
||||
),
|
||||
),
|
||||
),
|
||||
fullscreenDialog: true
|
||||
)
|
||||
).then((_) {
|
||||
eventBus.fire(ShowEntityPageEvent(entity: _entity));
|
||||
});
|
||||
} : null,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!started) {
|
||||
_entity = EntityModel
|
||||
.of(context)
|
||||
.entityWrapper
|
||||
.entity;
|
||||
started = true;
|
||||
if (!_isLoaded && (_loading == null || _loading.isCompleted)) {
|
||||
_loadResources().then((_) => setState((){ _isLoaded = true; }));
|
||||
}
|
||||
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
.entityId}?token=${_entity.attributes['access_token']}';
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
||||
iconSize: 50.0,
|
||||
onPressed: () => launchStream(),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
if (widget.withControls) {
|
||||
return Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
_buildScreen(),
|
||||
_buildControls()
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return _buildScreen();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_monitorTimer?.cancel();
|
||||
_videoPlayerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -10,9 +10,8 @@ class ClimateControlWidget extends StatefulWidget {
|
||||
|
||||
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
|
||||
bool _showPending = false;
|
||||
bool _temperaturePending = false;
|
||||
bool _changedHere = false;
|
||||
Timer _resetTimer;
|
||||
Timer _tempThrottleTimer;
|
||||
Timer _targetTempThrottleTimer;
|
||||
double _tmpTemperature = 0.0;
|
||||
@ -27,9 +26,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
bool _tmpAuxHeat = false;
|
||||
|
||||
void _resetVars(ClimateEntity entity) {
|
||||
_tmpTemperature = entity.temperature;
|
||||
_tmpTargetHigh = entity.targetHigh;
|
||||
_tmpTargetLow = entity.targetLow;
|
||||
if (!_temperaturePending) {
|
||||
_tmpTemperature = entity.temperature;
|
||||
_tmpTargetHigh = entity.targetHigh;
|
||||
_tmpTargetLow = entity.targetLow;
|
||||
}
|
||||
_tmpHVACMode = entity.state;
|
||||
_tmpFanMode = entity.fanMode;
|
||||
_tmpSwingMode = entity.swingMode;
|
||||
@ -38,7 +39,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_tmpAuxHeat = entity.auxHeat;
|
||||
_tmpTargetHumidity = entity.targetHumidity;
|
||||
|
||||
_showPending = false;
|
||||
_changedHere = false;
|
||||
}
|
||||
|
||||
@ -73,46 +73,44 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
|
||||
void _setTemperature(ClimateEntity entity) {
|
||||
if (_tempThrottleTimer!=null) {
|
||||
_tempThrottleTimer.cancel();
|
||||
}
|
||||
_tempThrottleTimer?.cancel();
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = true;
|
||||
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
||||
});
|
||||
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = false;
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "set_temperature",
|
||||
entityId: entity.entityId,
|
||||
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _setTargetTemp(ClimateEntity entity) {
|
||||
if (_targetTempThrottleTimer!=null) {
|
||||
_targetTempThrottleTimer.cancel();
|
||||
}
|
||||
_targetTempThrottleTimer?.cancel();
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = true;
|
||||
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
||||
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
||||
});
|
||||
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = false;
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "set_temperature",
|
||||
entityId: entity.entityId,
|
||||
data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -127,7 +125,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
entityId: entity.entityId,
|
||||
data: {"humidity": "$_tmpTargetHumidity"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -141,7 +138,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
entityId: entity.entityId,
|
||||
data: {"hvac_mode": "$_tmpHVACMode"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -155,7 +151,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
entityId: entity.entityId,
|
||||
data: {"swing_mode": "$_tmpSwingMode"}
|
||||
);
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -164,7 +159,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_tmpFanMode = value;
|
||||
_changedHere = true;
|
||||
ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"});
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -173,7 +167,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_tmpPresetMode = value;
|
||||
_changedHere = true;
|
||||
ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"});
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -191,17 +184,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_tmpAuxHeat = value;
|
||||
_changedHere = true;
|
||||
ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"});
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _resetStateTimer(ClimateEntity entity) {
|
||||
if (_resetTimer!=null) {
|
||||
_resetTimer.cancel();
|
||||
}
|
||||
_resetTimer = Timer(Duration(seconds: 3), () {
|
||||
setState(() {});
|
||||
_resetVars(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@ -209,11 +191,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
||||
Logger.d("[Climate widget build] changed here = $_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;
|
||||
} else {
|
||||
_resetTimer?.cancel();
|
||||
_resetVars(entity);
|
||||
}
|
||||
return Padding(
|
||||
@ -321,7 +303,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
)),
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTemperature,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
||||
onDec: () => _temperatureDown(entity),
|
||||
onInc: () => _temperatureUp(entity),
|
||||
)
|
||||
@ -338,7 +320,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
controls.addAll(<Widget>[
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetLow,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
||||
onDec: () => _targetLowDown(entity),
|
||||
onInc: () => _targetLowUp(entity),
|
||||
),
|
||||
@ -351,7 +333,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
controls.add(
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetHigh,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
||||
onDec: () => _targetHighDown(entity),
|
||||
onInc: () => _targetHighUp(entity),
|
||||
)
|
||||
@ -429,7 +411,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_resetTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ part of '../../../main.dart';
|
||||
class ModeSelectorWidget extends StatelessWidget {
|
||||
|
||||
final String caption;
|
||||
final List<String> options;
|
||||
final List options;
|
||||
final String value;
|
||||
final double captionFontSize;
|
||||
final double valueFontSize;
|
||||
@ -45,10 +45,10 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
color: Colors.black,
|
||||
),
|
||||
hint: Text("Select ${caption.toLowerCase()}"),
|
||||
items: options.map((String value) {
|
||||
items: options.map((value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
value: '$value',
|
||||
child: Text('$value'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => onChange(mode),
|
||||
|
@ -61,6 +61,11 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
entityModel.entityWrapper.handleTap();
|
||||
}
|
||||
},
|
||||
onDoubleTap: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleDoubleTap();
|
||||
}
|
||||
},
|
||||
child: result,
|
||||
);
|
||||
} else {
|
||||
|
@ -221,7 +221,7 @@ class Entity {
|
||||
|
||||
String getAttribute(String attributeName) {
|
||||
if (attributes != null) {
|
||||
return attributes["$attributeName"];
|
||||
return attributes["$attributeName"].toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
70
lib/entities/entity_picture.widget.dart
Normal file
70
lib/entities/entity_picture.widget.dart
Normal file
@ -0,0 +1,70 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class EntityPicture extends StatelessWidget {
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
final BoxFit fit;
|
||||
|
||||
const EntityPicture({Key key, this.padding: const EdgeInsets.all(0.0), this.fit: BoxFit.cover}) : super(key: key);
|
||||
|
||||
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||
String domain = entityId.split(".")[0];
|
||||
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
|
||||
String iconNameByDeviceClass;
|
||||
if (deviceClass != null) {
|
||||
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
|
||||
}
|
||||
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
|
||||
if (iconName != null) {
|
||||
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildIcon(EntityWrapper data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
String iconName = data.icon;
|
||||
int iconCode = 0;
|
||||
if (iconName.length > 0) {
|
||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||
} else {
|
||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||
data.entity.deviceClass, data.entity.state); //
|
||||
}
|
||||
Widget iconPicture = Container(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: Sizes.largeIconSize,
|
||||
color: EntityColor.defaultStateColor,
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
if (data.entityPicture != null) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: data.entityPicture,
|
||||
fit: this.fit,
|
||||
errorWidget: (context, _, __) => iconPicture,
|
||||
placeholder: (context, _) => iconPicture,
|
||||
);
|
||||
}
|
||||
|
||||
return iconPicture;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: buildIcon(
|
||||
entityWrapper
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,30 +2,29 @@ part of '../main.dart';
|
||||
|
||||
class EntityWrapper {
|
||||
|
||||
String displayName;
|
||||
String icon;
|
||||
String unitOfMeasurement;
|
||||
String entityPicture;
|
||||
String overrideName;
|
||||
final String overrideIcon;
|
||||
EntityUIAction uiAction;
|
||||
Entity entity;
|
||||
String unitOfMeasurementOverride;
|
||||
final List stateFilter;
|
||||
|
||||
String get icon => overrideIcon ?? entity.icon;
|
||||
String get entityPicture => entity.entityPicture;
|
||||
String get displayName => overrideName ?? entity.displayName;
|
||||
String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement;
|
||||
|
||||
EntityWrapper({
|
||||
this.entity,
|
||||
String icon,
|
||||
String displayName,
|
||||
this.uiAction
|
||||
this.overrideIcon,
|
||||
this.overrideName,
|
||||
this.uiAction,
|
||||
this.stateFilter
|
||||
}) {
|
||||
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||
this.icon = icon ?? entity.icon;
|
||||
if (icon == null) {
|
||||
entityPicture = entity.entityPicture;
|
||||
}
|
||||
this.displayName = displayName ?? entity.displayName;
|
||||
if (uiAction == null) {
|
||||
uiAction = EntityUIAction();
|
||||
}
|
||||
unitOfMeasurement = entity.unitOfMeasurement;
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,4 +112,44 @@ class EntityWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
void handleDoubleTap() {
|
||||
switch (uiAction.doubleTapAction) {
|
||||
case EntityUIAction.toggle: {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.callService: {
|
||||
if (uiAction.doubleTapService != null) {
|
||||
ConnectionManager().callService(
|
||||
domain: uiAction.doubleTapService.split(".")[0],
|
||||
service: uiAction.doubleTapService.split(".")[1],
|
||||
data: uiAction.doubleTapServiceData
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.moreInfo: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entity: entity));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
} else {
|
||||
Launcher.launchURL(uiAction.doubleTapService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -356,13 +356,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
volumeStepWidget = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||
onPressed: () => _setVolumeUp(entity.entityId)
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||
onPressed: () => _setVolumeDown(entity.entityId)
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||
onPressed: () => _setVolumeUp(entity.entityId)
|
||||
)
|
||||
],
|
||||
);
|
||||
|
@ -152,39 +152,12 @@ class EntityCollection {
|
||||
return _allEntities[entityId] != null;
|
||||
}
|
||||
|
||||
List<Entity> getByDomains({List<String> domains, List<String> stateFiler}) {
|
||||
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
|
||||
return _allEntities.values.where((entity) {
|
||||
return domains.contains(entity.domain) &&
|
||||
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
||||
return
|
||||
(excludeDomains.isEmpty || !excludeDomains.contains(entity.domain)) &&
|
||||
(includeDomains.isEmpty || includeDomains.contains(entity.domain)) &&
|
||||
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<Entity> filterEntitiesForDefaultView() {
|
||||
List<Entity> result = [];
|
||||
List<Entity> groups = [];
|
||||
List<Entity> nonGroupEntities = [];
|
||||
_allEntities.forEach((id, entity){
|
||||
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
|
||||
groups.add(entity);
|
||||
}
|
||||
if (!entity.isGroup) {
|
||||
nonGroupEntities.add(entity);
|
||||
}
|
||||
});
|
||||
|
||||
nonGroupEntities.forEach((entity) {
|
||||
bool foundInGroup = false;
|
||||
groups.forEach((groupEntity) {
|
||||
if (groupEntity.childEntityIds.contains(entity.entityId)) {
|
||||
foundInGroup = true;
|
||||
}
|
||||
});
|
||||
if (!foundInGroup) {
|
||||
result.add(entity);
|
||||
}
|
||||
});
|
||||
result.insertAll(0, groups);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ part of 'main.dart';
|
||||
|
||||
class HomeAssistant {
|
||||
|
||||
static const DEFAULT_DASHBOARD = 'lovelace';
|
||||
|
||||
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||
|
||||
factory HomeAssistant() {
|
||||
@ -11,27 +13,33 @@ class HomeAssistant {
|
||||
EntityCollection entities;
|
||||
HomeAssistantUI ui;
|
||||
Map _instanceConfig = {};
|
||||
Map services;
|
||||
String _userName;
|
||||
bool childMode;
|
||||
String _lovelaceDashbordUrl;
|
||||
HSVColor savedColor;
|
||||
int savedPlayerPosition;
|
||||
String sendToPlayerId;
|
||||
String sendFromPlayerId;
|
||||
Map services;
|
||||
bool autoUi = false;
|
||||
|
||||
String fcmToken;
|
||||
|
||||
Map _rawLovelaceData;
|
||||
var _rawStates;
|
||||
var _rawUserInfo;
|
||||
var _rawPanels;
|
||||
|
||||
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
|
||||
|
||||
List<Panel> panels = [];
|
||||
|
||||
Duration fetchTimeout = Duration(seconds: 30);
|
||||
|
||||
String get locationName {
|
||||
if (ConnectionManager().useLovelace) {
|
||||
return ui?.title ?? "";
|
||||
if (!autoUi) {
|
||||
return ui?.title ?? "Home";
|
||||
} else {
|
||||
return _instanceConfig["location_name"] ?? "";
|
||||
return _instanceConfig["location_name"] ?? "Home";
|
||||
}
|
||||
}
|
||||
String get userName => _userName ?? locationName;
|
||||
@ -42,38 +50,37 @@ class HomeAssistant {
|
||||
|
||||
HomeAssistant._internal() {
|
||||
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||
ConnectionManager().onLovelaceUpdatedCallback = _handleLovelaceUpdate;
|
||||
DeviceInfoManager().loadDeviceInfo();
|
||||
}
|
||||
|
||||
Completer _fetchCompleter;
|
||||
|
||||
Future fetchData() {
|
||||
Future fetchData(bool uiOnly) {
|
||||
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||
Logger.w("Previous data fetch is not completed yet");
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
_fetchCompleter = Completer();
|
||||
List<Future> futures = [];
|
||||
futures.add(_getStates());
|
||||
if (ConnectionManager().useLovelace) {
|
||||
futures.add(_getLovelace());
|
||||
if (!uiOnly) {
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
futures.add(_getStates(null));
|
||||
futures.add(_getConfig(null));
|
||||
futures.add(_getUserInfo(null));
|
||||
futures.add(_getPanels(null));
|
||||
futures.add(_getServices(null));
|
||||
}
|
||||
if (!autoUi) {
|
||||
futures.add(_getLovelace(null));
|
||||
}
|
||||
futures.add(_getConfig());
|
||||
futures.add(_getServices());
|
||||
futures.add(_getUserInfo());
|
||||
futures.add(_getPanels());
|
||||
futures.add(ConnectionManager().sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "state_changed"},
|
||||
));
|
||||
Future.wait(futures).then((_) {
|
||||
if (isMobileAppEnabled) {
|
||||
if (!childMode) _createUI();
|
||||
_createUI();
|
||||
_fetchCompleter.complete();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
||||
} else {
|
||||
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app-integration")]));
|
||||
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
|
||||
}
|
||||
}).catchError((e) {
|
||||
_fetchCompleter.completeError(e);
|
||||
@ -81,6 +88,48 @@ class HomeAssistant {
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
|
||||
Future<void> fetchDataFromCache() async {
|
||||
Logger.d('Loading cached data');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
bool cached = prefs.getBool('cached');
|
||||
if (cached != null && cached) {
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
try {
|
||||
_getStates(prefs);
|
||||
if (!autoUi) {
|
||||
_getLovelace(prefs);
|
||||
}
|
||||
_getConfig(prefs);
|
||||
_getUserInfo(prefs);
|
||||
_getPanels(prefs);
|
||||
_getServices(prefs);
|
||||
if (isMobileAppEnabled) {
|
||||
_createUI();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.d('Didnt get cached data: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void saveCache() async {
|
||||
Logger.d('Saving data to cache...');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
try {
|
||||
await prefs.setString('cached_states', json.encode(_rawStates));
|
||||
await prefs.setString('cached_lovelace', json.encode(_rawLovelaceData));
|
||||
await prefs.setString('cached_user', json.encode(_rawUserInfo));
|
||||
await prefs.setString('cached_config', json.encode(_instanceConfig));
|
||||
await prefs.setString('cached_panels', json.encode(_rawPanels));
|
||||
await prefs.setString('cached_services', json.encode(services));
|
||||
await prefs.setBool('cached', true);
|
||||
} catch (e) {
|
||||
await prefs.setBool('cached', false);
|
||||
Logger.e('Error saving cache: $e');
|
||||
}
|
||||
Logger.d('Done saving cache');
|
||||
}
|
||||
|
||||
Future logout() async {
|
||||
Logger.d("Logging out...");
|
||||
await ConnectionManager().logout().then((_) {
|
||||
@ -90,80 +139,177 @@ class HomeAssistant {
|
||||
});
|
||||
}
|
||||
|
||||
Future _getConfig() async {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
}).catchError((e) {
|
||||
throw HAError("Error getting config: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getStates() async {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||
(data) => entities.parse(data)
|
||||
).catchError((e) {
|
||||
throw HAError("Error getting states: $e");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getLovelace() {
|
||||
Completer completer = Completer();
|
||||
ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) {
|
||||
_rawLovelaceData = data;
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
if ("$e" == "config_not_found") {
|
||||
ConnectionManager().useLovelace = false;
|
||||
completer.complete();
|
||||
} else {
|
||||
completer.completeError(HAError("Error getting lovelace config: $e"));
|
||||
Future _getConfig(SharedPreferences sharedPrefs) async {
|
||||
if (sharedPrefs != null) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_config'));
|
||||
_parseConfig(data);
|
||||
} catch (e) {
|
||||
throw HAError("Error getting config: $e");
|
||||
}
|
||||
});
|
||||
return completer.future;
|
||||
} else {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
|
||||
throw HAError("Error getting config: $e");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future _getUserInfo() async {
|
||||
_userName = null;
|
||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) {
|
||||
_userName = data["name"];
|
||||
childMode = _userName.startsWith("[child]");
|
||||
}).catchError((e) {
|
||||
Logger.w("Can't get user info: $e");
|
||||
});
|
||||
void _parseConfig(data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
}
|
||||
|
||||
Future _getServices() async {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
|
||||
Logger.d("Got ${data.length} services");
|
||||
services = data;
|
||||
}).catchError((e) {
|
||||
Future _getStates(SharedPreferences sharedPrefs) async {
|
||||
if (sharedPrefs != null) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_states'));
|
||||
_parseStates(data);
|
||||
} catch (e) {
|
||||
throw HAError("Error getting states: $e");
|
||||
}
|
||||
} else {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||
(data) => _parseStates(data)
|
||||
).catchError((e) {
|
||||
throw HAError("Error getting states: $e");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parseStates(data) {
|
||||
_rawStates = data;
|
||||
entities.parse(data);
|
||||
}
|
||||
|
||||
Future _getLovelace(SharedPreferences sharedPrefs) {
|
||||
if (sharedPrefs != null) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
|
||||
_rawLovelaceData = data;
|
||||
} catch (e) {
|
||||
autoUi = true;
|
||||
}
|
||||
return Future.value();
|
||||
} else {
|
||||
Completer completer = Completer();
|
||||
ConnectionManager().sendSocketMessage(
|
||||
type: 'lovelace/config',
|
||||
additionalData: {
|
||||
'url_path': _lovelaceDashbordUrl == HomeAssistant.DEFAULT_DASHBOARD ? null : _lovelaceDashbordUrl
|
||||
}
|
||||
).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");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getPanels() async {
|
||||
panels.clear();
|
||||
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
|
||||
data.forEach((k,v) {
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
panels.add(Panel(
|
||||
id: k,
|
||||
type: v["component_name"],
|
||||
title: title,
|
||||
urlPath: v["url_path"],
|
||||
config: v["config"],
|
||||
icon: v["icon"]
|
||||
)
|
||||
);
|
||||
});
|
||||
}).catchError((e) {
|
||||
throw HAError("Error getting panels list: $e");
|
||||
void _parseServices(data) {
|
||||
services = data;
|
||||
}
|
||||
|
||||
Future _getUserInfo(SharedPreferences sharedPrefs) async {
|
||||
_userName = null;
|
||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
|
||||
Logger.w("Can't get user info: $e");
|
||||
});
|
||||
}
|
||||
|
||||
void _parseUserInfo(data) {
|
||||
_rawUserInfo = data;
|
||||
_userName = data["name"];
|
||||
}
|
||||
|
||||
Future _getPanels(SharedPreferences sharedPrefs) async {
|
||||
if (sharedPrefs != null) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
||||
_parsePanels(data);
|
||||
} catch (e) {
|
||||
throw HAError("Error getting panels list: $e");
|
||||
}
|
||||
} else {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e) {
|
||||
throw HAError("Error getting panels list: $e");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parsePanels(data) {
|
||||
_rawPanels = data;
|
||||
panels.clear();
|
||||
List<Panel> dashboards = [];
|
||||
data.forEach((k,v) {
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
if (v['component_name'] != null && v['component_name'] == 'lovelace') {
|
||||
dashboards.add(
|
||||
Panel(
|
||||
id: k,
|
||||
componentName: v['component_name'],
|
||||
title: title,
|
||||
urlPath: v['url_path'],
|
||||
config: v['config'],
|
||||
icon: (v['icon'] == null || v['icon'] == 'hass:view-dashboard') ? 'mdi:view-dashboard' : v['icon']
|
||||
)
|
||||
);
|
||||
} else {
|
||||
panels.add(
|
||||
Panel(
|
||||
id: k,
|
||||
componentName: v['component_name'],
|
||||
title: title,
|
||||
urlPath: v['url_path'],
|
||||
config: v['config'],
|
||||
icon: v['icon']
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
panels.insertAll(0, dashboards);
|
||||
}
|
||||
|
||||
Future getCameraStream(String entityId) {
|
||||
Completer completer = Completer();
|
||||
|
||||
ConnectionManager().sendSocketMessage(type: "camera/stream", additionalData: {"entity_id": entityId}).then((data) {
|
||||
completer.complete(data);
|
||||
}).catchError((e) {
|
||||
completer.completeError(e);
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void _handleLovelaceUpdate() {
|
||||
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||
eventBus.fire(new LovelaceChangedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
void _handleEntityStateChange(Map eventData) {
|
||||
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||
if (_fetchCompleter.isCompleted) {
|
||||
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||
Map data = Map.from(eventData);
|
||||
eventBus.fire(new StateChangedEvent(
|
||||
entityId: data["entity_id"],
|
||||
@ -172,212 +318,27 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
void _parseLovelace() {
|
||||
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
||||
ui.title = _rawLovelaceData["title"];
|
||||
int viewCounter = 0;
|
||||
Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
|
||||
_rawLovelaceData["views"].forEach((rawView){
|
||||
Logger.d("----view id: ${rawView['id']}");
|
||||
HAView view = HAView(
|
||||
count: viewCounter,
|
||||
id: "${rawView['id']}",
|
||||
name: rawView['title'],
|
||||
iconName: rawView['icon'],
|
||||
panel: rawView['panel'] ?? false,
|
||||
);
|
||||
|
||||
if (rawView['badges'] != null && rawView['badges'] is List) {
|
||||
rawView['badges'].forEach((entity) {
|
||||
if (entity is String) {
|
||||
if (entities.isExist(entity)) {
|
||||
Entity e = entities.get(entity);
|
||||
view.badges.add(e);
|
||||
}
|
||||
} else {
|
||||
String eId = '${entity['entity']}';
|
||||
if (entities.isExist(eId)) {
|
||||
Entity e = entities.get(eId);
|
||||
view.badges.add(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
|
||||
ui.views.add(
|
||||
view
|
||||
);
|
||||
viewCounter += 1;
|
||||
});
|
||||
}
|
||||
|
||||
List<HACard> _createLovelaceCards(List rawCards) {
|
||||
List<HACard> result = [];
|
||||
rawCards.forEach((rawCard){
|
||||
try {
|
||||
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
var rawCardInfo = rawCard["card"] ?? rawCard;
|
||||
HACard card = HACard(
|
||||
id: "card",
|
||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
|
||||
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
|
||||
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
|
||||
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],
|
||||
states: rawCardInfo['states'],
|
||||
conditions: rawCard['conditions'] ?? [],
|
||||
content: rawCardInfo['content'],
|
||||
min: rawCardInfo['min'] ?? 0,
|
||||
max: rawCardInfo['max'] ?? 100,
|
||||
unit: rawCardInfo['unit'],
|
||||
severity: rawCardInfo['severity']
|
||||
);
|
||||
if (rawCardInfo["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||
}
|
||||
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
|
||||
rawEntities?.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (entities.isExist(rawEntity)) {
|
||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||
} else {
|
||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (rawEntity["type"] == "divider") {
|
||||
card.entities.add(EntityWrapper(entity: Entity.divider()));
|
||||
} else if (rawEntity["type"] == "section") {
|
||||
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
||||
} else if (rawEntity["type"] == "call-service") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.callService,
|
||||
"service": rawEntity["service"],
|
||||
"service_data": rawEntity["service_data"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
card.entities.add(EntityWrapper(
|
||||
entity: Entity.callService(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
service: rawEntity["service"],
|
||||
actionName: rawEntity["action_name"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (rawEntity["type"] == "weblink") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.navigate,
|
||||
"service": rawEntity["url"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
card.entities.add(EntityWrapper(
|
||||
entity: Entity.weblink(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
url: rawEntity["url"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = entities.get(rawEntity["entity"]);
|
||||
card.entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
displayName: rawEntity["name"],
|
||||
icon: rawEntity["icon"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
|
||||
if (rawSingleEntity != null) {
|
||||
var en = rawSingleEntity;
|
||||
if (en is String) {
|
||||
if (entities.isExist(en)) {
|
||||
Entity e = entities.get(en);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: rawCardInfo["icon"],
|
||||
displayName: rawCardInfo["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
|
||||
}
|
||||
} else {
|
||||
if (entities.isExist(en["entity"])) {
|
||||
Entity e = entities.get(en["entity"]);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: en["icon"],
|
||||
displayName: en["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
result.add(card);
|
||||
} catch (e) {
|
||||
Logger.e("There was an error parsing card: ${e.toString()}");
|
||||
}
|
||||
});
|
||||
return result;
|
||||
bool isServiceExist(String service) {
|
||||
return services != null &&
|
||||
services.isNotEmpty &&
|
||||
services.containsKey(service);
|
||||
}
|
||||
|
||||
void _createUI() {
|
||||
ui = HomeAssistantUI();
|
||||
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
||||
Logger.d("Creating Lovelace UI");
|
||||
_parseLovelace();
|
||||
} else {
|
||||
Logger.d("Creating group-based UI");
|
||||
int viewCounter = 0;
|
||||
if (!entities.hasDefaultView) {
|
||||
HAView view = HAView(
|
||||
count: viewCounter,
|
||||
id: "group.default_view",
|
||||
name: "Home",
|
||||
childEntities: entities.filterEntitiesForDefaultView()
|
||||
Logger.d("Creating Lovelace UI");
|
||||
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
|
||||
if (isServiceExist('zha_map')) {
|
||||
panels.add(
|
||||
Panel(
|
||||
id: 'haclient_zha',
|
||||
componentName: 'haclient_zha',
|
||||
title: 'ZHA',
|
||||
urlPath: '/haclient_zha',
|
||||
icon: 'mdi:zigbee'
|
||||
)
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
114
lib/main.dart
114
lib/main.dart
@ -1,6 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -8,6 +10,7 @@ import 'package:web_socket_channel/io.dart';
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:date_format/date_format.dart';
|
||||
@ -22,15 +25,15 @@ import 'package:device_info/device_info.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/circular_slider/single_circular_slider.dart';
|
||||
import 'package:share/receive_share_state.dart';
|
||||
import 'package:share/share.dart';
|
||||
import 'plugins/dynamic_multi_column_layout.dart';
|
||||
import 'plugins/spoiler_card.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:workmanager/workmanager.dart' as workManager;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:battery/battery.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'utils/logger.dart';
|
||||
|
||||
@ -85,6 +88,7 @@ part 'entities/slider/widgets/slider_controls.dart';
|
||||
part 'entities/text/widgets/text_input_state.dart';
|
||||
part 'entities/select/widgets/select_state.dart';
|
||||
part 'entities/simple_state.widget.dart';
|
||||
part 'entities/entity_picture.widget.dart';
|
||||
part 'entities/timer/widgets/timer_state.dart';
|
||||
part 'entities/climate/widgets/climate_state.widget.dart';
|
||||
part 'entities/cover/widgets/cover_state.dart';
|
||||
@ -106,8 +110,9 @@ part 'pages/widgets/product_purchase.widget.dart';
|
||||
part 'pages/widgets/page_loading_indicator.dart';
|
||||
part 'pages/widgets/page_loading_error.dart';
|
||||
part 'pages/panel.page.dart';
|
||||
part 'pages/main.page.dart';
|
||||
part 'pages/main/main.page.dart';
|
||||
part 'pages/integration_settings.page.dart';
|
||||
part 'pages/zha_page.dart';
|
||||
part 'home_assistant.class.dart';
|
||||
part 'pages/log.page.dart';
|
||||
part 'pages/entity.page.dart';
|
||||
@ -137,13 +142,13 @@ part 'entities/entity_page_layout.widget.dart';
|
||||
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
|
||||
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
|
||||
part 'pages/whats_new.page.dart';
|
||||
part 'pages/fullscreen.page.dart';
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
final SentryClient _sentry = SentryClient(dsn: "https://03ef364745cc4c23a60ddbc874c69925@sentry.io/1836118");
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = "HA Client";
|
||||
const appVersionNumber = "0.7.6";
|
||||
const appVersionNumber = "0.8.0";
|
||||
const appVersionAdd = "";
|
||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
||||
|
||||
@ -152,27 +157,20 @@ Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||
if (Logger.isInDebugMode) {
|
||||
Logger.e('Caught error: $error');
|
||||
Logger.p(stackTrace);
|
||||
return;
|
||||
} else {
|
||||
Logger.e('Caught error: $error. Reporting to Senrty.');
|
||||
// Send the Exception and Stacktrace to Sentry in Production mode.
|
||||
_sentry.captureException(
|
||||
exception: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
Crashlytics.instance.recordError(error, stackTrace);
|
||||
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Crashlytics.instance.enableInDevMode = false;
|
||||
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
Logger.e(" Caut Flutter runtime error: ${details.exception}");
|
||||
if (Logger.isInDebugMode) {
|
||||
FlutterError.dumpErrorToConsole(details);
|
||||
} else {
|
||||
// In production mode, report to the application zone to report to
|
||||
// Sentry.
|
||||
Zone.current.handleUncaughtError(details.exception, details.stack);
|
||||
}
|
||||
Crashlytics.instance.recordFlutterError(details);
|
||||
};
|
||||
|
||||
runZoned(() {
|
||||
@ -182,7 +180,46 @@ void main() async {
|
||||
});
|
||||
}
|
||||
|
||||
class HAClientApp extends StatelessWidget {
|
||||
class HAClientApp extends StatefulWidget {
|
||||
|
||||
@override
|
||||
_HAClientAppState createState() => new _HAClientAppState();
|
||||
|
||||
}
|
||||
|
||||
class _HAClientAppState extends State<HAClientApp> {
|
||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
InAppPurchaseConnection.enablePendingPurchases();
|
||||
final Stream purchaseUpdates =
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
_subscription = purchaseUpdates.listen((purchases) {
|
||||
_handlePurchaseUpdates(purchases);
|
||||
});
|
||||
workManager.Workmanager.initialize(
|
||||
updateDeviceLocationIsolate,
|
||||
isInDebugMode: false
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _handlePurchaseUpdates(purchase) {
|
||||
if (purchase is List<PurchaseDetails>) {
|
||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||
eventBus.fire(ShowPopupMessageEvent(
|
||||
title: "Thanks a lot!",
|
||||
body: "Thank you for supporting HA Client development!",
|
||||
buttonText: "Ok"
|
||||
));
|
||||
} else {
|
||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||
}
|
||||
} else {
|
||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
||||
}
|
||||
}
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
@ -203,8 +240,43 @@ class HAClientApp extends StatelessWidget {
|
||||
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
|
||||
),
|
||||
"/log-view": (context) => LogViewPage(title: "Log"),
|
||||
"/whats-new": (context) => WhatsNewPage()
|
||||
"/webview": (context) => standaloneWebview.WebviewScaffold(
|
||||
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop()
|
||||
),
|
||||
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
|
||||
),
|
||||
),
|
||||
"/whats-new": (context) => WhatsNewPage(),
|
||||
"/haclient_zha": (context) => ZhaPage(),
|
||||
"/auth": (context) => new standaloneWebview.WebviewScaffold(
|
||||
url: "${ConnectionManager().oauthUrl}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.help),
|
||||
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#authentication")
|
||||
),
|
||||
title: new Text("Login with HA"),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text("Manual", style: TextStyle(color: Colors.white)),
|
||||
onPressed: () {
|
||||
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -9,46 +9,37 @@ class AuthManager {
|
||||
}
|
||||
|
||||
AuthManager._internal();
|
||||
StreamSubscription deepLinksSubscription;
|
||||
|
||||
Future start({String oauthUrl}) {
|
||||
Completer completer = Completer();
|
||||
deepLinksSubscription?.cancel();
|
||||
deepLinksSubscription = getUriLinksStream().listen((Uri uri) {
|
||||
Logger.d("[LINKED AUTH] We got something private");
|
||||
_getTempToken(oauthUrl, uri.queryParameters["code"])
|
||||
.then((tempToken) => completer.complete(tempToken))
|
||||
.catchError((_){
|
||||
completer.completeError(HAError("Auth error"));
|
||||
});
|
||||
}, onError: (err) {
|
||||
Logger.e("[LINKED AUTH] Error handling linked auth: $e");
|
||||
completer.completeError(HAError("Auth error"));
|
||||
});
|
||||
Logger.d("Launching OAuth");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future _getTempToken(String oauthUrl,String authCode) {
|
||||
Completer completer = Completer();
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/auth/token",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
includeAuthHeader: false,
|
||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems')}"
|
||||
final flutterWebviewPlugin = new standaloneWebview.FlutterWebviewPlugin();
|
||||
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
||||
if (url.startsWith("https://ha-client.app/service/auth_callback.html")) {
|
||||
Logger.d("url=$url");
|
||||
String authCode = url.split("=")[1];
|
||||
Logger.d("authCode=$authCode");
|
||||
Logger.d("We have auth code. Getting temporary access token...");
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/auth/token",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
includeAuthHeader: false,
|
||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('https://ha-client.app')}"
|
||||
).then((response) {
|
||||
Logger.d("Got temp token");
|
||||
String tempToken = json.decode(response)['access_token'];
|
||||
Logger.d("Closing webview...");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.complete(tempToken);
|
||||
}).catchError((e) {
|
||||
//flutterWebviewPlugin.close();
|
||||
Logger.e("Error getting temp token: ${e.toString()}");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.completeError(HAError("Error getting temp token"));
|
||||
});
|
||||
}).whenComplete(() => flutterWebviewPlugin.close());
|
||||
}
|
||||
});
|
||||
Logger.d("Launching OAuth");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -19,7 +19,6 @@ class ConnectionManager {
|
||||
String _tempToken;
|
||||
String oauthUrl;
|
||||
String webhookId;
|
||||
bool useLovelace = true;
|
||||
bool settingsLoaded = false;
|
||||
bool get isAuthenticated => _token != null;
|
||||
StreamSubscription _socketSubscription;
|
||||
@ -28,6 +27,7 @@ class ConnectionManager {
|
||||
bool isConnected = false;
|
||||
|
||||
var onStateChangeCallback;
|
||||
var onLovelaceUpdatedCallback;
|
||||
|
||||
IOWebSocketChannel _socket;
|
||||
|
||||
@ -38,9 +38,8 @@ class ConnectionManager {
|
||||
Completer completer = Completer();
|
||||
bool stopInit = false;
|
||||
if (loadSettings) {
|
||||
Logger.e("Loading settings...");
|
||||
Logger.d("Loading settings...");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
||||
_domain = prefs.getString('hassio-domain');
|
||||
_port = prefs.getString('hassio-port');
|
||||
webhookId = prefs.getString('app-webhook-id');
|
||||
@ -59,9 +58,9 @@ class ConnectionManager {
|
||||
_token = await storage.read(key: "hacl_llt");
|
||||
Logger.e("Long-lived token read successful");
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'http://ha-client.homemade.systems')}&redirect_uri=${Uri
|
||||
'https://ha-client.app')}&redirect_uri=${Uri
|
||||
.encodeComponent(
|
||||
'haclient://auth')}";
|
||||
'https://ha-client.app/service/auth_callback.html')}";
|
||||
settingsLoaded = true;
|
||||
} catch (e) {
|
||||
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||
@ -148,11 +147,21 @@ class ConnectionManager {
|
||||
});
|
||||
} else if (data["type"] == "auth_ok") {
|
||||
Logger.d("[Received] <== ${data.toString()}");
|
||||
_messageResolver["auth"]?.complete();
|
||||
_messageResolver.remove("auth");
|
||||
if (_token != null) {
|
||||
if (!connecting.isCompleted) connecting.complete();
|
||||
}
|
||||
Logger.d("[Connection] Subscribing to events");
|
||||
sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "lovelace_updated"},
|
||||
);
|
||||
sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "state_changed"},
|
||||
).whenComplete((){
|
||||
_messageResolver["auth"]?.complete();
|
||||
_messageResolver.remove("auth");
|
||||
if (_token != null) {
|
||||
if (!connecting.isCompleted) connecting.complete();
|
||||
}
|
||||
});
|
||||
} else if (data["type"] == "auth_invalid") {
|
||||
Logger.d("[Received] <== ${data.toString()}");
|
||||
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||
@ -205,6 +214,17 @@ class ConnectionManager {
|
||||
}
|
||||
_messageResolver.remove("${data["id"]}");
|
||||
} else if (data["type"] == "event") {
|
||||
if (data["event"] != null) {
|
||||
if (data["event"]["event_type"] == "state_changed") {
|
||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||
onStateChangeCallback(data["event"]["data"]);
|
||||
} else if (data["event"]["event_type"] == "lovelace_updated") {
|
||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: $data");
|
||||
onLovelaceUpdatedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||
onStateChangeCallback(data["event"]["data"]);
|
||||
@ -345,7 +365,7 @@ class ConnectionManager {
|
||||
_currentMessageId += 1;
|
||||
}
|
||||
|
||||
Future callService({@required String domain, @required String service, String entityId, Map data}) {
|
||||
Future callService({@required String domain, @required String service, entityId, Map data}) {
|
||||
eventBus.fire(NotifyServiceCallEvent(domain, service, entityId));
|
||||
Logger.d("Service call: $domain.$service, $entityId, $data");
|
||||
Completer completer = Completer();
|
||||
|
@ -95,8 +95,8 @@ class LocationManager {
|
||||
backoffPolicy: workManager.BackoffPolicy.linear,
|
||||
backoffPolicyDelay: interval,
|
||||
constraints: workManager.Constraints(
|
||||
networkType: workManager.NetworkType.connected
|
||||
)
|
||||
networkType: workManager.NetworkType.connected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -104,7 +104,7 @@ class LocationManager {
|
||||
|
||||
_stopLocationService() async {
|
||||
Logger.d("Canceling previous schedule if any...");
|
||||
await workManager.Workmanager.cancelByTag(backgroundTaskTag);
|
||||
await workManager.Workmanager.cancelAll();
|
||||
}
|
||||
|
||||
updateDeviceLocation() async {
|
||||
@ -148,52 +148,90 @@ class LocationManager {
|
||||
}
|
||||
|
||||
void updateDeviceLocationIsolate() {
|
||||
workManager.Workmanager.executeTask((backgroundTask, data) {
|
||||
print("[Background $backgroundTask] Started");
|
||||
final SentryClient sentryBackgroundClient = SentryClient(dsn: "https://5c868e5ef26947e2b61b189e391ec31b@sentry.io/1836366");
|
||||
workManager.Workmanager.executeTask((backgroundTask, data) async {
|
||||
//print("[Background $backgroundTask] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
int batteryLevel = 100;
|
||||
String webhookId = data["webhookId"];
|
||||
String httpWebHost = data["httpWebHost"];
|
||||
//String logData = '==> ${DateTime.now()} [Background $backgroundTask]:';
|
||||
//print("[Background $backgroundTask] Getting path for log file...");
|
||||
//final logFileDirectory = await getExternalStorageDirectory();
|
||||
//print("[Background $backgroundTask] Opening log file...");
|
||||
//File logFile = File('${logFileDirectory.path}/ha-client-background-log.txt');
|
||||
//print("[Background $backgroundTask] Log file path: ${logFile.path}");
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
print("[Background $backgroundTask] hour=$battery");
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map<String, String> headers = {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [],
|
||||
"gps_accuracy": 0,
|
||||
"battery": batteryLevel
|
||||
}
|
||||
};
|
||||
print("[Background $backgroundTask] Getting battery level...");
|
||||
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){
|
||||
print("[Background $backgroundTask] Getting device location...");
|
||||
geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways).then((location) {
|
||||
if (location != null) {
|
||||
print("[Background $backgroundTask] Got location: ${location.latitude} ${location.longitude}");
|
||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||
data["data"]["gps_accuracy"] = location.accuracy;
|
||||
print("[Background $backgroundTask] Sending data home.");
|
||||
http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: json.encode(data)
|
||||
);
|
||||
} else {
|
||||
throw "Can't get device location. Location is null";
|
||||
}
|
||||
}).catchError((e) {
|
||||
sentryBackgroundClient.captureException(
|
||||
exception: "${e.toString()}"
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map<String, String> headers = {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [],
|
||||
"gps_accuracy": 0,
|
||||
"battery": 100
|
||||
}
|
||||
};
|
||||
//print("[Background $backgroundTask] Getting battery level...");
|
||||
int batteryLevel;
|
||||
try {
|
||||
batteryLevel = await battery.batteryLevel;
|
||||
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
|
||||
} catch(e) {
|
||||
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
|
||||
batteryLevel = 0;
|
||||
//logData += 'Battery: error, $e';
|
||||
}
|
||||
if (batteryLevel != null) {
|
||||
data["data"]["battery"] = batteryLevel;
|
||||
//logData += 'Battery: success, $batteryLevel';
|
||||
}/* else {
|
||||
logData += 'Battery: error, level is null';
|
||||
}*/
|
||||
Position location;
|
||||
try {
|
||||
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
|
||||
if (location != null && location.latitude != null) {
|
||||
//logData += ' || Location: success, ${location.latitude} ${location.longitude} (${location.timestamp})';
|
||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||
data["data"]["gps_accuracy"] = location.accuracy;
|
||||
try {
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: json.encode(data)
|
||||
);
|
||||
print("[Background $backgroundTask] Error getting current location: ${e.toString()}");
|
||||
});
|
||||
});
|
||||
/*if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
logData += ' || Post: success, ${response.statusCode}';
|
||||
} else {
|
||||
logData += ' || Post: error, ${response.statusCode}';
|
||||
}*/
|
||||
} catch(e) {
|
||||
//logData += ' || Post: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += ' || Location: error, location is null';
|
||||
}*/
|
||||
} catch (e) {
|
||||
//print("[Background $backgroundTask] Location error: $e");
|
||||
//logData += ' || Location: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += 'Not configured';
|
||||
}*/
|
||||
//print("[Background $backgroundTask] Writing log data...");
|
||||
/*try {
|
||||
var fileMode;
|
||||
if (logFile.existsSync() && logFile.lengthSync() < 5000000) {
|
||||
fileMode = FileMode.append;
|
||||
} else {
|
||||
fileMode = FileMode.write;
|
||||
}
|
||||
await logFile.writeAsString('$logData\n', mode: fileMode);
|
||||
} catch (e) {
|
||||
print("[Background $backgroundTask] Error writing log: $e");
|
||||
}
|
||||
return Future.value(true);
|
||||
print("[Background $backgroundTask] Finished.");*/
|
||||
return true;
|
||||
});
|
||||
}
|
@ -9,12 +9,12 @@ class StartupUserMessagesManager {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
StartupUserMessagesManager._internal() {}
|
||||
StartupUserMessagesManager._internal();
|
||||
|
||||
bool _supportAppDevelopmentMessageShown;
|
||||
bool _whatsNewMessageShown;
|
||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||
static final _whatsNewMessageKey = "user-message-shown-whats-new-706";
|
||||
static final _whatsNewMessageKey = "user-message-shown-whats-new-884";
|
||||
|
||||
void checkMessagesToShow() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
18
lib/pages/fullscreen.page.dart
Normal file
18
lib/pages/fullscreen.page.dart
Normal file
@ -0,0 +1,18 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class FullScreenPage extends StatelessWidget {
|
||||
|
||||
final Widget child;
|
||||
|
||||
const FullScreenPage({Key key, this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: this.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -118,7 +118,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
InkWell(
|
||||
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#location-tracking"),
|
||||
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
|
||||
child: Text(
|
||||
"Please read documentation!",
|
||||
style: TextStyle(
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
MainPage({Key key, this.title}) : super(key: key);
|
||||
@ -9,10 +9,10 @@ class MainPage extends StatefulWidget {
|
||||
_MainPageState createState() => new _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||
|
||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _lovelaceSubscription;
|
||||
StreamSubscription _settingsSubscription;
|
||||
StreamSubscription _serviceCallSubscription;
|
||||
StreamSubscription _showEntityPageSubscription;
|
||||
@ -25,22 +25,11 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
int _previousViewCount;
|
||||
bool _showLoginButton = false;
|
||||
bool _preventAppRefresh = false;
|
||||
String _savedSharedText;
|
||||
Entity _entityToShow;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final Stream purchaseUpdates =
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
_subscription = purchaseUpdates.listen((purchases) {
|
||||
_handlePurchaseUpdates(purchases);
|
||||
});
|
||||
workManager.Workmanager.initialize(
|
||||
updateDeviceLocationIsolate,
|
||||
isInDebugMode: false
|
||||
);
|
||||
enableShareReceiving();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_firebaseMessaging.configure(
|
||||
@ -81,12 +70,6 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
_fullLoad();
|
||||
}
|
||||
|
||||
@override void receiveShare(Share shared) {
|
||||
if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) {
|
||||
_savedSharedText = shared.text;
|
||||
}
|
||||
}
|
||||
|
||||
Future onSelectNotification(String payload) async {
|
||||
if (payload != null) {
|
||||
Logger.d('Notification clicked: ' + payload);
|
||||
@ -108,43 +91,40 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
);
|
||||
}
|
||||
|
||||
void _fullLoad() async {
|
||||
void _fullLoad() {
|
||||
_showInfoBottomBar(progress: true,);
|
||||
_subscribe().then((_) {
|
||||
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||
_fetchData();
|
||||
LocationManager();
|
||||
StartupUserMessagesManager().checkMessagesToShow();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||
_fetchData(useCache: true);
|
||||
LocationManager();
|
||||
StartupUserMessagesManager().checkMessagesToShow();
|
||||
});
|
||||
}, onError: (e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _quickLoad() {
|
||||
void _quickLoad({bool uiOnly: false}) {
|
||||
_hideBottomBar();
|
||||
_showInfoBottomBar(progress: true,);
|
||||
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
||||
_fetchData();
|
||||
_fetchData(useCache: false, uiOnly: uiOnly);
|
||||
}, onError: (e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
}
|
||||
|
||||
_fetchData() async {
|
||||
if (_savedSharedText != null && !HomeAssistant().isNoEntities) {
|
||||
Logger.d("Got shared text: $_savedSharedText");
|
||||
Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText});
|
||||
_savedSharedText = null;
|
||||
_fetchData({useCache: false, uiOnly: false}) async {
|
||||
if (useCache && !uiOnly) {
|
||||
HomeAssistant().fetchDataFromCache().then((_) {
|
||||
setState((){});
|
||||
});
|
||||
}
|
||||
await HomeAssistant().fetchData().then((_) {
|
||||
await HomeAssistant().fetchData(uiOnly).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;
|
||||
}
|
||||
if (_entityToShow != null) {
|
||||
_entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
|
||||
}
|
||||
@ -163,40 +143,32 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
Logger.d("$state");
|
||||
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||
_quickLoad();
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
} else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||
HomeAssistant().saveCache();
|
||||
}
|
||||
}
|
||||
|
||||
Future _subscribe() {
|
||||
Completer completer = Completer();
|
||||
|
||||
if (_stateSubscription == null) {
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.needToRebuildUI) {
|
||||
Logger.d("New entity. Need to rebuild UI");
|
||||
Logger.d("Need to rebuild UI");
|
||||
_quickLoad();
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (_lovelaceSubscription == null) {
|
||||
_lovelaceSubscription = eventBus.on<LovelaceChangedEvent>().listen((event) {
|
||||
_quickLoad();
|
||||
});
|
||||
}
|
||||
if (_reloadUISubscription == null) {
|
||||
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
||||
_quickLoad();
|
||||
_quickLoad(uiOnly: true);
|
||||
});
|
||||
}
|
||||
if (_showPopupDialogSubscription == null) {
|
||||
@ -258,6 +230,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
_showOAuth();
|
||||
} else {
|
||||
_preventAppRefresh = false;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -271,9 +244,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
|
||||
void _showOAuth() {
|
||||
_preventAppRefresh = true;
|
||||
Launcher.launchURLInCustomTab(
|
||||
url: ConnectionManager().oauthUrl
|
||||
);
|
||||
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
|
||||
}
|
||||
|
||||
_setErrorState(HAError e) {
|
||||
@ -323,7 +294,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
);
|
||||
}
|
||||
|
||||
void _notifyServiceCalled(String domain, String service, String entityId) {
|
||||
void _notifyServiceCalled(String domain, String service, entityId) {
|
||||
_showInfoBottomBar(
|
||||
message: "Calling $domain.$service",
|
||||
duration: Duration(seconds: 4)
|
||||
@ -396,7 +367,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
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,)
|
||||
panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@ -460,26 +431,33 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
title: Text("Help"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("http://ha-client.homemade.systems/docs");
|
||||
Launcher.launchURL("http://ha-client.app/docs");
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
||||
title: Text("Join Discord channel"),
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:forum")),
|
||||
title: Text("Contacts/Discussion"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("https://discord.gg/AUzEvwn");
|
||||
Launcher.launchURL("https://spectrum.chat/ha-client");
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
title: Text("What's new?"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/whats-new');
|
||||
}
|
||||
),
|
||||
new AboutListTile(
|
||||
aboutBoxChildren: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("http://ha-client.homemade.systems/");
|
||||
Launcher.launchURL("http://ha-client.app/");
|
||||
},
|
||||
child: Text(
|
||||
"ha-client.homemade.systems",
|
||||
"ha-client.app",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
@ -492,7 +470,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/terms_and_conditions");
|
||||
},
|
||||
child: Text(
|
||||
"Terms and Conditions",
|
||||
@ -508,7 +486,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/privacy_policy");
|
||||
},
|
||||
child: Text(
|
||||
"Privacy Policy",
|
||||
@ -643,6 +621,13 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
List<PopupMenuItem<String>> serviceMenuItems = [];
|
||||
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>(
|
||||
child: new Text("Reload"),
|
||||
value: "reload",
|
||||
@ -658,7 +643,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
Widget mediaMenuIcon;
|
||||
int playersCount = 0;
|
||||
if (!empty && !HomeAssistant().entities.isEmpty) {
|
||||
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(domains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
||||
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
||||
playersCount = activePlayers.length;
|
||||
mediaMenuItems.addAll(
|
||||
activePlayers.map((entity) => PopupMenuItem<String>(
|
||||
@ -736,7 +721,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: HomeAssistant().buildViews(context, _viewsTabController),
|
||||
child: HomeAssistant().ui.build(context, _viewsTabController),
|
||||
),
|
||||
Container(
|
||||
width: Sizes.mainPageScreenSeparatorWidth,
|
||||
@ -751,7 +736,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
} else if (_entityToShow != null) {
|
||||
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
|
||||
} else {
|
||||
mainScrollBody = HomeAssistant().buildViews(context, _viewsTabController);
|
||||
mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
|
||||
}
|
||||
}
|
||||
|
||||
@ -789,7 +774,9 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
context: context,
|
||||
items: serviceMenuItems
|
||||
).then((String val) {
|
||||
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
|
||||
if (val == "reload") {
|
||||
|
||||
_quickLoad();
|
||||
} else if (val == "logout") {
|
||||
HomeAssistant().logout().then((_) {
|
||||
@ -872,41 +859,43 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
);
|
||||
}
|
||||
}
|
||||
// This method is rerun every time setState is called.
|
||||
if (HomeAssistant().isNoViews) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
primary: false,
|
||||
drawer: _buildAppDrawer(),
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: _buildScaffoldBody(true)
|
||||
);
|
||||
} else {
|
||||
return WillPopScope(
|
||||
child: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
drawer: _buildAppDrawer(),
|
||||
primary: false,
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: _buildScaffoldBody(false)
|
||||
),
|
||||
onWillPop: () {
|
||||
if (_entityToShow != null) {
|
||||
eventBus.fire(ShowEntityPageEvent());
|
||||
return Future.value(false);
|
||||
} else {
|
||||
return Future.value(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
primary: false,
|
||||
drawer: _buildAppDrawer(),
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: _buildScaffoldBody(true)
|
||||
);
|
||||
} else {
|
||||
return WillPopScope(
|
||||
child: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
drawer: _buildAppDrawer(),
|
||||
primary: false,
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: _buildScaffoldBody(false)
|
||||
),
|
||||
onWillPop: () {
|
||||
if (_entityToShow != null) {
|
||||
eventBus.fire(ShowEntityPageEvent());
|
||||
return Future.value(false);
|
||||
} else {
|
||||
return Future.value(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
//flutterWebviewPlugin.dispose();
|
||||
_viewsTabController?.dispose();
|
||||
_stateSubscription?.cancel();
|
||||
_lovelaceSubscription?.cancel();
|
||||
_settingsSubscription?.cancel();
|
||||
_serviceCallSubscription?.cancel();
|
||||
_showPopupDialogSubscription?.cancel();
|
||||
@ -914,7 +903,6 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
|
||||
_showEntityPageSubscription?.cancel();
|
||||
_showErrorSubscription?.cancel();
|
||||
_startAuthSubscription?.cancel();
|
||||
_subscription?.cancel();
|
||||
_showPageSubscription?.cancel();
|
||||
_reloadUISubscription?.cancel();
|
||||
//TODO disconnect
|
@ -57,9 +57,9 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
||||
_loaded = false;
|
||||
});
|
||||
} else {
|
||||
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
|
||||
_isMediaExtractorExist = HomeAssistant().isServiceExist("media_extractor");
|
||||
//_useMediaExtractor = _isMediaExtractorExist;
|
||||
_players = HomeAssistant().entities.getByDomains(domains: ["media_player"]);
|
||||
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
|
||||
setState(() {
|
||||
if (_players.isNotEmpty) {
|
||||
_loaded = true;
|
||||
|
@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
|
||||
error = "";
|
||||
});
|
||||
http.Response response;
|
||||
response = await http.get("http://ha-client.homemade.systems/service/whats_new_0.7.0.md");
|
||||
response = await http.get("http://ha-client.app/service/whats_new_0.8.md");
|
||||
if (response.statusCode == 200) {
|
||||
setState(() {
|
||||
data = response.body;
|
||||
|
90
lib/pages/zha_page.dart
Normal file
90
lib/pages/zha_page.dart
Normal file
@ -0,0 +1,90 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ZhaPage extends StatefulWidget {
|
||||
ZhaPage({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ZhaPageState createState() => new _ZhaPageState();
|
||||
}
|
||||
|
||||
class _ZhaPageState extends State<ZhaPage> {
|
||||
|
||||
List data = [];
|
||||
String error = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
_loadData() async {
|
||||
setState(() {
|
||||
data.clear();
|
||||
error = "";
|
||||
});
|
||||
ConnectionManager().sendSocketMessage(
|
||||
type: 'zha_map/devices'
|
||||
).then((response){
|
||||
setState(() {
|
||||
data = response['devices'];
|
||||
});
|
||||
}).catchError((e){
|
||||
setState(() {
|
||||
error = '$e';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (error.isNotEmpty) {
|
||||
body = PageLoadingError(errorText: error,);
|
||||
} else if (data.isEmpty) {
|
||||
body = PageLoadingIndicator();
|
||||
} else {
|
||||
List<Widget> devicesListWindet = [];
|
||||
data.forEach((device) {
|
||||
devicesListWindet.add(
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CardHeader(
|
||||
name: '${device['ieee']}',
|
||||
subtitle: Text('${device['manufacturer']}'),
|
||||
),
|
||||
Text('${device['device_type']}'),
|
||||
Text('model: ${device['model']}'),
|
||||
Text('offline: ${device['offline']}'),
|
||||
Text('neighbours: ${device['neighbours'].length}'),
|
||||
Text('raw: $device'),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
body = ListView(
|
||||
children: devicesListWindet
|
||||
);
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: () => _loadData(),
|
||||
)
|
||||
],
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: new Text('ZHA'),
|
||||
),
|
||||
body: body
|
||||
);
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ class Panel {
|
||||
};
|
||||
|
||||
final String id;
|
||||
final String type;
|
||||
final String componentName;
|
||||
final String title;
|
||||
final String urlPath;
|
||||
final Map config;
|
||||
@ -19,34 +19,61 @@ class Panel {
|
||||
bool isHidden = true;
|
||||
bool isWebView = false;
|
||||
|
||||
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
||||
Panel({this.id, this.componentName, this.title, this.urlPath, this.icon, this.config}) {
|
||||
if (icon == null || !icon.startsWith("mdi:")) {
|
||||
icon = Panel.iconsByComponent[type];
|
||||
icon = Panel.iconsByComponent[componentName];
|
||||
}
|
||||
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
|
||||
isWebView = (type != 'config');
|
||||
isHidden = (componentName == 'kiosk' || componentName == 'states' || componentName == 'profile' || componentName == 'developer-tools');
|
||||
isWebView = (componentName != 'config' && componentName != 'lovelace' && !componentName.startsWith('haclient'));
|
||||
}
|
||||
|
||||
void handleOpen(BuildContext context) {
|
||||
if (type == "config") {
|
||||
if (componentName == "config") {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PanelPage(title: "$title", panel: this),
|
||||
)
|
||||
);
|
||||
} else if (componentName.startsWith('haclient')) {
|
||||
Navigator.of(context).pushNamed(urlPath);
|
||||
} else if (componentName == 'lovelace') {
|
||||
HomeAssistant().lovelaceDashboardUrl = this.urlPath;
|
||||
HomeAssistant().autoUi = false;
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setString('lovelace_dashboard_url', this.urlPath);
|
||||
eventBus.fire(ReloadUIEvent());
|
||||
});
|
||||
} else {
|
||||
Launcher.launchURLInCustomTab(url: "${ConnectionManager().httpWebHost}/$urlPath");
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
|
||||
}
|
||||
}
|
||||
|
||||
Widget getMenuItemWidget(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(this.icon)),
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("${this.title}"),
|
||||
Container(width: 4.0,),
|
||||
isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
this.handleOpen(context);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget getWidget() {
|
||||
switch (type) {
|
||||
switch (componentName) {
|
||||
case "config": {
|
||||
return ConfigPanelWidget();
|
||||
}
|
||||
|
||||
default: {
|
||||
return Text("Unsupported panel component: $type");
|
||||
return Text("Unsupported panel component: $componentName");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ class LinkToWebConfig extends StatelessWidget {
|
||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
||||
subtitle: Text("Tap to open web version"),
|
||||
onTap: () {
|
||||
Launcher.launchURLInCustomTab(url: this.url);
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
|
||||
},
|
||||
)
|
||||
],
|
||||
|
@ -12,6 +12,8 @@ class StateChangedEvent {
|
||||
});
|
||||
}
|
||||
|
||||
class LovelaceChangedEvent {}
|
||||
|
||||
class SettingsChangedEvent {
|
||||
bool reconnect;
|
||||
|
||||
@ -36,7 +38,7 @@ class StartAuthEvent {
|
||||
class NotifyServiceCallEvent {
|
||||
String domain;
|
||||
String service;
|
||||
String entityId;
|
||||
var entityId;
|
||||
|
||||
NotifyServiceCallEvent(this.domain, this.service, this.entityId);
|
||||
}
|
||||
|
45
lib/ui.dart
45
lib/ui.dart
@ -6,8 +6,51 @@ class HomeAssistantUI {
|
||||
|
||||
bool get isEmpty => views == null || views.isEmpty;
|
||||
|
||||
HomeAssistantUI() {
|
||||
HomeAssistantUI({rawLovelaceConfig}) {
|
||||
if (rawLovelaceConfig == null) {
|
||||
rawLovelaceConfig = _generateLovelaceConfig();
|
||||
}
|
||||
views = [];
|
||||
Logger.d("--Title: ${rawLovelaceConfig["title"]}");
|
||||
title = rawLovelaceConfig["title"];
|
||||
int viewCounter = 0;
|
||||
Logger.d("--Views count: ${rawLovelaceConfig['views'].length}");
|
||||
rawLovelaceConfig["views"].forEach((rawView){
|
||||
Logger.d("----view id: ${rawView['id']}");
|
||||
HAView view = HAView(
|
||||
count: viewCounter,
|
||||
rawData: rawView
|
||||
);
|
||||
|
||||
views.add(
|
||||
view
|
||||
);
|
||||
viewCounter += 1;
|
||||
});
|
||||
}
|
||||
|
||||
Map _generateLovelaceConfig() {
|
||||
Map result = {};
|
||||
result['title'] = 'Home';
|
||||
result['views'] = [
|
||||
{
|
||||
'icon': 'mdi:home',
|
||||
'badges': HomeAssistant().entities.getByDomains(
|
||||
includeDomains: ['sensor', 'binary_sensor', 'device_tracker', 'person', 'sun']
|
||||
).map(
|
||||
(en) => en.entityId
|
||||
).toList(),
|
||||
'cards': [{
|
||||
'type': 'entities',
|
||||
'entities': HomeAssistant().entities.getByDomains(
|
||||
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
|
||||
).map(
|
||||
(en) => en.entityId
|
||||
).toList()
|
||||
}]
|
||||
}
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget build(BuildContext context, TabController tabController) {
|
||||
|
@ -35,4 +35,28 @@ class Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
static void launchAuthenticatedWebView({BuildContext context, String url, String title}) {
|
||||
if (url.contains("?")) {
|
||||
url += "&external_auth=1";
|
||||
} else {
|
||||
url += "?external_auth=1";
|
||||
}
|
||||
final flutterWebViewPlugin = new standaloneWebview.FlutterWebviewPlugin();
|
||||
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
|
||||
if (viewState.type == standaloneWebview.WebViewState.startLoad) {
|
||||
Logger.d("[WebView] Injecting external auth JS");
|
||||
rootBundle.loadString('assets/js/externalAuth.js').then((js){
|
||||
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
|
||||
});
|
||||
}
|
||||
});
|
||||
Navigator.of(context).pushNamed(
|
||||
"/webview",
|
||||
arguments: {
|
||||
"url": "$url",
|
||||
"title": "${title ?? ''}"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -4,77 +4,171 @@ class HAView {
|
||||
List<HACard> cards = [];
|
||||
List<Entity> badges = [];
|
||||
Entity linkedEntity;
|
||||
final String name;
|
||||
final String id;
|
||||
final String iconName;
|
||||
String name;
|
||||
String id;
|
||||
String iconName;
|
||||
final int count;
|
||||
final bool panel;
|
||||
bool isPanel;
|
||||
|
||||
HAView({
|
||||
this.name,
|
||||
this.id,
|
||||
this.count,
|
||||
this.iconName,
|
||||
this.panel: false,
|
||||
List<Entity> childEntities
|
||||
}) {
|
||||
if (childEntities != null) {
|
||||
_fillView(childEntities);
|
||||
}
|
||||
HAView({@required this.count, @required rawData}) {
|
||||
id = "${rawData['id']}";
|
||||
name = rawData['title'];
|
||||
iconName = rawData['icon'];
|
||||
isPanel = rawData['panel'] ?? false;
|
||||
|
||||
if (rawData['badges'] != null && rawData['badges'] is List) {
|
||||
rawData['badges'].forEach((entity) {
|
||||
if (entity is String) {
|
||||
if (HomeAssistant().entities.isExist(entity)) {
|
||||
Entity e = HomeAssistant().entities.get(entity);
|
||||
badges.add(e);
|
||||
}
|
||||
} else {
|
||||
String eId = '${entity['entity']}';
|
||||
if (HomeAssistant().entities.isExist(eId)) {
|
||||
Entity e = HomeAssistant().entities.get(eId);
|
||||
badges.add(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cards.addAll(_createLovelaceCards(rawData["cards"] ?? []));
|
||||
}
|
||||
|
||||
void _fillView(List<Entity> childEntities) {
|
||||
List<HACard> autoGeneratedCards = [];
|
||||
badges.addAll(childEntities.where((entity){ return entity.isBadge;}));
|
||||
childEntities.where((entity){ return entity.domain == "media_player";}).forEach((e){
|
||||
HACard card = HACard(
|
||||
name: e.displayName,
|
||||
id: e.entityId,
|
||||
linkedEntityWrapper: EntityWrapper(entity: e),
|
||||
type: CardType.MEDIA_CONTROL
|
||||
);
|
||||
cards.add(card);
|
||||
});
|
||||
childEntities.where((e){return (!e.isBadge && e.domain != "media_player");}).forEach((entity) {
|
||||
if (!entity.isGroup) {
|
||||
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
|
||||
if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) {
|
||||
HACard card = HACard(
|
||||
id: groupIdToAdd,
|
||||
name: entity.domain,
|
||||
type: CardType.ENTITIES
|
||||
);
|
||||
card.entities.add(EntityWrapper(entity: entity));
|
||||
autoGeneratedCards.add(card);
|
||||
} else {
|
||||
autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(EntityWrapper(entity: entity));
|
||||
}
|
||||
} else {
|
||||
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(
|
||||
name: entity.displayName,
|
||||
id: entity.entityId,
|
||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
||||
type: CardType.ENTITIES
|
||||
id: "card",
|
||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
|
||||
showHeaderToggle: (rawCardInfo['show_header_toggle'] ?? rawCard['show_header_toggle']) ?? true,
|
||||
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
|
||||
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
|
||||
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],
|
||||
states: rawCardInfo['states'],
|
||||
conditions: rawCard['conditions'] ?? [],
|
||||
content: rawCardInfo['content'],
|
||||
min: rawCardInfo['min'] ?? 0,
|
||||
max: rawCardInfo['max'] ?? 100,
|
||||
unit: rawCardInfo['unit'],
|
||||
severity: rawCardInfo['severity']
|
||||
);
|
||||
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
|
||||
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
|
||||
HACard mediaCard = HACard(
|
||||
name: entity.displayName,
|
||||
id: entity.entityId,
|
||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
||||
type: CardType.MEDIA_CONTROL
|
||||
);
|
||||
cards.add(mediaCard);
|
||||
if (rawCardInfo["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||
}
|
||||
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
|
||||
rawEntities?.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
card.entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||
} else {
|
||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (rawEntity["type"] == "divider") {
|
||||
card.entities.add(EntityWrapper(entity: Entity.divider()));
|
||||
} else if (rawEntity["type"] == "section") {
|
||||
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
||||
} else if (rawEntity["type"] == "call-service") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.callService,
|
||||
"service": rawEntity["service"],
|
||||
"service_data": rawEntity["service_data"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
card.entities.add(EntityWrapper(
|
||||
entity: Entity.callService(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
service: rawEntity["service"],
|
||||
actionName: rawEntity["action_name"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (rawEntity["type"] == "weblink") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.navigate,
|
||||
"service": rawEntity["url"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
card.entities.add(EntityWrapper(
|
||||
entity: Entity.weblink(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"],
|
||||
url: rawEntity["url"]
|
||||
),
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
card.entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
overrideName: rawEntity["name"],
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
cards.add(card);
|
||||
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
|
||||
if (rawSingleEntity != null) {
|
||||
var en = rawSingleEntity;
|
||||
if (en is String) {
|
||||
if (HomeAssistant().entities.isExist(en)) {
|
||||
Entity e = HomeAssistant().entities.get(en);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
overrideIcon: rawCardInfo["icon"],
|
||||
overrideName: rawCardInfo["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
|
||||
}
|
||||
} else {
|
||||
if (HomeAssistant().entities.isExist(en["entity"])) {
|
||||
Entity e = HomeAssistant().entities.get(en["entity"]);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
overrideIcon: en["icon"],
|
||||
overrideName: en["name"],
|
||||
stateFilter: en['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
result.add(card);
|
||||
} catch (e) {
|
||||
Logger.e("There was an error parsing card: ${e.toString()}");
|
||||
}
|
||||
});
|
||||
cards.addAll(autoGeneratedCards);
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget buildTab() {
|
||||
if (linkedEntity == null) {
|
||||
if (iconName != null) {
|
||||
if (iconName != null && iconName.isNotEmpty) {
|
||||
return
|
||||
Tab(
|
||||
icon:
|
||||
|
@ -10,22 +10,28 @@ class ViewWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (this.view.panel) {
|
||||
if (this.view.isPanel) {
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
heightFactor: 1,
|
||||
child: _buildPanelChild(context),
|
||||
);
|
||||
} else {
|
||||
Widget cardsContainer;
|
||||
if (this.view.cards.isNotEmpty) {
|
||||
cardsContainer = DynamicMultiColumnLayout(
|
||||
minColumnWidth: Sizes.minViewColumnWidth,
|
||||
children: this.view.cards.map((card) => card.build(context)).toList(),
|
||||
);
|
||||
} else {
|
||||
cardsContainer = Container();
|
||||
}
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.all(0),
|
||||
children: <Widget>[
|
||||
_buildBadges(context),
|
||||
DynamicMultiColumnLayout(
|
||||
minColumnWidth: Sizes.minViewColumnWidth,
|
||||
children: this.view.cards.map((card) => card.build(context)).toList(),
|
||||
)
|
||||
cardsContainer
|
||||
]
|
||||
);
|
||||
}
|
||||
|
46
pubspec.yaml
46
pubspec.yaml
@ -1,38 +1,39 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 0.7.6+760
|
||||
version: 0.8.0+885
|
||||
|
||||
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||
sdk: ">=2.2.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
web_socket_channel: any
|
||||
shared_preferences: any
|
||||
progress_indicators: any
|
||||
event_bus: any
|
||||
cached_network_image: any
|
||||
url_launcher: any
|
||||
date_format: any
|
||||
web_socket_channel: ^1.1.0
|
||||
shared_preferences: ^0.5.6+1
|
||||
progress_indicators: ^0.1.4
|
||||
path_provider: ^1.6.5
|
||||
event_bus: ^1.1.1
|
||||
cached_network_image: ^2.0.0
|
||||
url_launcher: ^5.4.1
|
||||
date_format: ^1.0.8
|
||||
charts_flutter: ^0.8.1
|
||||
flutter_markdown: 0.3.0
|
||||
in_app_purchase: ^0.2.1+4
|
||||
flutter_markdown: ^0.3.3
|
||||
in_app_purchase: ^0.3.0+3
|
||||
flutter_custom_tabs: ^0.6.0
|
||||
firebase_messaging: ^5.1.6
|
||||
uni_links: ^0.2.0
|
||||
flutter_webview_plugin: ^0.3.10+1
|
||||
webview_flutter: ^0.3.19+7
|
||||
firebase_messaging: ^6.0.9
|
||||
flutter_secure_storage: ^3.3.1+1
|
||||
device_info: ^0.4.0+3
|
||||
flutter_local_notifications: ^0.8.4
|
||||
geolocator: ^5.1.5
|
||||
workmanager: ^0.1.5
|
||||
battery: ^0.3.1+1
|
||||
sentry: ^2.3.1
|
||||
share:
|
||||
git:
|
||||
url: https://github.com/d-silveira/flutter-share.git
|
||||
device_info: ^0.4.1+4
|
||||
flutter_local_notifications: ^1.1.6
|
||||
geolocator: ^5.3.1
|
||||
workmanager: ^0.2.2
|
||||
battery: ^0.3.1+7
|
||||
firebase_crashlytics: ^0.1.3+3
|
||||
video_player: ^0.10.7
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -51,6 +52,7 @@ flutter:
|
||||
assets:
|
||||
- images/hassio-192x192.png
|
||||
- assets/js/externalAuth.js
|
||||
- assets/html/cameraView.html
|
||||
|
||||
fonts:
|
||||
- family: "Material Design Icons"
|
||||
|
Reference in New Issue
Block a user