Compare commits
3 Commits
rc/1.3.0-b
...
0.5.4
Author | SHA1 | Date | |
---|---|---|---|
5211d1ff46 | |||
d1c9fddba6 | |||
14cc55a2c7 |
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help improve HA Client
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**HA Client version:** [Main menu -> About HA Client]
|
|
||||||
|
|
||||||
**Home Assistant version:**
|
|
||||||
|
|
||||||
**Device name:**
|
|
||||||
|
|
||||||
**Android version:**
|
|
||||||
|
|
||||||
**Description**
|
|
||||||
[Replace with description]
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
[Replace with screenshots]
|
|
12
.github/ISSUE_TEMPLATE/entity-support-request.md
vendored
@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
name: Entity support request
|
|
||||||
about: Suggest to add support of any entity type
|
|
||||||
title: ''
|
|
||||||
labels: ENTITY, feature/improvement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Entity type:**
|
|
||||||
|
|
||||||
**Link to documentation:**
|
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for HA Client if it is not a card or entity support
|
|
||||||
title: ''
|
|
||||||
labels: feature/improvement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
name: Lovelace Card support request
|
|
||||||
about: Suggest to add any Lovelace card support
|
|
||||||
title: ''
|
|
||||||
labels: CARD, feature/improvement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Card name:**
|
|
||||||
|
|
||||||
**Link to card repository or web page:**
|
|
11
.github/no-response.yml
vendored
@ -1,11 +0,0 @@
|
|||||||
# Configuration for probot-no-response - https://github.com/probot/no-response
|
|
||||||
|
|
||||||
# Number of days of inactivity before an Issue is closed for lack of response
|
|
||||||
daysUntilClose: 14
|
|
||||||
# Label requiring a response
|
|
||||||
responseRequiredLabel: more info needed
|
|
||||||
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
|
|
||||||
closeComment: >
|
|
||||||
This issue has been automatically closed because there has been no response
|
|
||||||
to our request for more information from the original author. If the issue still relevant
|
|
||||||
feel free to reopen this issue and add more information, or report a new one.
|
|
10
.gitignore
vendored
@ -9,15 +9,5 @@ build/
|
|||||||
.flutter-plugins
|
.flutter-plugins
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
|
||||||
.theia/
|
|
||||||
.project/
|
|
||||||
.settings/
|
|
||||||
|
|
||||||
flutter_export_environment.sh
|
|
||||||
.flutter-plugins-dependencies
|
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
.secrets.dart
|
|
||||||
pubspec.lock
|
|
||||||
google-services.json
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
FROM gitpod/workspace-full:latest
|
|
||||||
|
|
||||||
ENV ANDROID_HOME=/workspace/android-sdk \
|
|
||||||
FLUTTER_ROOT=/workspace/flutter \
|
|
||||||
FLUTTER_HOME=/workspace/flutter
|
|
||||||
|
|
||||||
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
|
|
||||||
&& sdk install java 8.0.242.j9-adpt"
|
|
26
.gitpod.yml
@ -1,26 +0,0 @@
|
|||||||
image:
|
|
||||||
file: .gitpod.dockerfile
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- before: |
|
|
||||||
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
|
||||||
mkdir -p /home/gitpod/.android
|
|
||||||
touch /home/gitpod/.android/repositories.cfg
|
|
||||||
init: |
|
|
||||||
echo "Installing Flutter SDK..."
|
|
||||||
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.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"
|
|
||||||
echo "Init Flutter..."
|
|
||||||
cd /workspace/ha_client
|
|
||||||
flutter upgrade
|
|
||||||
flutter doctor --android-licenses
|
|
||||||
flutter pub get
|
|
||||||
command: |
|
|
||||||
echo "Ready to go!"
|
|
||||||
flutter doctor
|
|
||||||
vscode:
|
|
||||||
extensions:
|
|
||||||
- Dart-Code.dart-code@3.5.0-beta.1:Wg2nTABftVR/Dry4tqeY1w==
|
|
||||||
- Dart-Code.flutter@3.5.0:/kOacEWdiDRLyN/idUiM4A==
|
|
18
README.md
@ -1,18 +1,12 @@
|
|||||||
|
[](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart)
|
||||||
# HA Client
|
# HA Client
|
||||||
## Native Android client for Home Assistant
|
## Native Android client for Home Assistant
|
||||||
### With actionable notifications, location tracking and Lovelace UI support
|
### With Lovelace UI support
|
||||||
|
|
||||||
Visit [ha-client.app](http://ha-client.app/) for more info.
|
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
||||||
|
|
||||||
Download the app from [Google Play](https://play.google.com/store/apps/details?id=com.keyboardcrumbs.haclient)
|
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
|
||||||
|
|
||||||
Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
|
||||||
|
|
||||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)
|
||||||
|
|
||||||
#### Last release build status
|
|
||||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
|
||||||
|
|
||||||
#### Projects used
|
|
||||||
- [HANotify](https://github.com/Crewski/HANotify) by [Crewski](https://github.com/Crewski)
|
|
||||||
- [hassalarm](https://github.com/Johboh/hassalarm) by [Johboh](https://github.com/Johboh) distributed under [MIT License](https://github.com/Johboh/hassalarm/blob/master/LICENSE)
|
|
||||||
|
1
android/.gitignore
vendored
@ -8,4 +8,3 @@
|
|||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
GeneratedPluginRegistrant.java
|
GeneratedPluginRegistrant.java
|
||||||
.project/
|
|
@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<projectDescription>
|
|
||||||
<name>android</name>
|
|
||||||
<comment>Project android created by Buildship.</comment>
|
|
||||||
<projects>
|
|
||||||
</projects>
|
|
||||||
<buildSpec>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
</buildSpec>
|
|
||||||
<natures>
|
|
||||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
|
||||||
</natures>
|
|
||||||
</projectDescription>
|
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<classpath>
|
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
|
|
||||||
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
|
||||||
<classpathentry kind="output" path="bin/default"/>
|
|
||||||
</classpath>
|
|
@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<projectDescription>
|
|
||||||
<name>app</name>
|
|
||||||
<comment>Project app created by Buildship.</comment>
|
|
||||||
<projects>
|
|
||||||
</projects>
|
|
||||||
<buildSpec>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
</buildSpec>
|
|
||||||
<natures>
|
|
||||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
|
||||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
|
||||||
</natures>
|
|
||||||
</projectDescription>
|
|
@ -50,14 +50,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
if (!System.getenv()["CI"]) {
|
|
||||||
debug {
|
|
||||||
keyAlias keystoreProperties['debugKeyAlias']
|
|
||||||
keyPassword keystoreProperties['debugKeyPassword']
|
|
||||||
storeFile file(keystoreProperties['debugStoreFile'])
|
|
||||||
storePassword keystoreProperties['debugStorePassword']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
release {
|
release {
|
||||||
keyAlias keystoreProperties['keyAlias']
|
keyAlias keystoreProperties['keyAlias']
|
||||||
keyPassword keystoreProperties['keyPassword']
|
keyPassword keystoreProperties['keyPassword']
|
||||||
@ -78,15 +70,7 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
|
||||||
implementation 'com.google.firebase:firebase-messaging:20.2.0'
|
|
||||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
|
||||||
implementation 'androidx.work:work-runtime:2.3.4'
|
|
||||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'io.fabric'
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.keyboardcrumbs.hassclient"
|
package="com.keyboardcrumbs.hassclient">
|
||||||
android:installLocation="auto">
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen"
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
android:required="false" />
|
flutter needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
@ -17,19 +13,9 @@
|
|||||||
additional functionality it is fine to subclass or reimplement
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<application
|
<application
|
||||||
|
android:name="io.flutter.app.FlutterApplication"
|
||||||
android:label="HA Client"
|
android:label="HA Client"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher">
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:usesCleartextTraffic="true">
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="flutterEmbedding"
|
|
||||||
android:value="2" />
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
|
||||||
android:value="ha_notify" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
@ -37,64 +23,17 @@
|
|||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- This keeps the window background of the activity showing
|
||||||
|
until Flutter renders its first frame. It can be removed if
|
||||||
|
there is no splash screen (such as the default splash screen
|
||||||
|
defined in @style/LaunchTheme). -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||||
android:resource="@drawable/launch_background" />
|
android:value="true" />
|
||||||
<meta-data
|
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
|
||||||
android:resource="@style/NormalTheme" />
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".MessagingService"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
<service
|
|
||||||
android:name=".LocationUpdatesService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false" />
|
|
||||||
<service
|
|
||||||
android:name=".LocationRequestService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false" />
|
|
||||||
<receiver android:name=".NotificationActionReceiver" android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
|
||||||
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver android:name=".NextAlarmBroadcastReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver android:name=".RestartLocationUpdate">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<service
|
|
||||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
|
||||||
android:exported="false"/>
|
|
||||||
<receiver
|
|
||||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
|
|
||||||
android:exported="false"/>
|
|
||||||
<receiver
|
|
||||||
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
|
||||||
android:enabled="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.location.Location;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.HandlerThread;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.work.Constraints;
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import androidx.work.NetworkType;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
|
|
||||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
|
||||||
import com.google.android.gms.location.LocationCallback;
|
|
||||||
import com.google.android.gms.location.LocationRequest;
|
|
||||||
import com.google.android.gms.location.LocationResult;
|
|
||||||
import com.google.android.gms.location.LocationServices;
|
|
||||||
|
|
||||||
public class LocationRequestService extends Service {
|
|
||||||
|
|
||||||
private static final String TAG = LocationRequestService.class.getSimpleName();
|
|
||||||
|
|
||||||
private NotificationManager mNotificationManager;
|
|
||||||
|
|
||||||
private LocationRequest mLocationRequest;
|
|
||||||
|
|
||||||
private FusedLocationProviderClient mFusedLocationClient;
|
|
||||||
|
|
||||||
private LocationCallback mLocationCallback;
|
|
||||||
|
|
||||||
private Handler mServiceHandler;
|
|
||||||
|
|
||||||
public LocationRequestService() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
|
||||||
|
|
||||||
mLocationCallback = new LocationCallback() {
|
|
||||||
@Override
|
|
||||||
public void onLocationResult(LocationResult locationResult) {
|
|
||||||
super.onLocationResult(locationResult);
|
|
||||||
onNewLocation(locationResult.getLastLocation());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mLocationRequest = new LocationRequest();
|
|
||||||
|
|
||||||
HandlerThread handlerThread = new HandlerThread(TAG);
|
|
||||||
handlerThread.start();
|
|
||||||
mServiceHandler = new Handler(handlerThread.getLooper());
|
|
||||||
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
CharSequence name = "Location requests";
|
|
||||||
NotificationChannel mChannel =
|
|
||||||
new NotificationChannel(LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
|
||||||
|
|
||||||
mNotificationManager.createNotificationChannel(mChannel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
Log.i(TAG, "Service started. startId="+startId);
|
|
||||||
|
|
||||||
requestLocationUpdates();
|
|
||||||
|
|
||||||
return START_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
try {
|
|
||||||
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
|
|
||||||
} catch (SecurityException unlikely) {
|
|
||||||
//When we lost permission
|
|
||||||
Log.i(TAG, "No location permission");
|
|
||||||
}
|
|
||||||
mServiceHandler.removeCallbacksAndMessages(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestLocationUpdates() {
|
|
||||||
Log.i(TAG, "Requesting location update in 5 seconds.");
|
|
||||||
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
|
|
||||||
mLocationRequest.setInterval(5000);
|
|
||||||
|
|
||||||
startForeground(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(this, null, LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID));
|
|
||||||
try {
|
|
||||||
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
|
|
||||||
mLocationCallback, Looper.myLooper());
|
|
||||||
} catch (SecurityException unlikely) {
|
|
||||||
stopSelf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onNewLocation(Location location) {
|
|
||||||
Log.i(TAG, "New location: " + location);
|
|
||||||
|
|
||||||
mNotificationManager.notify(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(
|
|
||||||
this,
|
|
||||||
location,
|
|
||||||
LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID
|
|
||||||
));
|
|
||||||
|
|
||||||
Constraints constraints = new Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Data locationData = new Data.Builder()
|
|
||||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
|
||||||
.putDouble("Lat", location.getLatitude())
|
|
||||||
.putDouble("Long", location.getLongitude())
|
|
||||||
.putFloat("Acc", location.getAccuracy())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
|
|
||||||
OneTimeWorkRequest uploadWorkRequest =
|
|
||||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.setInputData(locationData)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
WorkManager
|
|
||||||
.getInstance(getApplicationContext())
|
|
||||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
|
||||||
stopSelf();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.location.Location;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.HandlerThread;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.Looper;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.work.Constraints;
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import androidx.work.NetworkType;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
|
||||||
import com.google.android.gms.location.LocationCallback;
|
|
||||||
import com.google.android.gms.location.LocationRequest;
|
|
||||||
import com.google.android.gms.location.LocationResult;
|
|
||||||
import com.google.android.gms.location.LocationServices;
|
|
||||||
|
|
||||||
public class LocationUpdatesService extends Service {
|
|
||||||
|
|
||||||
private static final String TAG = LocationUpdatesService.class.getSimpleName();
|
|
||||||
|
|
||||||
private NotificationManager mNotificationManager;
|
|
||||||
|
|
||||||
private LocationRequest mLocationRequest;
|
|
||||||
|
|
||||||
private FusedLocationProviderClient mFusedLocationClient;
|
|
||||||
|
|
||||||
private LocationCallback mLocationCallback;
|
|
||||||
|
|
||||||
private Handler mServiceHandler;
|
|
||||||
|
|
||||||
public LocationUpdatesService() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
|
||||||
|
|
||||||
mLocationCallback = new LocationCallback() {
|
|
||||||
@Override
|
|
||||||
public void onLocationResult(LocationResult locationResult) {
|
|
||||||
super.onLocationResult(locationResult);
|
|
||||||
onNewLocation(locationResult.getLastLocation());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mLocationRequest = new LocationRequest();
|
|
||||||
|
|
||||||
HandlerThread handlerThread = new HandlerThread(TAG);
|
|
||||||
handlerThread.start();
|
|
||||||
mServiceHandler = new Handler(handlerThread.getLooper());
|
|
||||||
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
CharSequence name = "Location updates";
|
|
||||||
NotificationChannel mChannel =
|
|
||||||
new NotificationChannel(LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
|
||||||
|
|
||||||
mNotificationManager.createNotificationChannel(mChannel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
Log.i(TAG, "Service started. startId="+startId);
|
|
||||||
|
|
||||||
requestLocationUpdates();
|
|
||||||
|
|
||||||
return START_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
try {
|
|
||||||
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
|
|
||||||
} catch (SecurityException unlikely) {
|
|
||||||
//When we lost permission
|
|
||||||
Log.i(TAG, "No location permission");
|
|
||||||
}
|
|
||||||
mServiceHandler.removeCallbacksAndMessages(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestLocationUpdates() {
|
|
||||||
long requestInterval = LocationUtils.getLocationUpdateIntervals(getApplicationContext());
|
|
||||||
int priority;
|
|
||||||
if (requestInterval >= 600000) {
|
|
||||||
mLocationRequest.setFastestInterval(60000);
|
|
||||||
priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
|
|
||||||
} else {
|
|
||||||
priority = LocationRequest.PRIORITY_HIGH_ACCURACY;
|
|
||||||
}
|
|
||||||
Log.i(TAG, "Requesting location updates. Every " + requestInterval + "ms with priority of " + priority);
|
|
||||||
mLocationRequest.setPriority(priority);
|
|
||||||
mLocationRequest.setInterval(requestInterval);
|
|
||||||
|
|
||||||
startForeground(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(this, null, LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID));
|
|
||||||
try {
|
|
||||||
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
|
|
||||||
mLocationCallback, Looper.myLooper());
|
|
||||||
} catch (SecurityException unlikely) {
|
|
||||||
stopSelf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onNewLocation(Location location) {
|
|
||||||
Log.i(TAG, "New location: " + location);
|
|
||||||
|
|
||||||
mNotificationManager.notify(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(
|
|
||||||
this,
|
|
||||||
location,
|
|
||||||
LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID
|
|
||||||
));
|
|
||||||
|
|
||||||
Constraints constraints = new Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Data locationData = new Data.Builder()
|
|
||||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
|
||||||
.putDouble("Lat", location.getLatitude())
|
|
||||||
.putDouble("Long", location.getLongitude())
|
|
||||||
.putFloat("Acc", location.getAccuracy())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
|
|
||||||
OneTimeWorkRequest uploadWorkRequest =
|
|
||||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.setInputData(locationData)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
WorkManager
|
|
||||||
.getInstance(getApplicationContext())
|
|
||||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.location.Location;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Looper;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.concurrent.futures.CallbackToFutureAdapter;
|
|
||||||
import androidx.work.Constraints;
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import androidx.work.ListenableWorker;
|
|
||||||
import androidx.work.NetworkType;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
|
||||||
import com.google.android.gms.location.LocationCallback;
|
|
||||||
import com.google.android.gms.location.LocationRequest;
|
|
||||||
import com.google.android.gms.location.LocationResult;
|
|
||||||
import com.google.android.gms.location.LocationServices;
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class LocationUpdatesWorker extends ListenableWorker {
|
|
||||||
|
|
||||||
private Context currentContext;
|
|
||||||
private LocationCallback callback;
|
|
||||||
private FusedLocationProviderClient fusedLocationClient;
|
|
||||||
|
|
||||||
public LocationUpdatesWorker(Context context, WorkerParameters params) {
|
|
||||||
super(context, params);
|
|
||||||
currentContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void finish() {
|
|
||||||
fusedLocationClient.removeLocationUpdates(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ListenableFuture<Result> startWork() {
|
|
||||||
return CallbackToFutureAdapter.getFuture(completer -> {
|
|
||||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(currentContext);
|
|
||||||
|
|
||||||
callback = new LocationCallback() {
|
|
||||||
@Override
|
|
||||||
public void onLocationResult(LocationResult locationResult) {
|
|
||||||
super.onLocationResult(locationResult);
|
|
||||||
Location location = locationResult.getLastLocation();
|
|
||||||
Constraints constraints = new Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Data locationData = new Data.Builder()
|
|
||||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
|
||||||
.putDouble("Lat", location.getLatitude())
|
|
||||||
.putDouble("Long", location.getLongitude())
|
|
||||||
.putFloat("Acc", location.getAccuracy())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
|
|
||||||
OneTimeWorkRequest uploadWorkRequest =
|
|
||||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.setInputData(locationData)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
WorkManager
|
|
||||||
.getInstance(getApplicationContext())
|
|
||||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
|
||||||
if (LocationUtils.showNotification(currentContext)) {
|
|
||||||
NotificationManager notificationManager;
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
|
||||||
notificationManager = currentContext.getSystemService(NotificationManager.class);
|
|
||||||
} else {
|
|
||||||
notificationManager = (NotificationManager)currentContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
CharSequence name = "Location updates";
|
|
||||||
NotificationChannel mChannel =
|
|
||||||
new NotificationChannel(LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(mChannel);
|
|
||||||
}
|
|
||||||
notificationManager.notify(LocationUtils.WORKER_NOTIFICATION_ID, LocationUtils.getNotification(currentContext, location, LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID));
|
|
||||||
}
|
|
||||||
finish();
|
|
||||||
completer.set(Result.success());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
LocationRequest locationRequest = new LocationRequest();
|
|
||||||
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
|
|
||||||
locationRequest.setInterval(5000);
|
|
||||||
try {
|
|
||||||
fusedLocationClient.requestLocationUpdates(locationRequest,
|
|
||||||
callback, Looper.myLooper());
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
completer.setException(e);
|
|
||||||
}
|
|
||||||
return callback;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
|
||||||
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.location.Location;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.PeriodicWorkRequest;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
class LocationUtils {
|
|
||||||
|
|
||||||
static final String KEY_REQUESTING_LOCATION_UPDATES = "flutter.location-updates-state";
|
|
||||||
static final String KEY_LOCATION_UPDATE_INTERVAL = "flutter.location-updates-interval";
|
|
||||||
static final String KEY_LOCATION_SHOW_NOTIFICATION = "flutter.location-updates-show-notification";
|
|
||||||
|
|
||||||
static final String WORKER_NOTIFICATION_CHANNEL_ID = "location_worker";
|
|
||||||
static final int WORKER_NOTIFICATION_ID = 954322;
|
|
||||||
static final String SERVICE_NOTIFICATION_CHANNEL_ID = "location_service";
|
|
||||||
static final int SERVICE_NOTIFICATION_ID = 954311;
|
|
||||||
static final String ONETIME_NOTIFICATION_CHANNEL_ID = "location_request";
|
|
||||||
static final int ONETIME_NOTIFICATION_ID = 954333;
|
|
||||||
|
|
||||||
static final String REQUEST_LOCATION_NOTIFICATION = "request_location_update";
|
|
||||||
|
|
||||||
static final String LOCATION_WORK_NAME = "HALocationWorker";
|
|
||||||
static final String LOCATION_REQUEST_NAME = "HALocationRequest";
|
|
||||||
|
|
||||||
static final int LOCATION_UPDATES_DISABLED = 0;
|
|
||||||
static final int LOCATION_UPDATES_SERVICE = 1;
|
|
||||||
static final int LOCATION_UPDATES_WORKER = 2;
|
|
||||||
|
|
||||||
static final int DEFAULT_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
|
|
||||||
static final long MIN_WORKER_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
|
|
||||||
|
|
||||||
static int getLocationUpdatesState(Context context) {
|
|
||||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getInt(KEY_REQUESTING_LOCATION_UPDATES, LOCATION_UPDATES_DISABLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
static long getLocationUpdateIntervals(Context context) {
|
|
||||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_INTERVAL, DEFAULT_LOCATION_UPDATE_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean showNotification(Context context) {
|
|
||||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getBoolean(KEY_LOCATION_SHOW_NOTIFICATION, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setLocationUpdatesState(Context context, int locationUpdatesState) {
|
|
||||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putInt(KEY_REQUESTING_LOCATION_UPDATES, locationUpdatesState)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setLocationUpdatesSettings(Context context, long interval, boolean showNotification) {
|
|
||||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putBoolean(KEY_LOCATION_SHOW_NOTIFICATION, showNotification)
|
|
||||||
.putLong(KEY_LOCATION_UPDATE_INTERVAL, interval)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void startService(Context context) {
|
|
||||||
Intent myService = new Intent(context, LocationUpdatesService.class);
|
|
||||||
context.startService(myService);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void startServiceFromBroadcast(Context context) {
|
|
||||||
Intent serviceIntent = new Intent(context, LocationUpdatesService.class);
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
context.startForegroundService(serviceIntent);
|
|
||||||
} else {
|
|
||||||
context.startService(serviceIntent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void startWorker(Context context, long interval) {
|
|
||||||
PeriodicWorkRequest periodicWork = new PeriodicWorkRequest.Builder(LocationUpdatesWorker.class, interval, TimeUnit.MILLISECONDS)
|
|
||||||
.build();
|
|
||||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(LocationUtils.LOCATION_WORK_NAME, ExistingPeriodicWorkPolicy.REPLACE, periodicWork);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void requestLocationOnce(Context context) {
|
|
||||||
Intent myService = new Intent(context, LocationRequestService.class);
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
context.startForegroundService(myService);
|
|
||||||
} else {
|
|
||||||
context.startService(myService);
|
|
||||||
}
|
|
||||||
//OneTimeWorkRequest oneTimeWork = new OneTimeWorkRequest.Builder(LocationUpdatesWorker.class)
|
|
||||||
// .build();
|
|
||||||
//WorkManager.getInstance(context).enqueueUniqueWork(LocationUtils.LOCATION_REQUEST_NAME, ExistingWorkPolicy.REPLACE, oneTimeWork);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Notification getNotification(Context context, Location location, String channelId) {
|
|
||||||
CharSequence title = "Location tracking";
|
|
||||||
CharSequence text = location == null ? "Accuracy: unknown" : "Accuracy: " + location.getAccuracy() + " m";
|
|
||||||
CharSequence bigText = location == null ? "Waiting for location..." : "Time: " + DateFormat.getDateTimeInstance().format(new Date(location.getTime())) +
|
|
||||||
System.getProperty("line.separator") + "Accuracy: " + location.getAccuracy() + " m" +
|
|
||||||
System.getProperty("line.separator") + "Location: " + location.getLatitude() + ", " + location.getLongitude();
|
|
||||||
|
|
||||||
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
|
|
||||||
new Intent(context, MainActivity.class), 0);
|
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
|
||||||
.setContentIntent(activityPendingIntent)
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setContentText(text)
|
|
||||||
.setStyle(new NotificationCompat.BigTextStyle()
|
|
||||||
.bigText(bigText))
|
|
||||||
.setPriority(-1)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setSmallIcon(R.drawable.mini_icon_location)
|
|
||||||
.setWhen(System.currentTimeMillis());
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Notification getRequestNotification(Context context, Location location, String channelId) {
|
|
||||||
CharSequence title = "Updating location...";
|
|
||||||
CharSequence text = location == null ? "Waiting for location..." : "Accuracy: " + location.getAccuracy() + " m";
|
|
||||||
|
|
||||||
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
|
|
||||||
new Intent(context, MainActivity.class), 0);
|
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
|
||||||
.setContentIntent(activityPendingIntent)
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setContentText(text)
|
|
||||||
.setPriority(-1)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setSmallIcon(R.drawable.mini_icon_location)
|
|
||||||
.setWhen(System.currentTimeMillis());
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,184 +1,13 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import android.os.Bundle;
|
||||||
import androidx.core.app.ActivityCompat;
|
import io.flutter.app.FlutterActivity;
|
||||||
import androidx.work.WorkManager;
|
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity;
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine;
|
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.location.Location;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
|
||||||
|
|
||||||
import com.google.android.gms.common.GoogleApiAvailability;
|
|
||||||
import com.google.android.gms.common.ConnectionResult;
|
|
||||||
import com.google.firebase.iid.FirebaseInstanceId;
|
|
||||||
|
|
||||||
public class MainActivity extends FlutterActivity {
|
public class MainActivity extends FlutterActivity {
|
||||||
|
|
||||||
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
|
|
||||||
|
|
||||||
private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34;
|
|
||||||
|
|
||||||
private int locationUpdatesType = LocationUtils.LOCATION_UPDATES_DISABLED;
|
|
||||||
private long locationUpdatesInterval = LocationUtils.DEFAULT_LOCATION_UPDATE_INTERVAL_MS;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
|
||||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
|
||||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
|
|
||||||
(call, result) -> {
|
|
||||||
Context context = getActivity();
|
|
||||||
switch (call.method) {
|
|
||||||
case "getFCMToken":
|
|
||||||
try {
|
|
||||||
if (checkPlayServices()) {
|
|
||||||
FirebaseInstanceId.getInstance().getInstanceId()
|
|
||||||
.addOnCompleteListener(task -> {
|
|
||||||
if (task.isSuccessful()) {
|
|
||||||
String token = task.getResult().getToken();
|
|
||||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
|
|
||||||
result.success(token);
|
|
||||||
} else {
|
|
||||||
Exception ex = task.getException();
|
|
||||||
if (ex != null) {
|
|
||||||
result.error("fcm_error", ex.getMessage(), null);
|
|
||||||
} else {
|
|
||||||
result.error("fcm_error", "Unknown", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result.error("google_play_service_error", "Google Play Services unavailable", null);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
result.error("get_token_exception", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "startLocationService":
|
|
||||||
try {
|
|
||||||
locationUpdatesInterval = ((Number)call.argument("location-updates-interval")).longValue();
|
|
||||||
boolean useForegroundService = (boolean)call.argument("foreground-location-tracking");
|
|
||||||
|
|
||||||
if (useForegroundService) {
|
|
||||||
locationUpdatesType = LocationUtils.LOCATION_UPDATES_SERVICE;
|
|
||||||
} else {
|
|
||||||
locationUpdatesType = LocationUtils.LOCATION_UPDATES_WORKER;
|
|
||||||
}
|
|
||||||
LocationUtils.setLocationUpdatesSettings(this, locationUpdatesInterval, (boolean)call.argument("location-updates-show-notification"));
|
|
||||||
if (isNoLocationPermissions()) {
|
|
||||||
requestLocationPermissions();
|
|
||||||
} else {
|
|
||||||
startLocationUpdates();
|
|
||||||
}
|
|
||||||
result.success("");
|
|
||||||
} catch (Exception e) {
|
|
||||||
result.error("location_error", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "stopLocationService":
|
|
||||||
try {
|
|
||||||
stopLocationUpdates();
|
|
||||||
result.success("");
|
|
||||||
} catch (Exception e) {
|
|
||||||
result.error("location_error", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "cancelOldLocationWorker":
|
|
||||||
WorkManager.getInstance(this).cancelAllWorkByTag("haclocation");
|
|
||||||
result.success("");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkPlayServices() {
|
|
||||||
return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startLocationUpdates() {
|
|
||||||
if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_SERVICE) {
|
|
||||||
LocationUtils.startService(this);
|
|
||||||
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
|
|
||||||
} else if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_WORKER) {
|
|
||||||
LocationUtils.startWorker(this, locationUpdatesInterval);
|
|
||||||
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
|
|
||||||
} else {
|
|
||||||
stopLocationUpdates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopLocationUpdates() {
|
|
||||||
Intent myService = new Intent(MainActivity.this, LocationUpdatesService.class);
|
|
||||||
stopService(myService);
|
|
||||||
WorkManager.getInstance(this).cancelUniqueWork(LocationUtils.LOCATION_WORK_NAME);
|
|
||||||
NotificationManager notificationManager;
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
|
||||||
notificationManager = getSystemService(NotificationManager.class);
|
|
||||||
} else {
|
|
||||||
notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
}
|
|
||||||
notificationManager.cancel(LocationUtils.WORKER_NOTIFICATION_ID);
|
|
||||||
LocationUtils.setLocationUpdatesState(this, LocationUtils.LOCATION_UPDATES_DISABLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
}
|
GeneratedPluginRegistrant.registerWith(this);
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isNoLocationPermissions() {
|
|
||||||
return PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestLocationPermissions() {
|
|
||||||
ActivityCompat.requestPermissions(MainActivity.this,
|
|
||||||
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
|
||||||
REQUEST_PERMISSIONS_REQUEST_CODE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
|
||||||
@NonNull int[] grantResults) {
|
|
||||||
if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) {
|
|
||||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
startLocationUpdates();
|
|
||||||
} else {
|
|
||||||
stopLocationUpdates();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -1,165 +0,0 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.media.RingtoneManager;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
|
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
|
||||||
import com.google.firebase.messaging.RemoteMessage;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.webkit.URLUtil;
|
|
||||||
|
|
||||||
|
|
||||||
public class MessagingService extends FirebaseMessagingService {
|
|
||||||
|
|
||||||
private static final String TAG = MessagingService.class.getSimpleName();
|
|
||||||
|
|
||||||
public static final String NOTIFICATION_ACTION_BROADCAST = "com.keyboardcrumbs.hassclient.haNotificationAction";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
|
||||||
Map<String, String> data = remoteMessage.getData();
|
|
||||||
if (data.size() > 0) {
|
|
||||||
if (data.containsKey("body") || data.containsKey("title")) {
|
|
||||||
sendNotification(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNewToken(@NonNull String token) {
|
|
||||||
getApplicationContext().getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendNotification(Map<String, String> data) {
|
|
||||||
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
|
|
||||||
boolean autoCancel;
|
|
||||||
if (!data.containsKey("body")) {
|
|
||||||
messageBody = "";
|
|
||||||
} else {
|
|
||||||
messageBody = data.get("body");
|
|
||||||
}
|
|
||||||
if (messageBody != null && messageBody.equals(LocationUtils.REQUEST_LOCATION_NOTIFICATION)) {
|
|
||||||
Log.d(TAG, "Location update request received");
|
|
||||||
LocationUtils.requestLocationOnce(this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String customChannelId = data.get("channelId");
|
|
||||||
if (customChannelId == null) {
|
|
||||||
channelId = "ha_notify";
|
|
||||||
channelDescription = "Default notification channel";
|
|
||||||
} else {
|
|
||||||
channelId = customChannelId;
|
|
||||||
channelDescription = channelId;
|
|
||||||
}
|
|
||||||
if (!data.containsKey("title")) {
|
|
||||||
messageTitle = "HA Client";
|
|
||||||
} else {
|
|
||||||
messageTitle = data.get("title");
|
|
||||||
}
|
|
||||||
if (!data.containsKey("tag")) {
|
|
||||||
nTag = String.valueOf(System.currentTimeMillis());
|
|
||||||
} else {
|
|
||||||
nTag = data.get("tag");
|
|
||||||
}
|
|
||||||
if (data.containsKey("dismiss")) {
|
|
||||||
try {
|
|
||||||
boolean dismiss = Boolean.parseBoolean(data.get("dismiss"));
|
|
||||||
if (dismiss) {
|
|
||||||
NotificationManager notificationManager =
|
|
||||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
notificationManager.cancel(nTag, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
//nope
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.containsKey("autoDismiss")) {
|
|
||||||
try {
|
|
||||||
autoCancel = Boolean.parseBoolean(data.get("autoDismiss"));
|
|
||||||
} catch (Exception e) {
|
|
||||||
autoCancel = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
autoCancel = true;
|
|
||||||
}
|
|
||||||
imageUrl = data.get("image");
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
|
|
||||||
PendingIntent.FLAG_ONE_SHOT);
|
|
||||||
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
|
||||||
NotificationCompat.Builder notificationBuilder =
|
|
||||||
new NotificationCompat.Builder(this, channelId)
|
|
||||||
.setSmallIcon(R.drawable.mini_icon)
|
|
||||||
.setContentTitle(messageTitle)
|
|
||||||
.setContentText(messageBody)
|
|
||||||
.setAutoCancel(autoCancel)
|
|
||||||
.setSound(defaultSoundUri)
|
|
||||||
.setContentIntent(pendingIntent);
|
|
||||||
Bitmap image = null;
|
|
||||||
if (URLUtil.isValidUrl(imageUrl)) {
|
|
||||||
image = getBitmapFromURL(imageUrl);
|
|
||||||
}
|
|
||||||
if (image != null) {
|
|
||||||
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
|
|
||||||
notificationBuilder.setLargeIcon(image);
|
|
||||||
} else {
|
|
||||||
notificationBuilder.setStyle(new NotificationCompat.BigTextStyle()
|
|
||||||
.bigText(messageBody));
|
|
||||||
}
|
|
||||||
for (int i = 1; i <= 3; i++) {
|
|
||||||
if (data.containsKey("action" + i)) {
|
|
||||||
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class).setAction(NOTIFICATION_ACTION_BROADCAST);
|
|
||||||
if (autoCancel) {
|
|
||||||
broadcastIntent.putExtra("tag", nTag);
|
|
||||||
}
|
|
||||||
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
|
|
||||||
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
|
||||||
notificationBuilder.addAction(R.drawable.blank_icon, data.get("action" + i), actionIntent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NotificationManager notificationManager =
|
|
||||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
|
|
||||||
// Since android Oreo notification channel is needed.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
NotificationChannel channel = new NotificationChannel(channelId,
|
|
||||||
channelDescription,
|
|
||||||
NotificationManager.IMPORTANCE_HIGH);
|
|
||||||
notificationManager.createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(nTag, 0 /* ID of notification */, notificationBuilder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap getBitmapFromURL(String imageUrl) {
|
|
||||||
try {
|
|
||||||
URL url = new URL(imageUrl);
|
|
||||||
URLConnection connection = url.openConnection();
|
|
||||||
connection.setDoInput(true);
|
|
||||||
connection.connect();
|
|
||||||
InputStream input = connection.getInputStream();
|
|
||||||
return BitmapFactory.decodeStream(input);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import androidx.work.BackoffPolicy;
|
|
||||||
import androidx.work.Constraints;
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import androidx.work.NetworkType;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
|
|
||||||
public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (intent == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final boolean isBootIntent = Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction());
|
|
||||||
final boolean isNextAlarmIntent = AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equalsIgnoreCase(intent.getAction());
|
|
||||||
if (!isBootIntent && !isNextAlarmIntent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Constraints constraints = new Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Data workerData = new Data.Builder()
|
|
||||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NEXT_ALARM)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
OneTimeWorkRequest uploadWorkRequest =
|
|
||||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
|
||||||
.setBackoffCriteria(
|
|
||||||
BackoffPolicy.EXPONENTIAL,
|
|
||||||
10,
|
|
||||||
TimeUnit.SECONDS)
|
|
||||||
.setInputData(workerData)
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
WorkManager
|
|
||||||
.getInstance(context)
|
|
||||||
.enqueueUniqueWork("NextAlarmUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
|
|
||||||
import androidx.work.BackoffPolicy;
|
|
||||||
import androidx.work.Constraints;
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import androidx.work.NetworkType;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class NotificationActionReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (intent == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String intentAction = intent.getAction();
|
|
||||||
if (intentAction == null || !intentAction.equalsIgnoreCase(MessagingService.NOTIFICATION_ACTION_BROADCAST)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String rawActionData = intent.getStringExtra("actionData");
|
|
||||||
if (intent.hasExtra("tag")) {
|
|
||||||
String notificationTag = intent.getStringExtra("tag");
|
|
||||||
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
notificationManager.cancel(notificationTag, 0);
|
|
||||||
}
|
|
||||||
Constraints constraints = new Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build();
|
|
||||||
Data workerData = new Data.Builder()
|
|
||||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NOTIFICATION_ACTION)
|
|
||||||
.putString("rawActionData", rawActionData)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
OneTimeWorkRequest uploadWorkRequest =
|
|
||||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
|
||||||
.setBackoffCriteria(
|
|
||||||
BackoffPolicy.EXPONENTIAL,
|
|
||||||
10,
|
|
||||||
TimeUnit.SECONDS)
|
|
||||||
.setInputData(workerData)
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
WorkManager
|
|
||||||
.getInstance(context)
|
|
||||||
.enqueueUniqueWork("NotificationAction", ExistingWorkPolicy.APPEND, uploadWorkRequest);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
public class RestartLocationUpdate extends BroadcastReceiver {
|
|
||||||
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (LocationUtils.getLocationUpdatesState(context) == LocationUtils.LOCATION_UPDATES_SERVICE &&
|
|
||||||
(Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction()) || Intent.ACTION_MY_PACKAGE_REPLACED.equalsIgnoreCase(intent.getAction()))) {
|
|
||||||
LocationUtils.startServiceFromBroadcast(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,220 +0,0 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.BatteryManager;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.webkit.URLUtil;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.work.Worker;
|
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class SendDataHomeWorker extends Worker {
|
|
||||||
public static final String DATA_TYPE_KEY = "dataType";
|
|
||||||
|
|
||||||
public static final int DATA_TYPE_LOCATION = 1;
|
|
||||||
public static final int DATA_TYPE_NEXT_ALARM = 2;
|
|
||||||
public static final int DATA_TYPE_NOTIFICATION_ACTION = 3;
|
|
||||||
|
|
||||||
private Context currentContext;
|
|
||||||
private static final String TAG = "SendDataHomeWorker";
|
|
||||||
|
|
||||||
public static final String KEY_LAT_ARG = "Lat";
|
|
||||||
public static final String KEY_LONG_ARG = "Long";
|
|
||||||
public static final String KEY_ACC_ARG = "Acc";
|
|
||||||
|
|
||||||
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
|
|
||||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
|
||||||
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
|
|
||||||
|
|
||||||
public SendDataHomeWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
|
||||||
super(context, workerParams);
|
|
||||||
currentContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Result doWork() {
|
|
||||||
Log.d(TAG, "Start sending data home");
|
|
||||||
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
|
||||||
String webhookId = prefs.getString("flutter.app-webhook-id", null);
|
|
||||||
if (webhookId != null) {
|
|
||||||
try {
|
|
||||||
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
|
|
||||||
"://" +
|
|
||||||
prefs.getString("flutter.hassio-domain", "") +
|
|
||||||
":" +
|
|
||||||
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
|
|
||||||
if (URLUtil.isValidUrl(requestUrl)) {
|
|
||||||
int dataType = getInputData().getInt(DATA_TYPE_KEY, 0);
|
|
||||||
String stringRequest;
|
|
||||||
if (dataType == DATA_TYPE_LOCATION) {
|
|
||||||
Log.d(TAG, "Location data");
|
|
||||||
stringRequest = getLocationDataToSend();
|
|
||||||
} else if (dataType == DATA_TYPE_NEXT_ALARM) {
|
|
||||||
Log.d(TAG, "Next alarm data");
|
|
||||||
stringRequest = getNextAlarmDataToSend();
|
|
||||||
} else if (dataType == DATA_TYPE_NOTIFICATION_ACTION) {
|
|
||||||
Log.d(TAG, "Notification action data");
|
|
||||||
stringRequest = getNotificationActionData();
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "doWork() unknown data type: " + dataType);
|
|
||||||
return Result.failure();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
URL url = new URL(requestUrl);
|
|
||||||
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
|
|
||||||
urlConnection.setRequestMethod("POST");
|
|
||||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
|
||||||
urlConnection.setDoOutput(true);
|
|
||||||
assert stringRequest != null;
|
|
||||||
byte[] outputBytes = stringRequest.getBytes(StandardCharsets.UTF_8);
|
|
||||||
OutputStream os = urlConnection.getOutputStream();
|
|
||||||
os.write(outputBytes);
|
|
||||||
|
|
||||||
int responseCode = urlConnection.getResponseCode();
|
|
||||||
urlConnection.disconnect();
|
|
||||||
if (responseCode >= 300) {
|
|
||||||
return Result.retry();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error sending data", e);
|
|
||||||
return Result.retry();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Invalid HA url");
|
|
||||||
return Result.failure();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error =(", e);
|
|
||||||
return Result.failure();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Webhook id not found");
|
|
||||||
return Result.failure();
|
|
||||||
}
|
|
||||||
return Result.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getLocationDataToSend() {
|
|
||||||
try {
|
|
||||||
JSONObject dataToSend = new JSONObject();
|
|
||||||
dataToSend.put("type", "update_location");
|
|
||||||
JSONObject dataObject = new JSONObject();
|
|
||||||
|
|
||||||
JSONArray gps = new JSONArray();
|
|
||||||
gps.put(0, getInputData().getDouble(KEY_LAT_ARG, 0));
|
|
||||||
gps.put(1, getInputData().getDouble(KEY_LONG_ARG, 0));
|
|
||||||
|
|
||||||
dataObject.put("gps", gps);
|
|
||||||
dataObject.put("gps_accuracy", getInputData().getFloat(KEY_ACC_ARG, 0));
|
|
||||||
|
|
||||||
BatteryManager bm;
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
|
||||||
bm = currentContext.getSystemService(BatteryManager.class);
|
|
||||||
} else {
|
|
||||||
bm = (BatteryManager)currentContext.getSystemService(Context.BATTERY_SERVICE);
|
|
||||||
}
|
|
||||||
int batLevel = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
|
||||||
|
|
||||||
dataObject.put("battery", batLevel);
|
|
||||||
|
|
||||||
dataToSend.put("data", dataObject);
|
|
||||||
return dataToSend.toString();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG,"getLocationDataToSend", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getNotificationActionData() {
|
|
||||||
try {
|
|
||||||
String rawActionData = getInputData().getString("rawActionData");
|
|
||||||
if (rawActionData == null || rawActionData.length() == 0) {
|
|
||||||
Log.e(TAG,"getNotificationActionData rawAction data is empty");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
JSONObject actionData = new JSONObject(rawActionData);
|
|
||||||
JSONObject dataToSend = new JSONObject();
|
|
||||||
JSONObject requestData = new JSONObject();
|
|
||||||
if (actionData.getString("action").equals("call-service")) {
|
|
||||||
dataToSend.put("type", "call_service");
|
|
||||||
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
|
|
||||||
requestData.put("service", actionData.getString("service").split("\\.")[1]);
|
|
||||||
if (actionData.has("service_data")) {
|
|
||||||
requestData.put("service_data", actionData.get("service_data"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dataToSend.put("type", "fire_event");
|
|
||||||
requestData.put("event_type", "ha_client_event");
|
|
||||||
JSONObject eventData = new JSONObject();
|
|
||||||
eventData.put("action", actionData.getString("action"));
|
|
||||||
requestData.put("event_data", eventData);
|
|
||||||
}
|
|
||||||
dataToSend.put("data", requestData);
|
|
||||||
return dataToSend.toString();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG,"getNotificationActionData", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getNextAlarmDataToSend() {
|
|
||||||
try {
|
|
||||||
final AlarmManager alarmManager;
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
|
||||||
alarmManager = currentContext.getSystemService(AlarmManager.class);
|
|
||||||
} else {
|
|
||||||
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
|
|
||||||
|
|
||||||
JSONObject dataToSend = new JSONObject();
|
|
||||||
dataToSend.put("type", "update_sensor_states");
|
|
||||||
JSONArray dataArray = new JSONArray();
|
|
||||||
JSONObject sensorData = new JSONObject();
|
|
||||||
JSONObject sensorAttrs = new JSONObject();
|
|
||||||
sensorData.put("unique_id", "next_alarm");
|
|
||||||
sensorData.put("type", "sensor");
|
|
||||||
final long triggerTimestamp;
|
|
||||||
if (alarmClockInfo != null) {
|
|
||||||
triggerTimestamp = alarmClockInfo.getTriggerTime();
|
|
||||||
final Calendar calendar = Calendar.getInstance();
|
|
||||||
calendar.setTimeInMillis(triggerTimestamp);
|
|
||||||
Date date = calendar.getTime();
|
|
||||||
sensorData.put("state", DATE_TIME_FORMAT.format(date));
|
|
||||||
sensorAttrs.put("date", DATE_FORMAT.format(date));
|
|
||||||
sensorAttrs.put("time", TIME_FORMAT.format(date));
|
|
||||||
sensorAttrs.put("timestamp", triggerTimestamp);
|
|
||||||
} else {
|
|
||||||
sensorData.put("state", "");
|
|
||||||
sensorAttrs.put("date", "");
|
|
||||||
sensorAttrs.put("time", "");
|
|
||||||
sensorAttrs.put("timestamp", 0);
|
|
||||||
}
|
|
||||||
sensorData.put("icon", "mdi:alarm");
|
|
||||||
sensorData.put("attributes", sensorAttrs);
|
|
||||||
dataArray.put(0, sensorData);
|
|
||||||
dataToSend.put("data", dataArray);
|
|
||||||
return dataToSend.toString();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG,"getNextAlarmDataToSend", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 461 B |
@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
|
|
||||||
<group>
|
|
||||||
<clip-path
|
|
||||||
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#709ac1"
|
|
||||||
android:fillAlpha="0"
|
|
||||||
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:fillAlpha="0.12"
|
|
||||||
android:pathData="M 70.506 38.389 L 108 72.466 L 108 108 L 77 108 L 35.066 72.466 L 38.373 63.769 L 36.268 50.216 L 43.335 44.523 L 51.841 34.578 L 63.096 42.478 L 68.586 42.478 L 70.506 38.389 Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:fillAlpha="0.12"
|
|
||||||
android:pathData="M 28.979 53.708 L 47.736 67.31 L 38.373 58.563 L 36.268 51.52 L 28.979 53.708 Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:pathData="M 77.131 54.24 L 72.878 54.24 L 72.878 72.415 L 56.339 72.415 L 56.339 64.85 L 62.931 58.511 L 64.609 58.784 C 67.349 58.784 69.57 56.649 69.57 54.013 C 69.57 51.378 67.349 49.242 64.609 49.242 C 61.868 49.242 59.647 51.378 59.647 54.013 L 59.883 55.626 L 56.339 59.079 L 56.339 46.63 C 57.898 45.812 58.938 44.244 58.938 42.427 C 58.938 39.792 56.717 37.656 53.976 37.656 C 51.236 37.656 49.015 39.792 49.015 42.427 C 49.015 44.244 50.054 45.812 51.614 46.63 L 51.614 59.079 L 48.07 55.626 L 48.306 54.013 C 48.306 51.378 46.084 49.242 43.344 49.242 C 40.604 49.242 38.383 51.378 38.383 54.013 C 38.383 56.648 40.604 58.784 43.344 58.784 L 45.022 58.511 L 51.614 64.85 L 51.614 72.415 L 35.075 72.415 L 35.075 54.24 L 30.94 54.24 C 29.948 54.24 28.979 54.24 28.979 53.763 C 29.003 53.263 29.995 52.309 31.011 51.332 L 51.614 31.522 C 52.393 30.772 53.197 30 53.976 30 C 54.756 30 55.559 30.772 56.339 31.522 L 65.79 40.609 L 65.79 38.338 L 70.515 38.338 L 70.515 45.153 L 77.084 51.469 C 78.029 52.377 78.997 53.309 79.021 53.786 C 79.021 54.24 78.076 54.24 77.131 54.24 Z M 43.344 51.969 C 43.908 51.969 44.449 52.184 44.848 52.567 C 45.247 52.951 45.471 53.471 45.471 54.013 C 45.471 54.555 45.247 55.076 44.848 55.459 C 44.449 55.842 43.908 56.058 43.344 56.058 C 42.78 56.058 42.239 55.842 41.841 55.459 C 41.442 55.076 41.218 54.555 41.218 54.013 C 41.218 53.471 41.442 52.951 41.841 52.567 C 42.239 52.184 42.78 51.969 43.344 51.969 Z M 64.609 51.969 C 65.79 51.969 66.735 52.877 66.735 54.013 C 66.735 55.149 65.79 56.058 64.609 56.058 C 64.045 56.058 63.504 55.842 63.105 55.459 C 62.706 55.076 62.482 54.555 62.482 54.013 C 62.482 53.471 62.706 52.951 63.105 52.567 C 63.504 52.184 64.045 51.969 64.609 51.969 Z M 53.976 40.382 C 55.158 40.382 56.103 41.291 56.103 42.427 C 56.103 43.563 55.158 44.472 53.976 44.472 C 52.795 44.472 51.85 43.563 51.85 42.427 C 51.85 41.291 52.795 40.382 53.976 40.382 Z" />
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@color/main_color" />
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
<!-- You can insert your own image assets here -->
|
||||||
<!-- <item>
|
<!-- <item>
|
||||||
|
Before Width: | Height: | Size: 612 B |
Before Width: | Height: | Size: 571 B |
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/main_color"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/main_color"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 20 KiB |
@ -1,12 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="main_color">#709AC1</color>
|
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
Flutter draws its first frame -->
|
Flutter draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -2,15 +2,10 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
maven {
|
|
||||||
url 'https://maven.fabric.io/public'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
classpath 'com.android.tools.build:gradle:3.1.2'
|
||||||
classpath 'com.google.gms:google-services:4.3.3'
|
|
||||||
classpath 'io.fabric.tools:gradle:1.26.1'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,9 +13,6 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
maven {
|
|
||||||
url 'https://maven.fabric.io/public'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
org.gradle.jvmargs=-Xmx512m
|
org.gradle.jvmargs=-Xmx2g
|
||||||
|
org.gradle.daemon=true
|
||||||
|
org.gradle.caching=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=false
|
||||||
android.enableR8=true
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||||
|
@ -1 +0,0 @@
|
|||||||
include ':app'
|
|
@ -1,61 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
widows: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
var messageChannel = '{{message_channel}}';
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<video id="screen" width="100%" controls></video>
|
|
||||||
<script>
|
|
||||||
if (Hls.isSupported()) {
|
|
||||||
var video = document.getElementById('screen');
|
|
||||||
var hls = new Hls();
|
|
||||||
hls.on(Hls.Events.ERROR, function (event, data) {
|
|
||||||
if (data.fatal) {
|
|
||||||
switch(data.type) {
|
|
||||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
||||||
// try to recover network error
|
|
||||||
console.log("fatal network error encountered, try to recover");
|
|
||||||
hls.startLoad();
|
|
||||||
break;
|
|
||||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
||||||
console.log("fatal media error encountered, try to recover");
|
|
||||||
hls.recoverMediaError();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// cannot recover
|
|
||||||
hls.destroy();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// bind them together
|
|
||||||
hls.attachMedia(video);
|
|
||||||
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
|
|
||||||
console.log("video and hls.js are now bound together !");
|
|
||||||
hls.loadSource("{{stream_url}}");
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
|
|
||||||
console.log("manifest loaded, found " + data.levels.length + " quality level");
|
|
||||||
video.play();
|
|
||||||
video.onloadedmetadata = function() {
|
|
||||||
window[messageChannel].postMessage(document.body.clientWidth / video.offsetHeight);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,28 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
widows: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
var messageChannel = '{{message_channel}}';
|
|
||||||
window.onload = function() {
|
|
||||||
var img = document.getElementById('screen');
|
|
||||||
if (img) {
|
|
||||||
window[messageChannel].postMessage(document.body.clientWidth / img.offsetHeight);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<img id="screen" src="{{stream_url}}">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,37 +0,0 @@
|
|||||||
window.externalApp = {};
|
|
||||||
window.externalApp.getExternalAuth = function(options) {
|
|
||||||
console.log("Starting external auth");
|
|
||||||
var options = JSON.parse(options);
|
|
||||||
if (options && options.callback) {
|
|
||||||
var responseData = {
|
|
||||||
access_token: "[token]",
|
|
||||||
expires_in: 1800
|
|
||||||
};
|
|
||||||
console.log("Waiting for callback to be added");
|
|
||||||
setTimeout(function(){
|
|
||||||
console.log("Calling a callback");
|
|
||||||
window[options.callback](true, responseData);
|
|
||||||
}, 900);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
window.externalApp.externalBus = function(message) {
|
|
||||||
console.log("External bus message: " + message);
|
|
||||||
var messageObj = JSON.parse(message);
|
|
||||||
if (messageObj.type == "config/get") {
|
|
||||||
var responseData = {
|
|
||||||
id: messageObj.id,
|
|
||||||
type: "result",
|
|
||||||
success: true,
|
|
||||||
result: {
|
|
||||||
hasSettingsScreen: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setTimeout(function(){
|
|
||||||
window.externalBus(responseData);
|
|
||||||
}, 500);
|
|
||||||
} else if (messageObj.type == "config_screen/show") {
|
|
||||||
HAClient.postMessage('show-settings');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
*/
|
|
@ -1,41 +0,0 @@
|
|||||||
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
|
||||||
|
|
||||||
## command
|
|
||||||
|
|
||||||
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
|
||||||
|
|
||||||
## exception
|
|
||||||
|
|
||||||
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/.packages, isolateId: isolates/68989666}}, details: Isolate must be runnable before this request is made.}}
|
|
||||||
|
|
||||||
```
|
|
||||||
null```
|
|
||||||
|
|
||||||
## flutter doctor
|
|
||||||
|
|
||||||
```
|
|
||||||
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
|
||||||
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
|
||||||
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
|
||||||
• Engine revision fee001c93f
|
|
||||||
• Dart version 2.4.0
|
|
||||||
|
|
||||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
|
||||||
• Android SDK at /home/estevez/Android/Sdk
|
|
||||||
• Android NDK location not configured (optional; useful for native profiling support)
|
|
||||||
• Platform android-29, build-tools 29.0.2
|
|
||||||
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
|
||||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
|
||||||
• All Android licenses accepted.
|
|
||||||
|
|
||||||
[✓] Android Studio (version 3.5)
|
|
||||||
• Android Studio at /home/estevez/bin/android-studio
|
|
||||||
• Flutter plugin version 38.2.3
|
|
||||||
• Dart plugin version 191.8423
|
|
||||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
|
||||||
|
|
||||||
[✓] Connected device (1 available)
|
|
||||||
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
|
||||||
|
|
||||||
• No issues found!
|
|
||||||
```
|
|
@ -1,41 +0,0 @@
|
|||||||
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
|
||||||
|
|
||||||
## command
|
|
||||||
|
|
||||||
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
|
||||||
|
|
||||||
## exception
|
|
||||||
|
|
||||||
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/.packages, isolateId: isolates/289688365}}, details: Isolate must be runnable before this request is made.}}
|
|
||||||
|
|
||||||
```
|
|
||||||
null```
|
|
||||||
|
|
||||||
## flutter doctor
|
|
||||||
|
|
||||||
```
|
|
||||||
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
|
||||||
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
|
||||||
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
|
||||||
• Engine revision fee001c93f
|
|
||||||
• Dart version 2.4.0
|
|
||||||
|
|
||||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
|
||||||
• Android SDK at /home/estevez/Android/Sdk
|
|
||||||
• Android NDK location not configured (optional; useful for native profiling support)
|
|
||||||
• Platform android-29, build-tools 29.0.2
|
|
||||||
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
|
||||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
|
||||||
• All Android licenses accepted.
|
|
||||||
|
|
||||||
[✓] Android Studio (version 3.5)
|
|
||||||
• Android Studio at /home/estevez/bin/android-studio
|
|
||||||
• Flutter plugin version 38.2.3
|
|
||||||
• Dart plugin version 191.8423
|
|
||||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
|
||||||
|
|
||||||
[✓] Connected device (1 available)
|
|
||||||
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
|
||||||
|
|
||||||
• No issues found!
|
|
||||||
```
|
|
@ -1,41 +0,0 @@
|
|||||||
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
|
||||||
|
|
||||||
## command
|
|
||||||
|
|
||||||
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
|
||||||
|
|
||||||
## exception
|
|
||||||
|
|
||||||
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/.packages, isolateId: isolates/866521062}}, details: Isolate must be runnable before this request is made.}}
|
|
||||||
|
|
||||||
```
|
|
||||||
null```
|
|
||||||
|
|
||||||
## flutter doctor
|
|
||||||
|
|
||||||
```
|
|
||||||
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
|
||||||
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
|
||||||
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
|
||||||
• Engine revision fee001c93f
|
|
||||||
• Dart version 2.4.0
|
|
||||||
|
|
||||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
|
||||||
• Android SDK at /home/estevez/Android/Sdk
|
|
||||||
• Android NDK location not configured (optional; useful for native profiling support)
|
|
||||||
• Platform android-29, build-tools 29.0.2
|
|
||||||
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
|
||||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
|
||||||
• All Android licenses accepted.
|
|
||||||
|
|
||||||
[✓] Android Studio (version 3.5)
|
|
||||||
• Android Studio at /home/estevez/bin/android-studio
|
|
||||||
• Flutter plugin version 38.2.3
|
|
||||||
• Dart plugin version 191.8423
|
|
||||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
|
||||||
|
|
||||||
[✓] Connected device (1 available)
|
|
||||||
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
|
||||||
|
|
||||||
• No issues found!
|
|
||||||
```
|
|
BIN
fonts/materialdesignicons-webfont-3-5-95.ttf
Normal file
BIN
fonts/materialdesignicons-webfont.ttf
Normal file
BIN
images/hassio-192x192.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
images/icon/icon.png
Normal file
After Width: | Height: | Size: 24 KiB |
@ -1,62 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class AlarmPanelCard extends StatelessWidget {
|
|
||||||
final AlarmPanelCardData card;
|
|
||||||
|
|
||||||
const AlarmPanelCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: MissedEntityWidget(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
List<Widget> body = [];
|
|
||||||
body.add(CardHeader(
|
|
||||||
name: card.name ?? "",
|
|
||||||
subtitle: Text("${card.entity.entity.displayState}",
|
|
||||||
),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
EntityIcon(
|
|
||||||
size: 50.0,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 26.0,
|
|
||||||
child: IconButton(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
"mdi:dots-vertical")),
|
|
||||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: card.entity.entity.entityId))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
));
|
|
||||||
body.add(
|
|
||||||
AlarmControlPanelControlsWidget(
|
|
||||||
extended: true,
|
|
||||||
states: card.states,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return CardWrapper(
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
handleTap: null,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: body
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,196 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class Badges extends StatelessWidget {
|
|
||||||
final BadgesData badges;
|
|
||||||
|
|
||||||
const Badges({Key key, this.badges}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow();
|
|
||||||
|
|
||||||
if (entitiesToShow.isNotEmpty) {
|
|
||||||
if (AppSettings().scrollBadges) {
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: BoxConstraints.tightFor(height: 112),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: entitiesToShow.map((entity) =>
|
|
||||||
EntityModel(
|
|
||||||
entityWrapper: entity,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
|
||||||
child: BadgeWidget(),
|
|
||||||
),
|
|
||||||
handleTap: true,
|
|
||||||
)).toList()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
|
||||||
child: Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 10.0,
|
|
||||||
runSpacing: 5,
|
|
||||||
children: entitiesToShow.map((entity) =>
|
|
||||||
EntityModel(
|
|
||||||
entityWrapper: entity,
|
|
||||||
child: BadgeWidget(),
|
|
||||||
handleTap: true,
|
|
||||||
)).toList(),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BadgeWidget extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final entityModel = EntityModel.of(context);
|
|
||||||
Widget badgeIcon;
|
|
||||||
String onBadgeTextValue;
|
|
||||||
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
|
||||||
switch (entityModel.entityWrapper.entity.domain) {
|
|
||||||
case "sun":
|
|
||||||
{
|
|
||||||
IconData iconData;
|
|
||||||
if (entityModel.entityWrapper.entity.state == "below_horizon") {
|
|
||||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc);
|
|
||||||
} else {
|
|
||||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8);
|
|
||||||
}
|
|
||||||
badgeIcon = Padding(
|
|
||||||
padding: EdgeInsets.all(10),
|
|
||||||
child: Icon(
|
|
||||||
iconData,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "camera":
|
|
||||||
case "media_player":
|
|
||||||
case "binary_sensor":
|
|
||||||
{
|
|
||||||
badgeIcon = EntityIcon(
|
|
||||||
imagePadding: EdgeInsets.all(0.0),
|
|
||||||
iconPadding: EdgeInsets.all(10),
|
|
||||||
color: Theme.of(context).textTheme.body2.color
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "device_tracker":
|
|
||||||
case "person":
|
|
||||||
{
|
|
||||||
badgeIcon = EntityIcon(
|
|
||||||
imagePadding: EdgeInsets.all(0.0),
|
|
||||||
iconPadding: EdgeInsets.all(10),
|
|
||||||
color: Theme.of(context).textTheme.body2.color
|
|
||||||
);
|
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
|
||||||
badgeIcon = Padding(
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: Text(
|
|
||||||
"${entityModel.entityWrapper.entity.displayState}",
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
softWrap: false,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.body1
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Widget onBadgeText;
|
|
||||||
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
|
||||||
onBadgeText = Container(width: 0.0, height: 0.0);
|
|
||||||
} else {
|
|
||||||
onBadgeText = Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: 50),
|
|
||||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
|
||||||
child: Text("$onBadgeTextValue",
|
|
||||||
style: Theme.of(context).textTheme.overline.copyWith(
|
|
||||||
color: HAClientTheme().getOnBadgeTextColor()
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.ellipsis
|
|
||||||
),
|
|
||||||
decoration: new BoxDecoration(
|
|
||||||
color: iconColor,
|
|
||||||
borderRadius: BorderRadius.circular(9.0),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return GestureDetector(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
Stack(
|
|
||||||
overflow: Overflow.visible,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
width: 45,
|
|
||||||
height: 45,
|
|
||||||
decoration: new BoxDecoration(
|
|
||||||
// Circle shape
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
// The border you want
|
|
||||||
border: Border.all(
|
|
||||||
width: 2.0,
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 41,
|
|
||||||
height: 41,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: badgeIcon,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -6,
|
|
||||||
child: onBadgeText
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: 45),
|
|
||||||
padding: EdgeInsets.only(top: 10),
|
|
||||||
child: Text(
|
|
||||||
"${entityModel.entityWrapper.displayName}",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.caption.copyWith(
|
|
||||||
fontSize: 10
|
|
||||||
),
|
|
||||||
softWrap: true,
|
|
||||||
maxLines: 3,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () => entityModel.entityWrapper.handleTap(),
|
|
||||||
onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(),
|
|
||||||
onLongPress: () => entityModel.entityWrapper.handleHold(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,730 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class CardData {
|
|
||||||
|
|
||||||
String type;
|
|
||||||
List<EntityWrapper> entities = [];
|
|
||||||
List conditions;
|
|
||||||
bool showEmpty;
|
|
||||||
List stateFilter;
|
|
||||||
bool stateColor = true;
|
|
||||||
|
|
||||||
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
|
|
||||||
|
|
||||||
factory CardData.parse(rawData) {
|
|
||||||
try {
|
|
||||||
if (rawData['type'] == null) {
|
|
||||||
rawData['type'] = CardType.ENTITIES;
|
|
||||||
} else if (!(rawData['type'] is String)) {
|
|
||||||
return CardData(null);
|
|
||||||
}
|
|
||||||
switch (rawData['type']) {
|
|
||||||
case CardType.ENTITIES:
|
|
||||||
case CardType.HISTORY_GRAPH:
|
|
||||||
case CardType.PICTURE_GLANCE:
|
|
||||||
case CardType.SENSOR:
|
|
||||||
case CardType.ENTITY:
|
|
||||||
case CardType.WEATHER_FORECAST:
|
|
||||||
case CardType.PLANT_STATUS:
|
|
||||||
if (rawData['entity'] != null) {
|
|
||||||
rawData['entities'] = [rawData['entity']];
|
|
||||||
}
|
|
||||||
return EntitiesCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.ALARM_PANEL:
|
|
||||||
return AlarmPanelCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.LIGHT:
|
|
||||||
return LightCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.PICTURE_ELEMENTS:
|
|
||||||
//TODO temporary solution
|
|
||||||
if (rawData.containsKey('camera_image')) {
|
|
||||||
rawData['entity'] = rawData['camera_image'];
|
|
||||||
return ButtonCardData(rawData);
|
|
||||||
} else {
|
|
||||||
return CardData(null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CardType.MAP:
|
|
||||||
return MapCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.ENTITY_BUTTON:
|
|
||||||
case CardType.BUTTON:
|
|
||||||
case CardType.PICTURE_ENTITY:
|
|
||||||
return ButtonCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.CONDITIONAL:
|
|
||||||
return CardData.parse(rawData['card']);
|
|
||||||
break;
|
|
||||||
case CardType.ENTITY_FILTER:
|
|
||||||
Map cardData = Map.from(rawData);
|
|
||||||
cardData.remove('type');
|
|
||||||
if (rawData.containsKey('card')) {
|
|
||||||
cardData.addAll(rawData['card']);
|
|
||||||
}
|
|
||||||
cardData['type'] ??= CardType.ENTITIES;
|
|
||||||
return CardData.parse(cardData);
|
|
||||||
break;
|
|
||||||
case CardType.GAUGE:
|
|
||||||
return GaugeCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.GLANCE:
|
|
||||||
case CardType.THERMOSTAT:
|
|
||||||
if (rawData['entity'] != null) {
|
|
||||||
rawData['entities'] = [rawData['entity']];
|
|
||||||
}
|
|
||||||
return GlanceCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.HORIZONTAL_STACK:
|
|
||||||
return HorizontalStackCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.VERTICAL_STACK:
|
|
||||||
return VerticalStackCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.MARKDOWN:
|
|
||||||
return MarkdownCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.MEDIA_CONTROL:
|
|
||||||
return MediaControlCardData(rawData);
|
|
||||||
break;
|
|
||||||
case CardType.BADGES:
|
|
||||||
return BadgesData(rawData);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (rawData.containsKey('entity')) {
|
|
||||||
rawData['entities'] = [rawData['entity']];
|
|
||||||
}
|
|
||||||
if (rawData.containsKey('entities') && rawData['entities'] is List) {
|
|
||||||
return EntitiesCardData(rawData);
|
|
||||||
}
|
|
||||||
return CardData(null);
|
|
||||||
}
|
|
||||||
} catch (error, stacktrace) {
|
|
||||||
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
|
|
||||||
return ErrorCardData(rawData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CardData(rawData) {
|
|
||||||
if (rawData != null && rawData is Map) {
|
|
||||||
type = rawData['type'];
|
|
||||||
conditions = rawData['conditions'] ?? [];
|
|
||||||
showEmpty = rawData['show_empty'] ?? true;
|
|
||||||
if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) {
|
|
||||||
stateFilter = rawData['state_filter'];
|
|
||||||
} else {
|
|
||||||
stateFilter = [];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
type = CardType.UNKNOWN;
|
|
||||||
conditions = [];
|
|
||||||
showEmpty = true;
|
|
||||||
stateFilter = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return UnsupportedCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<EntityWrapper> getEntitiesToShow() {
|
|
||||||
return entities.where((entityWrapper) {
|
|
||||||
if (entityWrapper.entity.isHidden) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
List currentStateFilter;
|
|
||||||
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
|
|
||||||
currentStateFilter = entityWrapper.stateFilter;
|
|
||||||
} else {
|
|
||||||
currentStateFilter = stateFilter;
|
|
||||||
}
|
|
||||||
bool showByFilter = currentStateFilter.isEmpty;
|
|
||||||
for (var allowedState in currentStateFilter) {
|
|
||||||
if (allowedState is String && allowedState == entityWrapper.entity.state) {
|
|
||||||
showByFilter = true;
|
|
||||||
break;
|
|
||||||
} else if (allowedState is Map) {
|
|
||||||
try {
|
|
||||||
var tmpVal = allowedState['attribute'] != null ? entityWrapper.entity.getAttribute(allowedState['attribute']) : entityWrapper.entity.state;
|
|
||||||
var valToCompareWith = allowedState['value'];
|
|
||||||
var valToCompare;
|
|
||||||
if (valToCompareWith is! String && tmpVal is String) {
|
|
||||||
valToCompare = double.tryParse(tmpVal);
|
|
||||||
} else {
|
|
||||||
valToCompare = tmpVal;
|
|
||||||
}
|
|
||||||
if (valToCompare != null) {
|
|
||||||
bool result;
|
|
||||||
switch (allowedState['operator']) {
|
|
||||||
case '<=': { result = valToCompare <= valToCompareWith;}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '<': { result = valToCompare < valToCompareWith;}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '>=': { result = valToCompare >= valToCompareWith;}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '>': { result = valToCompare > valToCompareWith;}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '!=': { result = valToCompare != valToCompareWith;}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'regex': {
|
|
||||||
RegExp regExp = RegExp(valToCompareWith.toString());
|
|
||||||
result = regExp.hasMatch(valToCompare.toString());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: {
|
|
||||||
result = valToCompare == valToCompareWith;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result) {
|
|
||||||
showByFilter = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState: $e', stacktrace: stacktrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return showByFilter;
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class BadgesData extends CardData {
|
|
||||||
|
|
||||||
String title;
|
|
||||||
String icon;
|
|
||||||
bool showHeaderToggle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return Badges(badges: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
BadgesData(rawData) : super(rawData) {
|
|
||||||
if (rawData['badges'] is List) {
|
|
||||||
rawData['badges'].forEach((dynamic rawBadge) {
|
|
||||||
if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {
|
|
||||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge)));
|
|
||||||
} else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) {
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(rawBadge['entity']),
|
|
||||||
overrideName: rawBadge["name"]?.toString(),
|
|
||||||
overrideIcon: rawBadge["icon"],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (rawBadge is Map && rawBadge.containsKey('entities')) {
|
|
||||||
_parseEntities(rawBadge);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseEntities(rawData) {
|
|
||||||
var rawEntities = rawData['entities'] ?? [];
|
|
||||||
rawEntities.forEach((rawEntity) {
|
|
||||||
if (rawEntity is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(rawEntity),
|
|
||||||
stateFilter: rawData['state_filter'] ?? [],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) {
|
|
||||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
overrideName: rawEntity["name"]?.toString(),
|
|
||||||
overrideIcon: rawEntity["icon"],
|
|
||||||
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class EntitiesCardData extends CardData {
|
|
||||||
|
|
||||||
String title;
|
|
||||||
String icon;
|
|
||||||
bool showHeaderToggle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return EntitiesCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
EntitiesCardData(rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
title = rawData['title']?.toString();
|
|
||||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
|
||||||
stateColor = rawData['state_color'] ?? false;
|
|
||||||
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
|
||||||
//Parsing entities
|
|
||||||
var rawEntities = rawData['entities'] ?? [];
|
|
||||||
rawEntities.forEach((rawEntity) {
|
|
||||||
if (rawEntity is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
|
||||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rawEntity["type"] == "divider") {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.divider()));
|
|
||||||
} else if (rawEntity["type"] == "section") {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
|
||||||
} else if (rawEntity["type"] == "call-service") {
|
|
||||||
Map uiActionData = {
|
|
||||||
"tap_action": {
|
|
||||||
"action": EntityUIAction.callService,
|
|
||||||
"service": rawEntity["service"],
|
|
||||||
"service_data": rawEntity["service_data"]
|
|
||||||
},
|
|
||||||
"hold_action": EntityUIAction.none
|
|
||||||
};
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: Entity.callService(
|
|
||||||
icon: rawEntity["icon"],
|
|
||||||
name: rawEntity["name"]?.toString(),
|
|
||||||
service: rawEntity["service"],
|
|
||||||
actionName: rawEntity["action_name"]
|
|
||||||
),
|
|
||||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
|
||||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (rawEntity["type"] == "weblink") {
|
|
||||||
Map uiActionData = {
|
|
||||||
"tap_action": {
|
|
||||||
"action": EntityUIAction.navigate,
|
|
||||||
"service": rawEntity["url"]
|
|
||||||
},
|
|
||||||
"hold_action": EntityUIAction.none
|
|
||||||
};
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: Entity.weblink(
|
|
||||||
icon: rawEntity["icon"],
|
|
||||||
name: rawEntity["name"]?.toString(),
|
|
||||||
url: rawEntity["url"]
|
|
||||||
),
|
|
||||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
|
||||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
|
||||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
|
||||||
overrideName: rawEntity["name"]?.toString(),
|
|
||||||
overrideIcon: rawEntity["icon"],
|
|
||||||
stateFilter: rawEntity['state_filter'] ?? [],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlarmPanelCardData extends CardData {
|
|
||||||
|
|
||||||
String name;
|
|
||||||
List<dynamic> states;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return AlarmPanelCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
AlarmPanelCardData(rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
name = rawData['name']?.toString();
|
|
||||||
states = rawData['states'];
|
|
||||||
//Parsing entity
|
|
||||||
var entitiId = rawData["entity"];
|
|
||||||
if (entitiId != null && entitiId is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(entitiId),
|
|
||||||
stateColor: true,
|
|
||||||
overrideName: name
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class LightCardData extends CardData {
|
|
||||||
|
|
||||||
String name;
|
|
||||||
String icon;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
if (this.entity != null && this.entity.entity is LightEntity) {
|
|
||||||
return LightCard(card: this);
|
|
||||||
}
|
|
||||||
return ErrorCard(
|
|
||||||
errorText: 'Specify an entity from within the light domain.',
|
|
||||||
showReportButton: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LightCardData(rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
name = rawData['name']?.toString();
|
|
||||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
|
||||||
//Parsing entity
|
|
||||||
var entitiId = rawData["entity"];
|
|
||||||
if (entitiId != null && entitiId is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(entitiId),
|
|
||||||
overrideName: name,
|
|
||||||
overrideIcon: icon,
|
|
||||||
uiAction: EntityUIAction()..tapAction = EntityUIAction.toggle
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ButtonCardData extends CardData {
|
|
||||||
|
|
||||||
String name;
|
|
||||||
String icon;
|
|
||||||
bool showName;
|
|
||||||
bool showIcon;
|
|
||||||
double iconHeightPx = 0;
|
|
||||||
double iconHeightRem = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return EntityButtonCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
ButtonCardData(rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
name = rawData['name']?.toString();
|
|
||||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
|
||||||
showName = rawData['show_name'] ?? true;
|
|
||||||
showIcon = rawData['show_icon'] ?? true;
|
|
||||||
stateColor = rawData['state_color'] ?? true;
|
|
||||||
var rawHeight = rawData['icon_height'];
|
|
||||||
if (rawHeight != null && rawHeight is String) {
|
|
||||||
if (rawHeight.contains('px')) {
|
|
||||||
iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0;
|
|
||||||
} else if (rawHeight.contains('rem')) {
|
|
||||||
iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;
|
|
||||||
} else if (rawHeight.contains('em')) {
|
|
||||||
iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Parsing entity
|
|
||||||
var entitiId = rawData["entity"];
|
|
||||||
if (entitiId != null && entitiId is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(entitiId),
|
|
||||||
overrideName: name,
|
|
||||||
overrideIcon: icon,
|
|
||||||
stateColor: stateColor,
|
|
||||||
uiAction: EntityUIAction(
|
|
||||||
rawEntityData: rawData
|
|
||||||
)
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
|
||||||
}
|
|
||||||
} else if (entitiId == null) {
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: Entity.ghost(
|
|
||||||
name,
|
|
||||||
icon,
|
|
||||||
),
|
|
||||||
stateColor: stateColor,
|
|
||||||
uiAction: EntityUIAction(
|
|
||||||
rawEntityData: rawData
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GaugeCardData extends CardData {
|
|
||||||
|
|
||||||
String name;
|
|
||||||
String unit;
|
|
||||||
double min;
|
|
||||||
double max;
|
|
||||||
Map severity;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return GaugeCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
GaugeCardData(rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
name = rawData['name']?.toString();
|
|
||||||
unit = rawData['unit'];
|
|
||||||
if (rawData['min'] is int) {
|
|
||||||
min = rawData['min'].toDouble();
|
|
||||||
} else if (rawData['min'] is double) {
|
|
||||||
min = rawData['min'];
|
|
||||||
} else {
|
|
||||||
min = 0;
|
|
||||||
}
|
|
||||||
if (rawData['max'] is int) {
|
|
||||||
max = rawData['max'].toDouble();
|
|
||||||
} else if (rawData['max'] is double) {
|
|
||||||
max = rawData['max'];
|
|
||||||
} else {
|
|
||||||
max = 100;
|
|
||||||
}
|
|
||||||
severity = rawData['severity'];
|
|
||||||
//Parsing entity
|
|
||||||
var entitiId = rawData["entity"] is List ? rawData["entity"][0] : rawData["entity"];
|
|
||||||
if (entitiId != null && entitiId is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(entitiId),
|
|
||||||
overrideName: name
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class GlanceCardData extends CardData {
|
|
||||||
|
|
||||||
String title;
|
|
||||||
bool showName;
|
|
||||||
bool showIcon;
|
|
||||||
bool showState;
|
|
||||||
bool stateColor;
|
|
||||||
int columnsCount;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return GlanceCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
GlanceCardData(rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
title = rawData["title"]?.toString();
|
|
||||||
showName = rawData['show_name'] ?? true;
|
|
||||||
showIcon = rawData['show_icon'] ?? true;
|
|
||||||
showState = rawData['show_state'] ?? true;
|
|
||||||
stateColor = rawData['state_color'] ?? true;
|
|
||||||
columnsCount = rawData['columns'] ?? 4;
|
|
||||||
//Parsing entities
|
|
||||||
var rawEntities = rawData["entities"] ?? [];
|
|
||||||
rawEntities.forEach((rawEntity) {
|
|
||||||
if (rawEntity is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
|
||||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
|
||||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
stateColor: stateColor,
|
|
||||||
overrideName: rawEntity["name"]?.toString(),
|
|
||||||
overrideIcon: rawEntity["icon"],
|
|
||||||
stateFilter: rawEntity['state_filter'] ?? [],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class HorizontalStackCardData extends CardData {
|
|
||||||
|
|
||||||
List<CardData> childCards;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return HorizontalStackCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalStackCardData(rawData) : super(rawData) {
|
|
||||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
|
||||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
|
||||||
return CardData.parse(childCard);
|
|
||||||
}).toList();
|
|
||||||
} else {
|
|
||||||
childCards = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class VerticalStackCardData extends CardData {
|
|
||||||
|
|
||||||
List<CardData> childCards;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return VerticalStackCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
VerticalStackCardData(rawData) : super(rawData) {
|
|
||||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
|
||||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
|
||||||
return CardData.parse(childCard);
|
|
||||||
}).toList();
|
|
||||||
} else {
|
|
||||||
childCards = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class MarkdownCardData extends CardData {
|
|
||||||
|
|
||||||
String title;
|
|
||||||
String content;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return MarkdownCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
MarkdownCardData(rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
title = rawData['title'];
|
|
||||||
content = rawData['content'];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class MapCardData extends CardData {
|
|
||||||
|
|
||||||
String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return MapCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
MapCardData(rawData) : super(rawData) {
|
|
||||||
//Parsing card data
|
|
||||||
title = rawData['title'];
|
|
||||||
List<dynamic> geoLocationSources = rawData['geo_location_sources'] ?? [];
|
|
||||||
if (geoLocationSources.isNotEmpty) {
|
|
||||||
//TODO add entities by source
|
|
||||||
}
|
|
||||||
var rawEntities = rawData["entities"] ?? [];
|
|
||||||
rawEntities.forEach((rawEntity) {
|
|
||||||
if (rawEntity is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
|
||||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
|
||||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
|
||||||
entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
stateColor: stateColor,
|
|
||||||
overrideName: rawEntity["name"]?.toString(),
|
|
||||||
overrideIcon: rawEntity["icon"],
|
|
||||||
stateFilter: rawEntity['state_filter'] ?? [],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class MediaControlCardData extends CardData {
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return MediaControlsCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaControlCardData(rawData) : super(rawData) {
|
|
||||||
var entitiId = rawData["entity"];
|
|
||||||
if (entitiId != null && entitiId is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
|
||||||
entities.add(EntityWrapper(
|
|
||||||
entity: HomeAssistant().entities.get(entitiId),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class ErrorCardData extends CardData {
|
|
||||||
|
|
||||||
String cardConfig;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildCardWidget() {
|
|
||||||
return ErrorCard(card: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorCardData(rawData) : super(rawData) {
|
|
||||||
cardConfig = '$rawData';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntitiesCard extends StatelessWidget {
|
|
||||||
final EntitiesCardData card;
|
|
||||||
|
|
||||||
const EntitiesCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
|
||||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
List<Widget> body = [];
|
|
||||||
Widget headerSwitch;
|
|
||||||
if (card.showHeaderToggle) {
|
|
||||||
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
|
|
||||||
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
|
|
||||||
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
|
|
||||||
}).map((EntityWrapper en) {
|
|
||||||
return en.entity.entityId;
|
|
||||||
}).toList();
|
|
||||||
headerSwitch = Switch(
|
|
||||||
value: headerToggleVal,
|
|
||||||
onChanged: (val) {
|
|
||||||
if (entitiesToToggle.isNotEmpty) {
|
|
||||||
ConnectionManager().callService(
|
|
||||||
domain: "homeassistant",
|
|
||||||
service: val ? "turn_on" : "turn_off",
|
|
||||||
entityId: entitiesToToggle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
body.add(
|
|
||||||
CardHeader(
|
|
||||||
name: card.title,
|
|
||||||
trailing: headerSwitch,
|
|
||||||
emptyPadding: Sizes.rowPadding,
|
|
||||||
leading: card.icon != null ? Icon(
|
|
||||||
MaterialDesignIcons.getIconDataFromIconName(card.icon),
|
|
||||||
size: Sizes.iconSize,
|
|
||||||
color: Theme.of(context).textTheme.headline.color
|
|
||||||
) : null,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
body.addAll(
|
|
||||||
entitiesToShow.map((EntityWrapper entity) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: entity,
|
|
||||||
handleTap: true,
|
|
||||||
child: entity.entity.buildDefaultWidget(context)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return CardWrapper(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
right: Sizes.rightWidgetPadding,
|
|
||||||
left: Sizes.leftWidgetPadding,
|
|
||||||
bottom: Sizes.rowPadding,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: body
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityButtonCard extends StatelessWidget {
|
|
||||||
|
|
||||||
final ButtonCardData card;
|
|
||||||
|
|
||||||
EntityButtonCard({
|
|
||||||
Key key, this.card
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
EntityWrapper entityWrapper = card.entity;
|
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: MissedEntityWidget(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
} else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
|
||||||
return Container(width: 0.0, height: 0.0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize);
|
|
||||||
|
|
||||||
Widget buttonIcon;
|
|
||||||
if (!card.showIcon) {
|
|
||||||
buttonIcon = Container(height: Sizes.rowPadding, width: 10);
|
|
||||||
} else if (iconSize > 0) {
|
|
||||||
buttonIcon = SizedBox(
|
|
||||||
height: iconSize,
|
|
||||||
child: FractionallySizedBox(
|
|
||||||
widthFactor: 0.5,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
child: EntityIcon(
|
|
||||||
//padding: EdgeInsets.only(top: 6),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
buttonIcon = AspectRatio(
|
|
||||||
aspectRatio: 2,
|
|
||||||
child: FractionallySizedBox(
|
|
||||||
widthFactor: 0.5,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.fitWidth,
|
|
||||||
child: EntityIcon(
|
|
||||||
//padding: EdgeInsets.only(top: 6),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CardWrapper(
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
child: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(top: 5),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
buttonIcon,
|
|
||||||
_buildName(context)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
handleTap: true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildName(BuildContext context) {
|
|
||||||
if (card.showName) {
|
|
||||||
return EntityName(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 3,
|
|
||||||
textStyle: Theme.of(context).textTheme.subhead,
|
|
||||||
wordsWrap: true,
|
|
||||||
textAlign: TextAlign.center
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Container(width: 0, height: 0);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class ErrorCard extends StatelessWidget {
|
|
||||||
final ErrorCardData card;
|
|
||||||
final String errorText;
|
|
||||||
final bool showReportButton;
|
|
||||||
|
|
||||||
const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
String error;
|
|
||||||
if (errorText == null) {
|
|
||||||
error = 'There was an error showing ${card?.type}';
|
|
||||||
} else {
|
|
||||||
error = errorText;
|
|
||||||
}
|
|
||||||
return CardWrapper(
|
|
||||||
color: Theme.of(context).errorColor,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
error,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
card != null ?
|
|
||||||
RaisedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Clipboard.setData(new ClipboardData(text: card.cardConfig));
|
|
||||||
},
|
|
||||||
child: Text('Copy card config'),
|
|
||||||
) :
|
|
||||||
Container(width: 0, height: 0),
|
|
||||||
showReportButton ?
|
|
||||||
RaisedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
|
|
||||||
},
|
|
||||||
child: Text('Report issue'),
|
|
||||||
) :
|
|
||||||
Container(width: 0, height: 0)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,201 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class GaugeCard extends StatelessWidget {
|
|
||||||
|
|
||||||
final GaugeCardData card;
|
|
||||||
|
|
||||||
GaugeCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
EntityWrapper entityWrapper = card.entity;
|
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: MissedEntityWidget(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
entityWrapper.overrideName = card.name ??
|
|
||||||
entityWrapper.displayName;
|
|
||||||
entityWrapper.unitOfMeasurementOverride = card.unit ??
|
|
||||||
entityWrapper.unitOfMeasurement;
|
|
||||||
double fixedValue;
|
|
||||||
double value = entityWrapper.entity.doubleState;
|
|
||||||
if (value > card.max) {
|
|
||||||
fixedValue = card.max.toDouble();
|
|
||||||
} else if (value < card.min) {
|
|
||||||
fixedValue = card.min.toDouble();
|
|
||||||
} else {
|
|
||||||
fixedValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<GaugeRange> ranges;
|
|
||||||
Color currentColor;
|
|
||||||
if (card.severity != null && card.severity["green"] is int && card.severity["red"] is int && card.severity["yellow"] is int) {
|
|
||||||
List<RangeContainer> rangesList = <RangeContainer>[
|
|
||||||
RangeContainer(card.severity["green"], HAClientTheme().getGreenGaugeColor()),
|
|
||||||
RangeContainer(card.severity["red"], HAClientTheme().getRedGaugeColor()),
|
|
||||||
RangeContainer(card.severity["yellow"], HAClientTheme().getYellowGaugeColor())
|
|
||||||
];
|
|
||||||
rangesList.sort((current, next) {
|
|
||||||
if (current.startFrom > next.startFrom) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (current.startFrom < next.startFrom) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fixedValue < rangesList[1].startFrom) {
|
|
||||||
currentColor = rangesList[0].color;
|
|
||||||
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
|
|
||||||
currentColor = rangesList[1].color;
|
|
||||||
} else {
|
|
||||||
currentColor = rangesList[2].color;
|
|
||||||
}
|
|
||||||
|
|
||||||
ranges = [
|
|
||||||
GaugeRange(
|
|
||||||
startValue: rangesList[0].startFrom.toDouble(),
|
|
||||||
endValue: rangesList[1].startFrom.toDouble(),
|
|
||||||
color: rangesList[0].color.withOpacity(0.1),
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
endWidth: 0.3,
|
|
||||||
startWidth: 0.3
|
|
||||||
),
|
|
||||||
GaugeRange(
|
|
||||||
startValue: rangesList[1].startFrom.toDouble(),
|
|
||||||
endValue: rangesList[2].startFrom.toDouble(),
|
|
||||||
color: rangesList[1].color.withOpacity(0.1),
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
endWidth: 0.3,
|
|
||||||
startWidth: 0.3
|
|
||||||
),
|
|
||||||
GaugeRange(
|
|
||||||
startValue: rangesList[2].startFrom.toDouble(),
|
|
||||||
endValue: card.max.toDouble(),
|
|
||||||
color: rangesList[2].color.withOpacity(0.1),
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
endWidth: 0.3,
|
|
||||||
startWidth: 0.3
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (ranges == null) {
|
|
||||||
currentColor = Theme.of(context).primaryColorDark;
|
|
||||||
ranges = <GaugeRange>[
|
|
||||||
GaugeRange(
|
|
||||||
startValue: card.min.toDouble(),
|
|
||||||
endValue: card.max.toDouble(),
|
|
||||||
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
endWidth: 0.3,
|
|
||||||
startWidth: 0.3,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return CardWrapper(
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: entityWrapper,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1.8,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
children: <Widget>[
|
|
||||||
IgnorePointer(
|
|
||||||
ignoring: true,
|
|
||||||
child: SfRadialGauge(
|
|
||||||
axes: <RadialAxis>[
|
|
||||||
RadialAxis(
|
|
||||||
maximum: card.max.toDouble(),
|
|
||||||
minimum: card.min.toDouble(),
|
|
||||||
showLabels: false,
|
|
||||||
useRangeColorForAxis: true,
|
|
||||||
showTicks: false,
|
|
||||||
canScaleToFit: true,
|
|
||||||
ranges: ranges,
|
|
||||||
axisLineStyle: AxisLineStyle(
|
|
||||||
thickness: 0.3,
|
|
||||||
thicknessUnit: GaugeSizeUnit.factor,
|
|
||||||
color: Colors.transparent
|
|
||||||
),
|
|
||||||
startAngle: 180,
|
|
||||||
endAngle: 0,
|
|
||||||
pointers: <GaugePointer>[
|
|
||||||
RangePointer(
|
|
||||||
value: fixedValue,
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
width: 0.3,
|
|
||||||
color: currentColor,
|
|
||||||
enableAnimation: true,
|
|
||||||
animationType: AnimationType.bounceOut,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Flexible(
|
|
||||||
flex: 8,
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: Container()
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 6,
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: FractionallySizedBox(
|
|
||||||
widthFactor: 0.4,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: SimpleEntityState(
|
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
expanded: false,
|
|
||||||
maxLines: 1,
|
|
||||||
textAlign: TextAlign.center
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 3,
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
child: EntityName(
|
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
textStyle: Theme.of(context).textTheme.subhead
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
handleTap: true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class RangeContainer {
|
|
||||||
final int startFrom;
|
|
||||||
Color color;
|
|
||||||
|
|
||||||
RangeContainer(this.startFrom, this.color);
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class GlanceCard extends StatelessWidget {
|
|
||||||
final GlanceCardData card;
|
|
||||||
|
|
||||||
const GlanceCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
|
||||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
int length = entitiesToShow.length;
|
|
||||||
int rowsCount;
|
|
||||||
int columnsCount;
|
|
||||||
if (length == 0) {
|
|
||||||
columnsCount = 0;
|
|
||||||
rowsCount = 0;
|
|
||||||
} else {
|
|
||||||
columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
|
||||||
rowsCount = (length / columnsCount).round();
|
|
||||||
}
|
|
||||||
List<TableRow> rows = [];
|
|
||||||
for (int i = 0; i < rowsCount; i++) {
|
|
||||||
int start = i*columnsCount;
|
|
||||||
int end = start + math.min(columnsCount, length - start);
|
|
||||||
List<Widget> rowChildren = [];
|
|
||||||
rowChildren.addAll(entitiesToShow.sublist(
|
|
||||||
start, end
|
|
||||||
).map(
|
|
||||||
(EntityWrapper entity){
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: entity,
|
|
||||||
child: _buildEntityContainer(context, entity),
|
|
||||||
handleTap: true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
).toList()
|
|
||||||
);
|
|
||||||
while (rowChildren.length < columnsCount) {
|
|
||||||
rowChildren.add(
|
|
||||||
Container()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
rows.add(
|
|
||||||
TableRow(
|
|
||||||
children: rowChildren
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return CardWrapper(
|
|
||||||
child: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
CardHeader(
|
|
||||||
name: card.title,
|
|
||||||
emptyPadding: Sizes.rowPadding,
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
children: rows
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) {
|
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return MissedEntityWidget();
|
|
||||||
} else if (entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
|
||||||
return Container(width: 0.0, height: 0.0,);
|
|
||||||
}
|
|
||||||
List<Widget> result = [];
|
|
||||||
if (card.showName) {
|
|
||||||
result.add(_buildName(context));
|
|
||||||
}
|
|
||||||
result.add(
|
|
||||||
EntityIcon(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
size: Sizes.iconSize,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (card.showState) {
|
|
||||||
result.add(_buildState());
|
|
||||||
}
|
|
||||||
|
|
||||||
return InkResponse(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: result,
|
|
||||||
),
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildName(BuildContext context) {
|
|
||||||
return EntityName(
|
|
||||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
wordsWrap: false,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
textStyle: Theme.of(context).textTheme.body1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildState() {
|
|
||||||
return SimpleEntityState(
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
expanded: false,
|
|
||||||
maxLines: 1,
|
|
||||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class HorizontalStackCard extends StatelessWidget {
|
|
||||||
final HorizontalStackCardData card;
|
|
||||||
|
|
||||||
const HorizontalStackCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.childCards.isNotEmpty) {
|
|
||||||
List<Widget> children = [];
|
|
||||||
children = card.childCards.map((childCard) => Flexible(
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: childCard.buildCardWidget()
|
|
||||||
)
|
|
||||||
).toList();
|
|
||||||
return IntrinsicHeight(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,161 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class LightCard extends StatefulWidget {
|
|
||||||
|
|
||||||
final LightCardData card;
|
|
||||||
|
|
||||||
LightCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return _LightCardState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LightCardState extends State<LightCard> {
|
|
||||||
|
|
||||||
double _actualBrightness;
|
|
||||||
double _newBrightness;
|
|
||||||
bool _changedHere = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setBrightness(double value, LightEntity entity) {
|
|
||||||
setState((){
|
|
||||||
_newBrightness = value;
|
|
||||||
_changedHere = true;
|
|
||||||
});
|
|
||||||
ConnectionManager().callService(
|
|
||||||
domain: entity.domain,
|
|
||||||
service: "turn_on",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"brightness": value.round()}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
EntityWrapper entityWrapper = widget.card.entity;
|
|
||||||
LightEntity entity = entityWrapper.entity;
|
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: widget.card.entity,
|
|
||||||
child: MissedEntityWidget(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
entityWrapper.overrideName = widget.card.name ??
|
|
||||||
entityWrapper.displayName;
|
|
||||||
entityWrapper.overrideIcon = widget.card.icon ??
|
|
||||||
entityWrapper.icon;
|
|
||||||
|
|
||||||
if (!_changedHere) {
|
|
||||||
_actualBrightness = (entity.brightness ?? 0).toDouble();
|
|
||||||
_newBrightness = _actualBrightness;
|
|
||||||
} else {
|
|
||||||
_changedHere = false;
|
|
||||||
}
|
|
||||||
Color lightColor = entity.color?.toColor();
|
|
||||||
Color color;
|
|
||||||
if (lightColor != null && lightColor != Colors.white) {
|
|
||||||
color = lightColor;
|
|
||||||
} else {
|
|
||||||
color = Theme.of(context).accentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CardWrapper(
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: entityWrapper,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints.loose(Size(200, 200)),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
SfRadialGauge(
|
|
||||||
axes: <RadialAxis>[
|
|
||||||
RadialAxis(
|
|
||||||
onAxisTapped: (val) {
|
|
||||||
_setBrightness(val, entity);
|
|
||||||
},
|
|
||||||
maximum: 255,
|
|
||||||
minimum: 0,
|
|
||||||
showLabels: false,
|
|
||||||
showTicks: false,
|
|
||||||
axisLineStyle: AxisLineStyle(
|
|
||||||
thickness: 0.05,
|
|
||||||
thicknessUnit: GaugeSizeUnit.factor,
|
|
||||||
color: HAClientTheme().getDisabledStateColor(context)
|
|
||||||
),
|
|
||||||
pointers: <GaugePointer>[
|
|
||||||
RangePointer(
|
|
||||||
value: _actualBrightness,
|
|
||||||
sizeUnit: GaugeSizeUnit.factor,
|
|
||||||
width: 0.05,
|
|
||||||
color: color,
|
|
||||||
enableAnimation: true,
|
|
||||||
animationType: AnimationType.bounceOut,
|
|
||||||
),
|
|
||||||
MarkerPointer(
|
|
||||||
value: _newBrightness,
|
|
||||||
markerType: MarkerType.circle,
|
|
||||||
markerHeight: 20,
|
|
||||||
markerWidth: 20,
|
|
||||||
enableDragging: true,
|
|
||||||
onValueChangeEnd: (val) {
|
|
||||||
_setBrightness(val, entity);
|
|
||||||
},
|
|
||||||
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
|
||||||
//enableAnimation: true,
|
|
||||||
//animationType: AnimationType.bounceOut,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
FractionallySizedBox(
|
|
||||||
heightFactor: 0.4,
|
|
||||||
widthFactor: 0.4,
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: InkResponse(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
child: EntityIcon(
|
|
||||||
showBadge: false,
|
|
||||||
padding: EdgeInsets.all(0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
EntityName(
|
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
wordsWrap: true,
|
|
||||||
maxLines: 3,
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
handleTap: true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class MapCard extends StatefulWidget {
|
|
||||||
final MapCardData card;
|
|
||||||
|
|
||||||
const MapCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_MapCardState createState() => _MapCardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MapCardState extends State<MapCard> {
|
|
||||||
|
|
||||||
void _openMap(BuildContext context) {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (bc) {
|
|
||||||
return Scaffold(
|
|
||||||
primary: false,
|
|
||||||
/*appBar: new AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
|
||||||
Navigator.pop(context);
|
|
||||||
}),
|
|
||||||
actions: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.fullscreen),
|
|
||||||
onPressed: () {},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
// 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("${widget.card.title ?? ""}"),
|
|
||||||
),*/
|
|
||||||
body: Container(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
child: SafeArea(
|
|
||||||
child: Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
EntitiesMap(
|
|
||||||
entities: widget.card.entities,
|
|
||||||
interactive: true
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
child: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
|
||||||
Navigator.pop(context);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return CardWrapper(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
CardHeader(name: widget.card.title),
|
|
||||||
Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _openMap(context),
|
|
||||||
child: EntitiesMap(
|
|
||||||
aspectRatio: 1,
|
|
||||||
interactive: false,
|
|
||||||
entities: widget.card.entities,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
child: Text('Tap to open interactive map', style: Theme.of(context).textTheme.caption)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class MarkdownCard extends StatelessWidget {
|
|
||||||
final MarkdownCardData card;
|
|
||||||
|
|
||||||
const MarkdownCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.content == null) {
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
} else if (card.content == '***') {
|
|
||||||
return Container(height: Sizes.rowPadding, width: 0.0,);
|
|
||||||
}
|
|
||||||
return CardWrapper(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
CardHeader(name: card.title),
|
|
||||||
MarkdownBody(
|
|
||||||
data: card.content,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class MediaControlsCard extends StatelessWidget {
|
|
||||||
final MediaControlCardData card;
|
|
||||||
|
|
||||||
const MediaControlsCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: MissedEntityWidget(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
} else if (card.entity.entity.domain == null || card.entity.entity.domain != 'media_player') {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
child: ErrorEntityWidget(
|
|
||||||
text: '${card.entity.entity?.entityId} is not a media_player',
|
|
||||||
),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return CardWrapper(
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: card.entity,
|
|
||||||
handleTap: null,
|
|
||||||
child: MediaPlayerWidget()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class UnsupportedCard extends StatelessWidget {
|
|
||||||
final CardData card;
|
|
||||||
|
|
||||||
const UnsupportedCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(height: 20);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class VerticalStackCard extends StatelessWidget {
|
|
||||||
final VerticalStackCardData card;
|
|
||||||
|
|
||||||
const VerticalStackCard({Key key, this.card}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (card.childCards.isNotEmpty) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: card.childCards.map<Widget>(
|
|
||||||
(childCard) => childCard.buildCardWidget()
|
|
||||||
).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class CardWrapper extends StatelessWidget {
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
final EdgeInsets padding;
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
color: color,
|
|
||||||
child: Padding(
|
|
||||||
padding: padding,
|
|
||||||
child: child
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class EntitiesMap extends StatelessWidget {
|
|
||||||
|
|
||||||
final List<EntityWrapper> entities;
|
|
||||||
final bool interactive;
|
|
||||||
final double aspectRatio;
|
|
||||||
final LatLng center;
|
|
||||||
final double zoom;
|
|
||||||
|
|
||||||
const EntitiesMap({Key key, this.entities: const [], this.aspectRatio, this.interactive: true, this.center, this.zoom}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List<Marker> markers = [];
|
|
||||||
List<LatLng> points = [];
|
|
||||||
entities.forEach((entityWrapper) {
|
|
||||||
double lat = entityWrapper.entity._getDoubleAttributeValue("latitude");
|
|
||||||
double long = entityWrapper.entity._getDoubleAttributeValue("longitude");
|
|
||||||
if (lat != null && long != null) {
|
|
||||||
points.add(LatLng(lat, long));
|
|
||||||
markers.add(
|
|
||||||
Marker(
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
point: LatLng(lat, long),
|
|
||||||
builder: (ctx) => EntityModel(
|
|
||||||
handleTap: true,
|
|
||||||
entityWrapper: entityWrapper,
|
|
||||||
child: EntityIcon(
|
|
||||||
size: 36,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
MapOptions mapOptions;
|
|
||||||
if (center != null) {
|
|
||||||
mapOptions = MapOptions(
|
|
||||||
interactive: interactive,
|
|
||||||
center: center,
|
|
||||||
zoom: zoom ?? 10,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
mapOptions = MapOptions(
|
|
||||||
interactive: interactive,
|
|
||||||
bounds: LatLngBounds.fromPoints(points),
|
|
||||||
boundsOptions: FitBoundsOptions(padding: EdgeInsets.all(40)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Widget map = FlutterMap(
|
|
||||||
options: mapOptions,
|
|
||||||
layers: [
|
|
||||||
new TileLayerOptions(
|
|
||||||
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
||||||
subdomains: ['a', 'b', 'c']
|
|
||||||
),
|
|
||||||
new MarkerLayerOptions(
|
|
||||||
markers: markers,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (aspectRatio != null) {
|
|
||||||
return AspectRatio(
|
|
||||||
aspectRatio: aspectRatio,
|
|
||||||
child: map
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
part of 'main.dart';
|
|
||||||
|
|
||||||
class EntityState {
|
|
||||||
static const on = 'on';
|
|
||||||
static const off = 'off';
|
|
||||||
static const home = 'home';
|
|
||||||
static const not_home = 'not_home';
|
|
||||||
static const unknown = 'unknown';
|
|
||||||
static const open = 'open';
|
|
||||||
static const opening = 'opening';
|
|
||||||
static const closed = 'closed';
|
|
||||||
static const closing = 'closing';
|
|
||||||
static const playing = 'playing';
|
|
||||||
static const paused = 'paused';
|
|
||||||
static const idle = 'idle';
|
|
||||||
static const standby = 'standby';
|
|
||||||
static const alarm_disarmed = 'disarmed';
|
|
||||||
static const alarm_armed_home = 'armed_home';
|
|
||||||
static const alarm_armed_away = 'armed_away';
|
|
||||||
static const alarm_armed_night = 'armed_night';
|
|
||||||
static const alarm_armed_custom_bypass = 'armed_custom_bypass';
|
|
||||||
static const alarm_pending = 'pending';
|
|
||||||
static const alarm_arming = 'arming';
|
|
||||||
static const alarm_disarming = 'disarming';
|
|
||||||
static const alarm_triggered = 'triggered';
|
|
||||||
static const locked = 'locked';
|
|
||||||
static const unlocked = 'unlocked';
|
|
||||||
static const unavailable = 'unavailable';
|
|
||||||
static const ok = 'ok';
|
|
||||||
static const problem = 'problem';
|
|
||||||
static const active = 'active';
|
|
||||||
static const cleaning = 'cleaning';
|
|
||||||
static const docked = 'docked';
|
|
||||||
static const returning = 'returning';
|
|
||||||
static const error = 'error';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class CardType {
|
|
||||||
static const HORIZONTAL_STACK = "horizontal-stack";
|
|
||||||
static const VERTICAL_STACK = "vertical-stack";
|
|
||||||
static const ENTITIES = "entities";
|
|
||||||
static const GLANCE = "glance";
|
|
||||||
static const MEDIA_CONTROL = "media-control";
|
|
||||||
static const WEATHER_FORECAST = "weather-forecast";
|
|
||||||
static const THERMOSTAT = "thermostat";
|
|
||||||
static const SENSOR = "sensor";
|
|
||||||
static const PLANT_STATUS = "plant-status";
|
|
||||||
static const PICTURE_ENTITY = "picture-entity";
|
|
||||||
static const PICTURE_ELEMENTS = "picture-elements";
|
|
||||||
static const PICTURE = "picture";
|
|
||||||
static const MAP = "map";
|
|
||||||
static const IFRAME = "iframe";
|
|
||||||
static const GAUGE = "gauge";
|
|
||||||
static const ENTITY_BUTTON = "entity-button";
|
|
||||||
static const ENTITY = "entity";
|
|
||||||
static const BUTTON = "button";
|
|
||||||
static const CONDITIONAL = "conditional";
|
|
||||||
static const ALARM_PANEL = "alarm-panel";
|
|
||||||
static const MARKDOWN = "markdown";
|
|
||||||
static const LIGHT = "light";
|
|
||||||
static const ENTITY_FILTER = "entity-filter";
|
|
||||||
static const UNKNOWN = "unknown";
|
|
||||||
static const HISTORY_GRAPH = "history-graph";
|
|
||||||
static const PICTURE_GLANCE = "picture-glance";
|
|
||||||
static const BADGES = "badges";
|
|
||||||
}
|
|
||||||
|
|
||||||
class Sizes {
|
|
||||||
static const rightWidgetPadding = 10.0;
|
|
||||||
static const leftWidgetPadding = 10.0;
|
|
||||||
static const buttonPadding = 4.0;
|
|
||||||
static const extendedWidgetHeight = 50.0;
|
|
||||||
static const iconSize = 28.0;
|
|
||||||
static const largeIconSize = 46.0;
|
|
||||||
//static const stateFontSize = 15.0;
|
|
||||||
//static const nameFontSize = 15.0;
|
|
||||||
//static const smallFontSize = 14.0;
|
|
||||||
//static const largeFontSize = 24.0;
|
|
||||||
static const inputWidth = 160.0;
|
|
||||||
static const rowPadding = 10.0;
|
|
||||||
static const doubleRowPadding = rowPadding*2;
|
|
||||||
static const minViewColumnWidth = 350;
|
|
||||||
static const entityPageMaxWidth = 400.0;
|
|
||||||
static const mainPageScreenSeparatorWidth = 5.0;
|
|
||||||
static const tabletMinWidth = minViewColumnWidth + entityPageMaxWidth + 5;
|
|
||||||
}
|
|
@ -1,190 +0,0 @@
|
|||||||
part of '../../../main.dart';
|
|
||||||
|
|
||||||
class CameraStreamView extends StatefulWidget {
|
|
||||||
|
|
||||||
final bool withControls;
|
|
||||||
|
|
||||||
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_CameraStreamViewState createState() => _CameraStreamViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CameraStreamViewState extends State<CameraStreamView> {
|
|
||||||
|
|
||||||
CameraEntity _entity;
|
|
||||||
String _streamUrl = "";
|
|
||||||
bool _isLoaded = false;
|
|
||||||
double _aspectRatio = 1.33;
|
|
||||||
String _webViewHtml;
|
|
||||||
String _jsMessageChannelName = 'unknown';
|
|
||||||
Completer _loading;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _loadResources() {
|
|
||||||
if (_loading != null && !_loading.isCompleted) {
|
|
||||||
Logger.d("[Camera Player] Resources loading is not finished yet");
|
|
||||||
return _loading.future;
|
|
||||||
}
|
|
||||||
Logger.d("[Camera Player] Loading resources");
|
|
||||||
_loading = Completer();
|
|
||||||
_entity = EntityModel
|
|
||||||
.of(context)
|
|
||||||
.entityWrapper
|
|
||||||
.entity;
|
|
||||||
if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
|
|
||||||
HomeAssistant().getCameraStream(_entity.entityId)
|
|
||||||
.then((data) {
|
|
||||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
|
||||||
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
|
|
||||||
_webViewHtml = Uri.dataFromString(
|
|
||||||
file.replaceFirst('{{stream_url}}', '${AppSettings().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
|
||||||
mimeType: 'text/html',
|
|
||||||
encoding: Encoding.getByName('utf-8')
|
|
||||||
).toString();
|
|
||||||
_loading.complete();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catchError((e) {
|
|
||||||
if (e == 'start_stream_failed') {
|
|
||||||
Logger.e("[Camera Player] Home Assistant failed starting stream. Forcing MJPEG: $e");
|
|
||||||
_loadMJPEG().then((_) {
|
|
||||||
_loading.complete();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_loading.completeError(e);
|
|
||||||
Logger.e("[Camera Player] Error loading stream: $e");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_loadMJPEG().then((_) {
|
|
||||||
_loading.complete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _loading.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _loadMJPEG() async {
|
|
||||||
_streamUrl = '${AppSettings().httpWebHost}/api/camera_proxy_stream/${_entity
|
|
||||||
.entityId}?token=${_entity.attributes['access_token']}';
|
|
||||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
|
||||||
var file = await rootBundle.loadString('assets/html/cameraView.html');
|
|
||||||
_webViewHtml = Uri.dataFromString(
|
|
||||||
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
|
||||||
mimeType: 'text/html',
|
|
||||||
encoding: Encoding.getByName('utf-8')
|
|
||||||
).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildScreen() {
|
|
||||||
Widget screenWidget;
|
|
||||||
if (!_isLoaded) {
|
|
||||||
screenWidget = Center(
|
|
||||||
child: EntityPicture(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
screenWidget = WebView(
|
|
||||||
initialUrl: _webViewHtml,
|
|
||||||
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
|
|
||||||
debuggingEnabled: Logger.isInDebugMode,
|
|
||||||
gestureNavigationEnabled: false,
|
|
||||||
javascriptMode: JavascriptMode.unrestricted,
|
|
||||||
javascriptChannels: {
|
|
||||||
JavascriptChannel(
|
|
||||||
name: _jsMessageChannelName,
|
|
||||||
onMessageReceived: ((message) {
|
|
||||||
Logger.d('[Camera Player] Message from page: $message');
|
|
||||||
setState((){
|
|
||||||
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return AspectRatio(
|
|
||||||
aspectRatio: _aspectRatio,
|
|
||||||
child: screenWidget
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildControls() {
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.refresh),
|
|
||||||
iconSize: 40,
|
|
||||||
color: Theme.of(context).accentColor,
|
|
||||||
onPressed: _isLoaded ? () {
|
|
||||||
setState(() {
|
|
||||||
_isLoaded = false;
|
|
||||||
});
|
|
||||||
} : null,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Container(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.fullscreen),
|
|
||||||
iconSize: 40,
|
|
||||||
color: Theme.of(context).accentColor,
|
|
||||||
onPressed: _isLoaded ? () {
|
|
||||||
Navigator.of(context).pushReplacement(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (conext) => FullScreenPage(
|
|
||||||
child: EntityModel(
|
|
||||||
child: CameraStreamView(
|
|
||||||
withControls: false
|
|
||||||
),
|
|
||||||
handleTap: false,
|
|
||||||
entityWrapper: EntityWrapper(
|
|
||||||
entity: _entity
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
fullscreenDialog: true
|
|
||||||
)
|
|
||||||
).then((_){
|
|
||||||
eventBus.fire(ShowEntityPageEvent(entityId: _entity.entityId));
|
|
||||||
});
|
|
||||||
} : null,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (!_isLoaded && (_loading == null || _loading.isCompleted)) {
|
|
||||||
_loadResources().then((_) => setState((){ _isLoaded = true; }));
|
|
||||||
}
|
|
||||||
if (widget.withControls) {
|
|
||||||
return Card(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
_buildScreen(),
|
|
||||||
_buildControls()
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return _buildScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
part of '../../../main.dart';
|
|
||||||
|
|
||||||
class TemperatureControlWidget extends StatelessWidget {
|
|
||||||
final double value;
|
|
||||||
final bool active;
|
|
||||||
final onInc;
|
|
||||||
final onDec;
|
|
||||||
|
|
||||||
TemperatureControlWidget(
|
|
||||||
{Key key,
|
|
||||||
@required this.value,
|
|
||||||
@required this.onInc,
|
|
||||||
@required this.onDec,
|
|
||||||
//this.fontSize,
|
|
||||||
this.active: false
|
|
||||||
})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
"$value",
|
|
||||||
style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
'mdi:chevron-up')),
|
|
||||||
iconSize: 30.0,
|
|
||||||
onPressed: () => onInc(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
'mdi:chevron-down')),
|
|
||||||
iconSize: 30.0,
|
|
||||||
onPressed: () => onDec(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class DefaultEntityContainer extends StatelessWidget {
|
|
||||||
DefaultEntityContainer({
|
|
||||||
Key key,
|
|
||||||
@required this.state
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Widget state;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
|
||||||
return MissedEntityWidget();
|
|
||||||
}
|
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.divider) {
|
|
||||||
return Divider();
|
|
||||||
}
|
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
Divider(),
|
|
||||||
Text(
|
|
||||||
"${entityModel.entityWrapper.entity.displayName}",
|
|
||||||
style: HAClientTheme().getLinkTextStyle(context).copyWith(
|
|
||||||
decoration: TextDecoration.none
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Widget result = Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
EntityIcon(),
|
|
||||||
Flexible(
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
flex: 3,
|
|
||||||
child: EntityName(
|
|
||||||
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
state
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
return InkWell(
|
|
||||||
onLongPress: () {
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
entityModel.entityWrapper.handleHold();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
entityModel.entityWrapper.handleTap();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDoubleTap: () {
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
entityModel.entityWrapper.handleDoubleTap();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: result,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,138 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityIcon extends StatelessWidget {
|
|
||||||
|
|
||||||
final EdgeInsetsGeometry padding;
|
|
||||||
final EdgeInsetsGeometry iconPadding;
|
|
||||||
final EdgeInsetsGeometry imagePadding;
|
|
||||||
final double size;
|
|
||||||
final Color color;
|
|
||||||
final bool showBadge;
|
|
||||||
|
|
||||||
const EntityIcon({Key key, this.color, this.showBadge: true, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key);
|
|
||||||
|
|
||||||
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
|
||||||
if (entityId == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
String domain = entityId.split(".")[0];
|
|
||||||
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
|
|
||||||
String iconNameByDeviceClass;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
|
||||||
Color iconColor;
|
|
||||||
if (color != null) {
|
|
||||||
iconColor = color;
|
|
||||||
} else if (entityWrapper.stateColor) {
|
|
||||||
iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context);
|
|
||||||
} else {
|
|
||||||
iconColor = HAClientTheme().getOffStateColor(context);
|
|
||||||
}
|
|
||||||
Widget iconWidget;
|
|
||||||
bool isPicture = false;
|
|
||||||
if (entityWrapper == null) {
|
|
||||||
iconWidget = Container(
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (entityWrapper.entityPicture != null) {
|
|
||||||
iconWidget = ClipOval(
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: '${entityWrapper.entityPicture}',
|
|
||||||
width: size+12,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
height: size+12,
|
|
||||||
errorWidget: (context, str, dyn) {
|
|
||||||
return Padding(
|
|
||||||
padding: iconPadding ?? padding,
|
|
||||||
child: _buildIcon(entityWrapper, iconColor)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
isPicture = true;
|
|
||||||
} else {
|
|
||||||
iconWidget = _buildIcon(entityWrapper, iconColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EdgeInsetsGeometry computedPadding;
|
|
||||||
if (isPicture && imagePadding != null) {
|
|
||||||
computedPadding = imagePadding;
|
|
||||||
} else if (!isPicture && iconPadding != null) {
|
|
||||||
computedPadding = iconPadding;
|
|
||||||
} else {
|
|
||||||
computedPadding = padding;
|
|
||||||
}
|
|
||||||
return Padding(
|
|
||||||
padding: computedPadding,
|
|
||||||
child: iconWidget,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildIcon(EntityWrapper entityWrapper, Color iconColor) {
|
|
||||||
Widget iconWidget;
|
|
||||||
String iconName = entityWrapper.icon;
|
|
||||||
int iconCode = 0;
|
|
||||||
if (iconName.length > 0) {
|
|
||||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
|
||||||
} else {
|
|
||||||
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
|
|
||||||
entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
|
|
||||||
}
|
|
||||||
if (showBadge && entityWrapper.entity is LightEntity &&
|
|
||||||
(entityWrapper.entity as LightEntity).supportColor &&
|
|
||||||
(entityWrapper.entity as LightEntity).color != null &&
|
|
||||||
(entityWrapper.entity as LightEntity).color.toColor() != Colors.white
|
|
||||||
) {
|
|
||||||
Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();
|
|
||||||
iconWidget = Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
Icon(
|
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
|
||||||
size: size,
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Container(
|
|
||||||
width: size / 3,
|
|
||||||
height: size / 3,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: lightColor,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
boxShadow: <BoxShadow>[
|
|
||||||
BoxShadow(
|
|
||||||
spreadRadius: 0,
|
|
||||||
blurRadius: 0,
|
|
||||||
offset: Offset(0.3, 0.3)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
iconWidget = Icon(
|
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
|
||||||
size: size,
|
|
||||||
color: iconColor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return iconWidget;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityModel extends InheritedWidget {
|
|
||||||
const EntityModel({
|
|
||||||
Key key,
|
|
||||||
@required this.entityWrapper,
|
|
||||||
@required this.handleTap,
|
|
||||||
@required Widget child,
|
|
||||||
}) : super(key: key, child: child);
|
|
||||||
|
|
||||||
final EntityWrapper entityWrapper;
|
|
||||||
final bool handleTap;
|
|
||||||
|
|
||||||
static EntityModel of(BuildContext context) {
|
|
||||||
return context.dependOnInheritedWidgetOfExactType<EntityModel>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool updateShouldNotify(EntityModel oldWidget) {
|
|
||||||
return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityPageLayout extends StatelessWidget {
|
|
||||||
|
|
||||||
final Entity entity;
|
|
||||||
|
|
||||||
EntityPageLayout({Key key, this.entity}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: EntityWrapper(entity: entity),
|
|
||||||
child: ListView(
|
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
|
||||||
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),
|
|
||||||
),
|
|
||||||
LastUpdatedWidget(),
|
|
||||||
Divider(),
|
|
||||||
entity._buildAdditionalControlsForPage(context),
|
|
||||||
Divider(),
|
|
||||||
SpoilerCard(
|
|
||||||
title: "State history",
|
|
||||||
body: EntityHistoryWidget(),
|
|
||||||
),
|
|
||||||
SpoilerCard(
|
|
||||||
title: "Attributes",
|
|
||||||
body: EntityAttributesList(),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,217 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityWrapper {
|
|
||||||
|
|
||||||
String overrideName;
|
|
||||||
String overrideIcon;
|
|
||||||
final bool stateColor;
|
|
||||||
EntityUIAction uiAction;
|
|
||||||
Entity entity;
|
|
||||||
String unitOfMeasurementOverride;
|
|
||||||
final List stateFilter;
|
|
||||||
|
|
||||||
String get icon => overrideIcon ?? entity.icon;
|
|
||||||
String get entityPicture => entity.entityPicture;
|
|
||||||
String get displayName => overrideName ?? entity.displayName;
|
|
||||||
String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement;
|
|
||||||
|
|
||||||
EntityWrapper({
|
|
||||||
this.entity,
|
|
||||||
this.overrideIcon,
|
|
||||||
this.overrideName,
|
|
||||||
this.stateColor: true,
|
|
||||||
this.uiAction,
|
|
||||||
this.stateFilter
|
|
||||||
}) {
|
|
||||||
if (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || entity.statelessType == StatelessEntityType.webLink) {
|
|
||||||
if (uiAction == null) {
|
|
||||||
uiAction = EntityUIAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleTap() {
|
|
||||||
switch (uiAction.tapAction) {
|
|
||||||
case EntityUIAction.toggle: {
|
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.callService: {
|
|
||||||
if (uiAction.tapService != null) {
|
|
||||||
ConnectionManager().callService(
|
|
||||||
domain: uiAction.tapService.split(".")[0],
|
|
||||||
service: uiAction.tapService.split(".")[1],
|
|
||||||
data: uiAction.tapServiceData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.none: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.moreInfo: {
|
|
||||||
eventBus.fire(
|
|
||||||
new ShowEntityPageEvent(entityId: entity.entityId));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.navigate: {
|
|
||||||
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
|
|
||||||
//TODO handle local urls
|
|
||||||
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.tapService}');
|
|
||||||
} else {
|
|
||||||
Launcher.launchURLInBrowser(uiAction.tapService);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleHold() {
|
|
||||||
switch (uiAction.holdAction) {
|
|
||||||
case EntityUIAction.toggle: {
|
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.callService: {
|
|
||||||
if (uiAction.holdService != null) {
|
|
||||||
ConnectionManager().callService(
|
|
||||||
domain: uiAction.holdService.split(".")[0],
|
|
||||||
service: uiAction.holdService.split(".")[1],
|
|
||||||
data: uiAction.holdServiceData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.moreInfo: {
|
|
||||||
eventBus.fire(
|
|
||||||
new ShowEntityPageEvent(entityId: entity.entityId));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.navigate: {
|
|
||||||
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
|
|
||||||
//TODO handle local urls
|
|
||||||
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.holdService}');
|
|
||||||
} else {
|
|
||||||
Launcher.launchURLInBrowser(uiAction.holdService);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleDoubleTap() {
|
|
||||||
switch (uiAction.doubleTapAction) {
|
|
||||||
case EntityUIAction.toggle: {
|
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.callService: {
|
|
||||||
if (uiAction.doubleTapService != null) {
|
|
||||||
ConnectionManager().callService(
|
|
||||||
domain: uiAction.doubleTapService.split(".")[0],
|
|
||||||
service: uiAction.doubleTapService.split(".")[1],
|
|
||||||
data: uiAction.doubleTapServiceData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.moreInfo: {
|
|
||||||
eventBus.fire(
|
|
||||||
new ShowEntityPageEvent(entityId: entity.entityId));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EntityUIAction.navigate: {
|
|
||||||
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
|
|
||||||
//TODO handle local urls
|
|
||||||
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.doubleTapService}');
|
|
||||||
} else {
|
|
||||||
Launcher.launchURLInBrowser(uiAction.doubleTapService);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class EntityUIAction {
|
|
||||||
static const moreInfo = 'more-info';
|
|
||||||
static const toggle = 'toggle';
|
|
||||||
static const callService = 'call-service';
|
|
||||||
static const navigate = 'navigate';
|
|
||||||
static const none = 'none';
|
|
||||||
|
|
||||||
String tapAction = EntityUIAction.moreInfo;
|
|
||||||
String tapNavigationPath;
|
|
||||||
String tapService;
|
|
||||||
Map<String, dynamic> tapServiceData;
|
|
||||||
String holdAction = EntityUIAction.moreInfo;
|
|
||||||
String holdNavigationPath;
|
|
||||||
String holdService;
|
|
||||||
Map<String, dynamic> holdServiceData;
|
|
||||||
String doubleTapAction = EntityUIAction.none;
|
|
||||||
String doubleTapNavigationPath;
|
|
||||||
String doubleTapService;
|
|
||||||
Map<String, dynamic> doubleTapServiceData;
|
|
||||||
|
|
||||||
EntityUIAction({rawEntityData}) {
|
|
||||||
if (rawEntityData != null) {
|
|
||||||
if (rawEntityData["tap_action"] != null) {
|
|
||||||
if (rawEntityData["tap_action"] is String) {
|
|
||||||
tapAction = rawEntityData["tap_action"];
|
|
||||||
} else {
|
|
||||||
tapAction =
|
|
||||||
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
|
|
||||||
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
|
|
||||||
tapService = rawEntityData["tap_action"]["service"];
|
|
||||||
tapServiceData = rawEntityData["tap_action"]["service_data"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rawEntityData["hold_action"] != null) {
|
|
||||||
if (rawEntityData["hold_action"] is String) {
|
|
||||||
holdAction = rawEntityData["hold_action"];
|
|
||||||
} else {
|
|
||||||
holdAction =
|
|
||||||
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
|
|
||||||
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
|
|
||||||
holdService = rawEntityData["hold_action"]["service"];
|
|
||||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rawEntityData["double_tap_action"] != null) {
|
|
||||||
if (rawEntityData["double_tap_action"] is String) {
|
|
||||||
doubleTapAction = rawEntityData["double_tap_action"];
|
|
||||||
} else {
|
|
||||||
doubleTapAction =
|
|
||||||
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
|
|
||||||
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
|
|
||||||
doubleTapService = rawEntityData["double_tap_action"]["service"];
|
|
||||||
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class ErrorEntityWidget extends StatelessWidget {
|
|
||||||
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
ErrorEntityWidget({
|
|
||||||
Key key, this.text
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
|
||||||
String errorText = text ?? "Entity error: ${entityModel.entityWrapper.entity?.entityId}";
|
|
||||||
return Container(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(5.0),
|
|
||||||
child: Text(errorText),
|
|
||||||
),
|
|
||||||
color: Theme.of(context).errorColor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
part of '../../../main.dart';
|
|
||||||
|
|
||||||
class MediaPlayerProgressBar extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
_MediaPlayerProgressBarState createState() => _MediaPlayerProgressBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
|
|
||||||
|
|
||||||
Timer _timer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
initState() {
|
|
||||||
super.initState();
|
|
||||||
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
|
||||||
setState(() {
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
|
||||||
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
|
||||||
double progress = 0;
|
|
||||||
int currentPosition;
|
|
||||||
if (entity.canCalculateActualPosition()) {
|
|
||||||
currentPosition = entity.getActualPosition().toInt();
|
|
||||||
if (currentPosition > 0) {
|
|
||||||
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return LinearProgressIndicator(
|
|
||||||
value: progress,
|
|
||||||
backgroundColor: Colors.black45,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(HAClientTheme().getOnStateColor(context)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_timer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
part of '../../../main.dart';
|
|
||||||
|
|
||||||
class MediaPlayerSeekBar extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
_MediaPlayerSeekBarState createState() => _MediaPlayerSeekBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|
||||||
|
|
||||||
Timer _timer;
|
|
||||||
bool _seekStarted = false;
|
|
||||||
bool _changedHere = false;
|
|
||||||
double _currentPosition = 0;
|
|
||||||
int _savedPosition = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
initState() {
|
|
||||||
super.initState();
|
|
||||||
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
|
||||||
if (!_seekStarted && !_changedHere) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
|
||||||
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
|
||||||
|
|
||||||
if (entity.canCalculateActualPosition() && entity.state != EntityState.idle) {
|
|
||||||
if (HomeAssistant().sendToPlayerId == entity.entityId && HomeAssistant().savedPlayerPosition != null) {
|
|
||||||
_savedPosition = HomeAssistant().savedPlayerPosition;
|
|
||||||
HomeAssistant().savedPlayerPosition = null;
|
|
||||||
HomeAssistant().sendToPlayerId = null;
|
|
||||||
}
|
|
||||||
if (entity.state == EntityState.playing && !_seekStarted &&
|
|
||||||
!_changedHere) {
|
|
||||||
_currentPosition = entity.getActualPosition();
|
|
||||||
} else if (entity.state == EntityState.paused) {
|
|
||||||
_currentPosition = entity.positionSeconds.toDouble();
|
|
||||||
} else if (_changedHere) {
|
|
||||||
_changedHere = false;
|
|
||||||
}
|
|
||||||
List<Widget> buttons = [];
|
|
||||||
if (_savedPosition > 0) {
|
|
||||||
buttons.add(
|
|
||||||
RaisedButton(
|
|
||||||
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
|
|
||||||
color: Theme.of(context).accentColor,
|
|
||||||
onPressed: () {
|
|
||||||
ConnectionManager().callService(
|
|
||||||
domain: "media_player",
|
|
||||||
service: "media_seek",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"seek_position": _savedPosition}
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
_savedPosition = 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 20, Sizes.rightWidgetPadding, 0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
Text("00:00"),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
"${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.title.copyWith(
|
|
||||||
color: Colors.blue
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
|
|
||||||
],
|
|
||||||
),
|
|
||||||
UniversalSlider(
|
|
||||||
min: 0,
|
|
||||||
activeColor: Theme.of(context).accentColor,
|
|
||||||
max: entity.durationSeconds.toDouble(),
|
|
||||||
value: _currentPosition,
|
|
||||||
onChangeStart: (val) {
|
|
||||||
_seekStarted = true;
|
|
||||||
},
|
|
||||||
onChanged: (val) {
|
|
||||||
setState(() {
|
|
||||||
_currentPosition = val;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onChangeEnd: (val) {
|
|
||||||
_seekStarted = false;
|
|
||||||
Timer(Duration(milliseconds: 500), () {
|
|
||||||
if (!_seekStarted) {
|
|
||||||
ConnectionManager().callService(
|
|
||||||
domain: "media_player",
|
|
||||||
service: "media_seek",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
data: {"seek_position": val}
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
_changedHere = true;
|
|
||||||
_currentPosition = val;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ButtonBar(
|
|
||||||
children: buttons,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_timer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class SunEntity extends Entity {
|
|
||||||
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class UniversalSlider extends StatefulWidget {
|
|
||||||
|
|
||||||
final Function onChanged;
|
|
||||||
final Function onChangeEnd;
|
|
||||||
final Function onChangeStart;
|
|
||||||
final Widget leading;
|
|
||||||
final Widget closing;
|
|
||||||
final String title;
|
|
||||||
final double min;
|
|
||||||
final Color activeColor;
|
|
||||||
final double max;
|
|
||||||
final double value;
|
|
||||||
final int divisions;
|
|
||||||
final String label;
|
|
||||||
final EdgeInsets padding;
|
|
||||||
|
|
||||||
const UniversalSlider({Key key, this.onChanged, this.label, this.onChangeStart, this.activeColor, this.divisions, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return UniversalSliderState();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class UniversalSliderState extends State<UniversalSlider> {
|
|
||||||
|
|
||||||
double _value;
|
|
||||||
bool _changeStarted = false;
|
|
||||||
bool _changedHere = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List <Widget> row = [];
|
|
||||||
List <Widget> col = [];
|
|
||||||
if (!_changedHere) {
|
|
||||||
_value = widget.value;
|
|
||||||
} else {
|
|
||||||
_changedHere = false;
|
|
||||||
}
|
|
||||||
if (widget.leading != null) {
|
|
||||||
row.add(widget.leading);
|
|
||||||
}
|
|
||||||
row.add(
|
|
||||||
Flexible(
|
|
||||||
child: Slider(
|
|
||||||
value: _value ?? math.max(widget.max ?? 100, _value ?? 0),
|
|
||||||
min: widget.min ?? 0,
|
|
||||||
max: widget.max ?? 100,
|
|
||||||
activeColor: widget.activeColor,
|
|
||||||
label: widget.label,
|
|
||||||
onChangeStart: (value) {
|
|
||||||
_changeStarted = true;
|
|
||||||
widget.onChangeStart?.call(value);
|
|
||||||
},
|
|
||||||
divisions: widget.divisions,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_value = value;
|
|
||||||
_changedHere = true;
|
|
||||||
});
|
|
||||||
widget.onChanged?.call(value);
|
|
||||||
},
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
_changeStarted = false;
|
|
||||||
setState(() {
|
|
||||||
_value = value;
|
|
||||||
_changedHere = true;
|
|
||||||
});
|
|
||||||
Timer(Duration(milliseconds: 500), () {
|
|
||||||
if (!_changeStarted) {
|
|
||||||
widget.onChangeEnd?.call(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (widget.closing != null) {
|
|
||||||
row.add(widget.closing);
|
|
||||||
}
|
|
||||||
if (widget.title != null) {
|
|
||||||
col.addAll(<Widget>[
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text('${widget.title}'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
col.addAll(<Widget>[
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: row,
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,)
|
|
||||||
]);
|
|
||||||
return Padding(
|
|
||||||
padding: widget.padding,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: col,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class VacuumEntity extends Entity {
|
|
||||||
|
|
||||||
static const SUPPORT_TURN_ON = 1;
|
|
||||||
static const SUPPORT_TURN_OFF = 2;
|
|
||||||
static const SUPPORT_PAUSE = 4;
|
|
||||||
static const SUPPORT_STOP = 8;
|
|
||||||
static const SUPPORT_RETURN_HOME = 16;
|
|
||||||
static const SUPPORT_FAN_SPEED = 32;
|
|
||||||
static const SUPPORT_BATTERY = 64;
|
|
||||||
static const SUPPORT_STATUS = 128;
|
|
||||||
static const SUPPORT_SEND_COMMAND = 256;
|
|
||||||
static const SUPPORT_LOCATE = 512;
|
|
||||||
static const SUPPORT_CLEAN_SPOT = 1024;
|
|
||||||
static const SUPPORT_MAP = 2048;
|
|
||||||
static const SUPPORT_STATE = 4096;
|
|
||||||
static const SUPPORT_START = 8192;
|
|
||||||
|
|
||||||
VacuumEntity(Map rawData, String webHost) : super(rawData, webHost);
|
|
||||||
|
|
||||||
bool get supportTurnOn => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_TURN_ON) ==
|
|
||||||
VacuumEntity.SUPPORT_TURN_ON);
|
|
||||||
bool get supportTurnOff => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_TURN_OFF) ==
|
|
||||||
VacuumEntity.SUPPORT_TURN_OFF);
|
|
||||||
bool get supportPause => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_PAUSE) ==
|
|
||||||
VacuumEntity.SUPPORT_PAUSE);
|
|
||||||
bool get supportStop => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_STOP) ==
|
|
||||||
VacuumEntity.SUPPORT_STOP);
|
|
||||||
bool get supportReturnHome => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_RETURN_HOME) ==
|
|
||||||
VacuumEntity.SUPPORT_RETURN_HOME);
|
|
||||||
bool get supportFanSpeed => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_FAN_SPEED) ==
|
|
||||||
VacuumEntity.SUPPORT_FAN_SPEED);
|
|
||||||
bool get supportBattery => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_BATTERY) ==
|
|
||||||
VacuumEntity.SUPPORT_BATTERY);
|
|
||||||
bool get supportStatus => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_STATUS) ==
|
|
||||||
VacuumEntity.SUPPORT_STATUS);
|
|
||||||
bool get supportSendCommand => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_SEND_COMMAND) ==
|
|
||||||
VacuumEntity.SUPPORT_SEND_COMMAND);
|
|
||||||
bool get supportLocate => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_LOCATE) ==
|
|
||||||
VacuumEntity.SUPPORT_LOCATE);
|
|
||||||
bool get supportCleanSpot => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_CLEAN_SPOT) ==
|
|
||||||
VacuumEntity.SUPPORT_CLEAN_SPOT);
|
|
||||||
bool get supportMap => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_MAP) ==
|
|
||||||
VacuumEntity.SUPPORT_MAP);
|
|
||||||
bool get supportState => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_STATE) ==
|
|
||||||
VacuumEntity.SUPPORT_STATE);
|
|
||||||
bool get supportStart => ((supportedFeatures &
|
|
||||||
VacuumEntity.SUPPORT_START) ==
|
|
||||||
VacuumEntity.SUPPORT_START);
|
|
||||||
|
|
||||||
List<String> get fanSpeedList => getStringListAttributeValue("fan_speed_list");
|
|
||||||
String get fanSpeed => getAttribute("fan_speed");
|
|
||||||
String get status => getAttribute("status");
|
|
||||||
int get batteryLevel => _getIntAttributeValue("battery_level");
|
|
||||||
String get batteryIcon => getAttribute("battery_icon");
|
|
||||||
double get cleanedArea => _getDoubleAttributeValue("cleaned_area");
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildStatePart(BuildContext context) {
|
|
||||||
if (supportTurnOn || supportTurnOff) {
|
|
||||||
return SwitchStateWidget(
|
|
||||||
domainForService: "vacuum",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return SimpleEntityState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildStatePartForPage(BuildContext context) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: EntityWrapper(
|
|
||||||
entity: this
|
|
||||||
),
|
|
||||||
child: VacuumStateButton(),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
|
||||||
return VacuumControls();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,229 +0,0 @@
|
|||||||
part of '../../../main.dart';
|
|
||||||
|
|
||||||
class VacuumControls extends StatelessWidget {
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
VacuumEntity entity = EntityModel.of(context).entityWrapper.entity;
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
_buildStatusAndBattery(entity, context),
|
|
||||||
_buildCommands(entity),
|
|
||||||
_buildFanSpeed(entity),
|
|
||||||
_buildAdditionalInfo(entity)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStatusAndBattery(VacuumEntity entity, BuildContext context) {
|
|
||||||
List<Widget> result = [];
|
|
||||||
if (entity.supportStatus) {
|
|
||||||
result.addAll(
|
|
||||||
<Widget>[
|
|
||||||
Text("Status:"),
|
|
||||||
Container(width: 6,),
|
|
||||||
Expanded(
|
|
||||||
//flex: 1,
|
|
||||||
child: Text(
|
|
||||||
"${entity.status}",
|
|
||||||
maxLines: 1,
|
|
||||||
softWrap: true,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.body2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (entity.supportBattery && entity.batteryLevel != null) {
|
|
||||||
String iconName = entity.batteryIcon ?? "mdi:battery";
|
|
||||||
int batteryLevel = entity.batteryLevel ?? 100;
|
|
||||||
result.addAll(<Widget>[
|
|
||||||
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
|
|
||||||
Container(width: 6,),
|
|
||||||
Text("$batteryLevel %")
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isEmpty) {
|
|
||||||
return Container(width: 0, height: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: result,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCommands(VacuumEntity entity) {
|
|
||||||
List<Widget> commandButtons = [];
|
|
||||||
double iconSize = 32;
|
|
||||||
if (entity.supportStart) {
|
|
||||||
commandButtons.add(
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:play")),
|
|
||||||
iconSize: iconSize,
|
|
||||||
onPressed: () => ConnectionManager().callService(
|
|
||||||
domain: "vacuum",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
service: "start"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (entity.supportPause && !entity.supportStart) {
|
|
||||||
commandButtons.add(
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:play-pause")),
|
|
||||||
iconSize: iconSize,
|
|
||||||
onPressed: () => ConnectionManager().callService(
|
|
||||||
domain: "vacuum",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
service: "start_pause"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (entity.supportPause) {
|
|
||||||
commandButtons.add(
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:pause")),
|
|
||||||
iconSize: iconSize,
|
|
||||||
onPressed: () => ConnectionManager().callService(
|
|
||||||
domain: "vacuum",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
service: "pause"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (entity.supportStop) {
|
|
||||||
commandButtons.add(
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:stop")),
|
|
||||||
iconSize: iconSize,
|
|
||||||
onPressed: () => ConnectionManager().callService(
|
|
||||||
domain: "vacuum",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
service: "stop"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (entity.supportCleanSpot) {
|
|
||||||
commandButtons.add(
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:broom")),
|
|
||||||
iconSize: iconSize,
|
|
||||||
onPressed: () => ConnectionManager().callService(
|
|
||||||
domain: "vacuum",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
service: "clean_spot"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (entity.supportLocate) {
|
|
||||||
commandButtons.add(
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:map-marker")),
|
|
||||||
iconSize: iconSize,
|
|
||||||
onPressed: () => ConnectionManager().callService(
|
|
||||||
domain: "vacuum",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
service: "locate"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (entity.supportReturnHome) {
|
|
||||||
commandButtons.add(
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-map-marker")),
|
|
||||||
iconSize: iconSize,
|
|
||||||
onPressed: () => ConnectionManager().callService(
|
|
||||||
domain: "vacuum",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
service: "return_to_base"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commandButtons.isEmpty) {
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text("Vacuum cleaner commands:"),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: commandButtons.map((button) => Expanded(
|
|
||||||
child: button,
|
|
||||||
)).toList(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFanSpeed(VacuumEntity entity) {
|
|
||||||
if (entity.supportFanSpeed) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
|
||||||
child: ModeSelectorWidget(
|
|
||||||
caption: "Fan speed",
|
|
||||||
options: entity.fanSpeedList,
|
|
||||||
value: entity.fanSpeed,
|
|
||||||
onChange: (val) => ConnectionManager().callService(
|
|
||||||
domain: "vacuum",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
service: "set_fan_speed",
|
|
||||||
data: {"fan_speed": val}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAdditionalInfo(VacuumEntity entity) {
|
|
||||||
List<Widget> rows = [];
|
|
||||||
if (entity.cleanedArea != null) {
|
|
||||||
rows.add(
|
|
||||||
Text("Cleaned area: ${entity.cleanedArea}")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rows.isEmpty) {
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: rows,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
part of '../../../main.dart';
|
|
||||||
|
|
||||||
class VacuumStateButton extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Widget result;
|
|
||||||
VacuumEntity entity = EntityModel.of(context).entityWrapper.entity;
|
|
||||||
if (entity.supportTurnOn && entity.supportTurnOff) {
|
|
||||||
result = FlatServiceButton(
|
|
||||||
serviceDomain: "vacuum",
|
|
||||||
serviceName: entity.state == EntityState.on ? "turn_off" : "turn_on",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
text: entity.state == EntityState.on ? "TURN OFF" : "TURN ON"
|
|
||||||
);
|
|
||||||
} else if (entity.supportStart && (entity.state == EntityState.docked || entity.state == EntityState.idle)) {
|
|
||||||
result = FlatServiceButton(
|
|
||||||
serviceDomain: "vacuum",
|
|
||||||
serviceName: "start",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
text: "START CLEANING"
|
|
||||||
);
|
|
||||||
} else if (entity.supportReturnHome && entity.state == EntityState.cleaning) {
|
|
||||||
result = FlatServiceButton(
|
|
||||||
serviceDomain: "vacuum",
|
|
||||||
serviceName: "return_to_base",
|
|
||||||
entityId: entity.entityId,
|
|
||||||
text: "RETURN TO DOCK"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
result = Text(entity.state.toUpperCase(), style: Theme.of(context).textTheme.subhead);
|
|
||||||
}
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(right: 15),
|
|
||||||
child: result,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +1,55 @@
|
|||||||
part of '../main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class EntityViewPage extends StatefulWidget {
|
class EntityViewPage extends StatefulWidget {
|
||||||
EntityViewPage({Key key, @required this.entityId}) : super(key: key);
|
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
|
||||||
|
|
||||||
final String entityId;
|
final String entityId;
|
||||||
|
final HomeAssistant homeAssistant;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_EntityViewPageState createState() => new _EntityViewPageState();
|
_EntityViewPageState createState() => new _EntityViewPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EntityViewPageState extends State<EntityViewPage> {
|
class _EntityViewPageState extends State<EntityViewPage> {
|
||||||
|
String _title;
|
||||||
StreamSubscription _refreshDataSubscription;
|
StreamSubscription _refreshDataSubscription;
|
||||||
StreamSubscription _stateSubscription;
|
StreamSubscription _stateSubscription;
|
||||||
Entity _entity;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
if (event.entityId == widget.entityId) {
|
if (event.entityId == widget.entityId) {
|
||||||
Logger.d("[Entity page] State change event handled: ${event.entityId}");
|
Logger.d("State change event handled by entity page: ${event.entityId}");
|
||||||
setState(() {
|
setState(() {});
|
||||||
_getEntity();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||||
Logger.d("[Entity page] Refresh data event handled");
|
setState(() {});
|
||||||
setState(() {
|
|
||||||
_getEntity();
|
|
||||||
});
|
});
|
||||||
});
|
_prepareData();
|
||||||
_getEntity();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_getEntity() {
|
void _prepareData() async {
|
||||||
_entity = HomeAssistant().entities?.get(widget.entityId);
|
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String entityNameToDisplay = '${(_entity?.displayName ?? widget.entityId) ?? ''}';
|
|
||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}),
|
}),
|
||||||
title: new Text(entityNameToDisplay),
|
// 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(_title),
|
||||||
|
),
|
||||||
|
body: HomeAssistantModel(
|
||||||
|
homeAssistant: widget.homeAssistant,
|
||||||
|
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
|
||||||
),
|
),
|
||||||
body: _entity == null ? PageLoadingError(
|
|
||||||
errorText: 'Entity is not available $entityNameToDisplay',
|
|
||||||
) : EntityPageLayout(entity: _entity),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class AlarmControlPanelEntity extends Entity {
|
class AlarmControlPanelEntity extends Entity {
|
||||||
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);
|
AlarmControlPanelEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
@ -1,8 +1,7 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class AutomationEntity extends Entity {
|
class AutomationEntity extends Entity {
|
||||||
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);
|
AutomationEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
@ -1,8 +1,7 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class ButtonEntity extends Entity {
|
class ButtonEntity extends Entity {
|
||||||
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
|
ButtonEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildStatePart(BuildContext context) {
|
Widget _buildStatePart(BuildContext context) {
|
@ -1,18 +1,14 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class CameraEntity extends Entity {
|
class CameraEntity extends Entity {
|
||||||
|
|
||||||
static const SUPPORT_ON_OFF = 1;
|
static const SUPPORT_ON_OFF = 1;
|
||||||
static const SUPPORT_STREAM = 2;
|
|
||||||
|
|
||||||
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
CameraEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
bool get supportOnOff => ((supportedFeatures &
|
bool get supportOnOff => ((supportedFeatures &
|
||||||
CameraEntity.SUPPORT_ON_OFF) ==
|
CameraEntity.SUPPORT_ON_OFF) ==
|
||||||
CameraEntity.SUPPORT_ON_OFF);
|
CameraEntity.SUPPORT_ON_OFF);
|
||||||
bool get supportStream => ((supportedFeatures &
|
|
||||||
CameraEntity.SUPPORT_STREAM) ==
|
|
||||||
CameraEntity.SUPPORT_STREAM);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class ClimateEntity extends Entity {
|
class ClimateEntity extends Entity {
|
||||||
|
|
||||||
@ -10,57 +10,69 @@ class ClimateEntity extends Entity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
||||||
static const SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
|
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
|
||||||
static const SUPPORT_TARGET_HUMIDITY = 4;
|
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
|
||||||
static const SUPPORT_FAN_MODE = 8;
|
static const SUPPORT_TARGET_HUMIDITY = 8;
|
||||||
static const SUPPORT_PRESET_MODE = 16;
|
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
|
||||||
static const SUPPORT_SWING_MODE = 32;
|
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
|
||||||
static const SUPPORT_AUX_HEAT = 64;
|
static const SUPPORT_FAN_MODE = 64;
|
||||||
|
static const SUPPORT_OPERATION_MODE = 128;
|
||||||
|
static const SUPPORT_HOLD_MODE = 256;
|
||||||
//static const SUPPORT_OPERATION_MODE = 16;
|
static const SUPPORT_SWING_MODE = 512;
|
||||||
//static const SUPPORT_HOLD_MODE = 256;
|
static const SUPPORT_AWAY_MODE = 1024;
|
||||||
//static const SUPPORT_AWAY_MODE = 1024;
|
static const SUPPORT_AUX_HEAT = 2048;
|
||||||
//static const SUPPORT_ON_OFF = 4096;
|
static const SUPPORT_ON_OFF = 4096;
|
||||||
|
|
||||||
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
|
|
||||||
|
|
||||||
bool get supportTargetTemperature => ((supportedFeatures &
|
bool get supportTargetTemperature => ((supportedFeatures &
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
||||||
bool get supportTargetTemperatureRange => ((supportedFeatures &
|
bool get supportTargetTemperatureHigh => ((supportedFeatures &
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) ==
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
|
||||||
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE);
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
|
||||||
|
bool get supportTargetTemperatureLow => ((supportedFeatures &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
|
||||||
bool get supportTargetHumidity => ((supportedFeatures &
|
bool get supportTargetHumidity => ((supportedFeatures &
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
||||||
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
||||||
|
bool get supportTargetHumidityHigh => ((supportedFeatures &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
|
||||||
|
bool get supportTargetHumidityLow => ((supportedFeatures &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
|
||||||
bool get supportFanMode =>
|
bool get supportFanMode =>
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
|
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||||
ClimateEntity.SUPPORT_FAN_MODE);
|
ClimateEntity.SUPPORT_FAN_MODE);
|
||||||
|
bool get supportOperationMode => ((supportedFeatures &
|
||||||
|
ClimateEntity.SUPPORT_OPERATION_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_OPERATION_MODE);
|
||||||
|
bool get supportHoldMode =>
|
||||||
|
((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_HOLD_MODE);
|
||||||
bool get supportSwingMode =>
|
bool get supportSwingMode =>
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
|
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||||
ClimateEntity.SUPPORT_SWING_MODE);
|
ClimateEntity.SUPPORT_SWING_MODE);
|
||||||
bool get supportPresetMode =>
|
bool get supportAwayMode =>
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
|
((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) ==
|
||||||
ClimateEntity.SUPPORT_PRESET_MODE);
|
ClimateEntity.SUPPORT_AWAY_MODE);
|
||||||
bool get supportAuxHeat =>
|
bool get supportAuxHeat =>
|
||||||
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||||
ClimateEntity.SUPPORT_AUX_HEAT);
|
ClimateEntity.SUPPORT_AUX_HEAT);
|
||||||
|
bool get supportOnOff =>
|
||||||
|
((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) ==
|
||||||
|
ClimateEntity.SUPPORT_ON_OFF);
|
||||||
|
|
||||||
List<String> get hvacModes => attributes["hvac_modes"] != null
|
List<String> get operationList => attributes["operation_list"] != null
|
||||||
? (attributes["hvac_modes"] as List).cast<String>()
|
? (attributes["operation_list"] as List).cast<String>()
|
||||||
: null;
|
: null;
|
||||||
List<String> get fanModes => attributes["fan_modes"] != null
|
List<String> get fanList => attributes["fan_list"] != null
|
||||||
? (attributes["fan_modes"] as List).cast<String>()
|
? (attributes["fan_list"] as List).cast<String>()
|
||||||
: null;
|
: null;
|
||||||
List<String> get presetModes => attributes["preset_modes"] != null
|
List<String> get swingList => attributes["swing_list"] != null
|
||||||
? (attributes["preset_modes"] as List).cast<String>()
|
? (attributes["swing_list"] as List).cast<String>()
|
||||||
: null;
|
|
||||||
List<String> get swingModes => attributes["swing_modes"] != null
|
|
||||||
? (attributes["swing_modes"] as List).cast<String>()
|
|
||||||
: null;
|
: null;
|
||||||
double get temperature => _getDoubleAttributeValue('temperature');
|
double get temperature => _getDoubleAttributeValue('temperature');
|
||||||
double get currentTemperature => _getDoubleAttributeValue('current_temperature');
|
|
||||||
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
||||||
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
||||||
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
||||||
@ -69,22 +81,25 @@ class ClimateEntity extends Entity {
|
|||||||
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||||
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||||
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
|
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
|
||||||
String get hvacAction => attributes['hvac_action'];
|
String get operationMode => attributes['operation_mode'];
|
||||||
String get fanMode => attributes['fan_mode'];
|
String get fanMode => attributes['fan_mode'];
|
||||||
String get presetMode => attributes['preset_mode'];
|
|
||||||
String get swingMode => attributes['swing_mode'];
|
String get swingMode => attributes['swing_mode'];
|
||||||
bool get awayMode => attributes['away_mode'] == "on";
|
bool get awayMode => attributes['away_mode'] == "on";
|
||||||
//bool get isOff => state == EntityState.off;
|
bool get isOff => state == EntityState.off;
|
||||||
bool get auxHeat => attributes['aux_heat'] == "on";
|
bool get auxHeat => attributes['aux_heat'] == "on";
|
||||||
|
|
||||||
|
ClimateEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void update(Map rawData, String webHost) {
|
void update(Map rawData) {
|
||||||
super.update(rawData, webHost);
|
super.update(rawData);
|
||||||
if (supportTargetTemperature) {
|
if (supportTargetTemperature) {
|
||||||
historyConfig.numericAttributesToShow.add("temperature");
|
historyConfig.numericAttributesToShow.add("temperature");
|
||||||
}
|
}
|
||||||
if (supportTargetTemperatureRange) {
|
if (supportTargetTemperatureHigh) {
|
||||||
historyConfig.numericAttributesToShow.add("target_temp_high");
|
historyConfig.numericAttributesToShow.add("target_temp_high");
|
||||||
|
}
|
||||||
|
if (supportTargetTemperatureLow) {
|
||||||
historyConfig.numericAttributesToShow.add("target_temp_low");
|
historyConfig.numericAttributesToShow.add("target_temp_low");
|
||||||
}
|
}
|
||||||
}
|
}
|