Compare commits

..

3 Commits
1.2.0 ... 0.5.4

Author SHA1 Message Date
5211d1ff46 v. 0.5.4 2019-05-09 14:15:21 +03:00
d1c9fddba6 v. 0.5.4 2019-05-09 13:17:30 +03:00
14cc55a2c7 Resolves #373 2019-05-09 13:16:35 +03:00
208 changed files with 8259 additions and 15157 deletions

View File

@ -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]

View File

@ -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:**

View File

@ -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.

View File

@ -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:**

View File

@ -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
View File

@ -9,15 +9,5 @@ build/
.flutter-plugins
.idea/
.vscode/
.theia/
.project/
.settings/
flutter_export_environment.sh
.flutter-plugins-dependencies
key.properties
.secrets.dart
pubspec.lock
google-services.json

View File

@ -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"

View File

@ -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==

View File

@ -1,18 +1,12 @@
[![flutter](https://somegeeky.website/assets/badges/flutter_badge_v3.svg)](https://somegeeky.website/badges/flutter) [![dart](https://somegeeky.website/assets/badges/dart_badge_v3.svg)](https://somegeeky.website/badges/dart)
# HA Client
## 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
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
#### Last release build status
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](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)
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)

1
android/.gitignore vendored
View File

@ -8,4 +8,3 @@
/build
/captures
GeneratedPluginRegistrant.java
.project/

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -50,14 +50,6 @@ android {
}
signingConfigs {
if (!System.getenv()["CI"]) {
debug {
keyAlias keystoreProperties['debugKeyAlias']
keyPassword keystoreProperties['debugKeyPassword']
storeFile file(keystoreProperties['debugStoreFile'])
storePassword keystoreProperties['debugStorePassword']
}
}
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
@ -78,13 +70,7 @@ flutter {
}
dependencies {
implementation 'com.google.firebase:firebase-analytics:17.2.2'
implementation 'com.google.firebase:firebase-messaging:20.2.0'
implementation 'androidx.work:work-runtime:2.3.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
apply plugin: 'io.fabric'
apply plugin: 'com.google.gms.google-services'

View File

@ -1,15 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keyboardcrumbs.hassclient"
android:installLocation="auto">
package="com.keyboardcrumbs.hassclient">
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
<!-- The INTERNET permission is required for development. Specifically,
flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.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
calls FlutterMain.startInitialization(this); in its onCreate method.
@ -17,19 +13,9 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="HA Client"
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" />
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
@ -37,50 +23,17 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".MessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<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>
<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>
</manifest>

View File

@ -1,71 +1,13 @@
package com.keyboardcrumbs.hassclient;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.ConnectionResult;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;
import com.google.firebase.messaging.FirebaseMessaging;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
Context context = getActivity();
if (call.method.equals("getFCMToken")) {
if (checkPlayServices()) {
FirebaseInstanceId.getInstance().getInstanceId()
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
@Override
public void onComplete(@NonNull Task<InstanceIdResult> task) {
if (task.isSuccessful()) {
String token = task.getResult().getToken();
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
updateTokenTask.execute(token);
result.success(token);
} else {
result.error("fcm_error", task.getException().getMessage(), null);
}
}
});
} else {
result.error("google_play_service_error", "Google Play Services unavailable", null);
}
}
}
}
);
}
private boolean checkPlayServices() {
return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
}
}

View File

@ -1,152 +0,0 @@
package com.keyboardcrumbs.hassclient;
import java.util.Map;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
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.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.webkit.URLUtil;
public class MessagingService extends FirebaseMessagingService {
private static final String TAG = "MessagingService";
@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(String token) {
UpdateTokenTask updateTokenTask = new UpdateTokenTask(this);
updateTokenTask.execute(token);
}
private void sendNotification(Map<String, String> data) {
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
boolean autoCancel;
if (!data.containsKey("channelId")) {
channelId = "ha_notify";
channelDescription = "Default notification channel";
} else {
channelId = data.get("channelId");
channelDescription = channelId;
}
if (!data.containsKey("body")) {
messageBody = "";
} else {
messageBody = data.get("body");
}
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);
if (URLUtil.isValidUrl(imageUrl)) {
Bitmap image = getBitmapFromURL(imageUrl);
if (image != null) {
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
notificationBuilder.setLargeIcon(image);
}
}
for (int i = 1; i <= 3; i++) {
if (data.containsKey("action" + i)) {
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class);
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.mini_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;
}
}
}

View File

@ -1,51 +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.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import java.util.concurrent.TimeUnit;
public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "NextAlarmReceiver";
@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();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(UpdateNextAlarmWorker.class)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.SECONDS)
.setConstraints(constraints)
.build();
WorkManager
.getInstance(context)
.enqueueUniqueWork("NextAlarmUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
}
}

View File

@ -1,73 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.Context;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.app.NotificationManager;
import android.webkit.URLUtil;
import org.json.JSONObject;
import android.content.SharedPreferences;
public class NotificationActionReceiver extends BroadcastReceiver {
private static final String TAG = "NotificationAction";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
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);
}
SharedPreferences prefs = context.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;
JSONObject actionData = new JSONObject(rawActionData);
if (URLUtil.isValidUrl(requestUrl)) {
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);
String stringRequest = dataToSend.toString();
SendTask sendTask = new SendTask();
sendTask.execute(requestUrl, stringRequest);
} else {
Log.w(TAG, "Invalid HA url");
}
} catch (Exception e) {
Log.e(TAG, "Error handling notification action", e);
}
} else {
Log.w(TAG, "Webhook id not found");
}
}
}

View File

@ -1,46 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.util.Log;
import android.os.AsyncTask;
import java.net.URL;
import java.net.HttpURLConnection;
import java.io.OutputStream;
public class SendTask extends AsyncTask<String, String, String> {
private static final String TAG = "SendTask";
public SendTask(){
//set context variables if required
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(String... params) {
String urlString = params[0];
String data = params[1];
try {
URL url = new URL(urlString);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
byte[] outputBytes = data.getBytes("UTF-8");
OutputStream os = urlConnection.getOutputStream();
os.write(outputBytes);
int responseCode = urlConnection.getResponseCode();
urlConnection.disconnect();
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);
}
return null;
}
}

View File

@ -1,119 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.Context;
import android.content.SharedPreferences;
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.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
public class UpdateNextAlarmWorker extends Worker {
private Context currentContext;
private static final String TAG = "NextAlarmWorker";
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 UpdateNextAlarmWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
currentContext = context;
}
@NonNull
@Override
public Result doWork() {
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();
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;
JSONObject dataToSend = new JSONObject();
if (URLUtil.isValidUrl(requestUrl)) {
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);
sensorData.put("state", DATE_TIME_FORMAT.format(calendar.getTime()));
sensorAttrs.put("date", DATE_FORMAT.format(calendar.getTime()));
sensorAttrs.put("time", TIME_FORMAT.format(calendar.getTime()));
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);
String stringRequest = dataToSend.toString();
try {
URL url = new URL(requestUrl);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
byte[] outputBytes = stringRequest.getBytes("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 setting next alarm", e);
return Result.failure();
}
} else {
Log.w(TAG, "Webhook id not found");
return Result.failure();
}
return Result.success();
}
}

View File

@ -1,46 +0,0 @@
package com.keyboardcrumbs.hassclient;
import android.util.Log;
import android.os.AsyncTask;
import java.net.URL;
import java.net.HttpURLConnection;
import java.io.OutputStream;
import android.webkit.URLUtil;
import org.json.JSONObject;
import android.content.SharedPreferences;
import android.content.Context;
import java.lang.ref.WeakReference;
public class UpdateTokenTask extends AsyncTask<String, String, String> {
private static final String TAG = "UpdateTokenTask";
private WeakReference<Context> contextRef;
public UpdateTokenTask(Context context){
contextRef = new WeakReference<>(context);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(String... params) {
Log.d(TAG, "Updating push token");
Context context = contextRef.get();
if (context != null) {
String token = params[0];
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("flutter.npush-token", token);
editor.commit();
}
return null;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

View File

@ -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>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<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 -->
<!-- <item>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 B

View File

@ -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>

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,12 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="main_color">#709AC1</color>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>

View File

@ -2,15 +2,10 @@ buildscript {
repositories {
google()
jcenter()
maven {
url 'https://maven.fabric.io/public'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'io.fabric.tools:gradle:1.26.1'
classpath 'com.android.tools.build:gradle:3.1.2'
}
}
@ -18,9 +13,6 @@ allprojects {
repositories {
google()
jcenter()
maven {
url 'https://maven.fabric.io/public'
}
}
}

View File

@ -1,4 +1,5 @@
org.gradle.jvmargs=-Xmx512m
org.gradle.jvmargs=-Xmx2g
org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true
android.enableJetifier=false

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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

View File

@ -1 +0,0 @@
include ':app'

View File

@ -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>

View File

@ -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>

View File

@ -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');
}
};
*/

View File

@ -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!
```

View File

@ -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!
```

View File

@ -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!
```

Binary file not shown.

Binary file not shown.

BIN
images/hassio-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
images/icon/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -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
)
)
);
}
}

View File

@ -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(),
);
}
}

View File

@ -1,682 +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.MAP:
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.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 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';
}
}

View File

@ -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
)
),
)
);
}
}

View File

@ -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);
}
}

View File

@ -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)
],
),
)
);
}
}

View File

@ -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);
}

View File

@ -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),
);
}
}

View File

@ -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,);
}
}

View File

@ -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
)
);
}
}

View File

@ -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,
)
],
),
)
);
}
}

View File

@ -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()
)
);
}
}

View File

@ -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);
}
}

View File

@ -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,);
}
}

View File

@ -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
),
);
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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(),
)
],
)
],
);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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,
);
}
}

View File

@ -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"];
}
}
}
}
}

View File

@ -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,
);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -1,5 +0,0 @@
part of '../../main.dart';
class SunEntity extends Entity {
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
}

View File

@ -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,
),
);
}
}

View File

@ -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();
}
}

View File

@ -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,
),
);
}
}

View File

@ -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,
);
}
}

View File

@ -1,56 +1,55 @@
part of '../main.dart';
part of 'main.dart';
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 HomeAssistant homeAssistant;
@override
_EntityViewPageState createState() => new _EntityViewPageState();
}
class _EntityViewPageState extends State<EntityViewPage> {
String _title;
StreamSubscription _refreshDataSubscription;
StreamSubscription _stateSubscription;
Entity _entity;
@override
void initState() {
super.initState();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId == widget.entityId) {
Logger.d("[Entity page] State change event handled: ${event.entityId}");
setState(() {
_getEntity();
});
Logger.d("State change event handled by entity page: ${event.entityId}");
setState(() {});
}
});
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
Logger.d("[Entity page] Refresh data event handled");
setState(() {
_getEntity();
setState(() {});
});
});
_getEntity();
_prepareData();
}
_getEntity() {
_entity = HomeAssistant().entities?.get(widget.entityId);
void _prepareData() async {
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
}
@override
Widget build(BuildContext context) {
String entityNameToDisplay = '${(_entity?.displayName ?? widget.entityId) ?? ''}';
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
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),
);
}

View File

@ -1,8 +1,7 @@
part of '../../main.dart';
part of '../main.dart';
class AlarmControlPanelEntity extends Entity {
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);
AlarmControlPanelEntity(Map rawData) : super(rawData);
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {

View File

@ -1,8 +1,7 @@
part of '../../main.dart';
part of '../main.dart';
class AutomationEntity extends Entity {
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);
AutomationEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {

View File

@ -1,8 +1,7 @@
part of '../../main.dart';
part of '../main.dart';
class ButtonEntity extends Entity {
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
ButtonEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {

View File

@ -1,18 +1,14 @@
part of '../../main.dart';
part of '../main.dart';
class CameraEntity extends Entity {
static const SUPPORT_ON_OFF = 1;
static const SUPPORT_STREAM = 2;
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
CameraEntity(Map rawData) : super(rawData);
bool get supportOnOff => ((supportedFeatures &
CameraEntity.SUPPORT_ON_OFF) ==
CameraEntity.SUPPORT_ON_OFF);
bool get supportStream => ((supportedFeatures &
CameraEntity.SUPPORT_STREAM) ==
CameraEntity.SUPPORT_STREAM);
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../main.dart';
class ClimateEntity extends Entity {
@ -10,57 +10,69 @@ class ClimateEntity extends Entity {
);
static const SUPPORT_TARGET_TEMPERATURE = 1;
static const SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
static const SUPPORT_TARGET_HUMIDITY = 4;
static const SUPPORT_FAN_MODE = 8;
static const SUPPORT_PRESET_MODE = 16;
static const SUPPORT_SWING_MODE = 32;
static const SUPPORT_AUX_HEAT = 64;
//static const SUPPORT_OPERATION_MODE = 16;
//static const SUPPORT_HOLD_MODE = 256;
//static const SUPPORT_AWAY_MODE = 1024;
//static const SUPPORT_ON_OFF = 4096;
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
static const SUPPORT_TARGET_HUMIDITY = 8;
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
static const SUPPORT_FAN_MODE = 64;
static const SUPPORT_OPERATION_MODE = 128;
static const SUPPORT_HOLD_MODE = 256;
static const SUPPORT_SWING_MODE = 512;
static const SUPPORT_AWAY_MODE = 1024;
static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096;
bool get supportTargetTemperature => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
bool get supportTargetTemperatureRange => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE);
bool get supportTargetTemperatureHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
bool get supportTargetTemperatureLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
bool get supportTargetHumidity => ((supportedFeatures &
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 =>
((supportedFeatures & 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 =>
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE);
bool get supportPresetMode =>
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
ClimateEntity.SUPPORT_PRESET_MODE);
bool get supportAwayMode =>
((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) ==
ClimateEntity.SUPPORT_AWAY_MODE);
bool get supportAuxHeat =>
((supportedFeatures & 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
? (attributes["hvac_modes"] as List).cast<String>()
List<String> get operationList => attributes["operation_list"] != null
? (attributes["operation_list"] as List).cast<String>()
: null;
List<String> get fanModes => attributes["fan_modes"] != null
? (attributes["fan_modes"] as List).cast<String>()
List<String> get fanList => attributes["fan_list"] != null
? (attributes["fan_list"] as List).cast<String>()
: null;
List<String> get presetModes => attributes["preset_modes"] != null
? (attributes["preset_modes"] as List).cast<String>()
: null;
List<String> get swingModes => attributes["swing_modes"] != null
? (attributes["swing_modes"] as List).cast<String>()
List<String> get swingList => attributes["swing_list"] != null
? (attributes["swing_list"] as List).cast<String>()
: null;
double get temperature => _getDoubleAttributeValue('temperature');
double get currentTemperature => _getDoubleAttributeValue('current_temperature');
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
double get targetLow => _getDoubleAttributeValue('target_temp_low');
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
@ -69,22 +81,25 @@ class ClimateEntity extends Entity {
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
double get minHumidity => _getDoubleAttributeValue('min_humidity');
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 presetMode => attributes['preset_mode'];
String get swingMode => attributes['swing_mode'];
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";
ClimateEntity(Map rawData) : super(rawData);
@override
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
void update(Map rawData) {
super.update(rawData);
if (supportTargetTemperature) {
historyConfig.numericAttributesToShow.add("temperature");
}
if (supportTargetTemperatureRange) {
if (supportTargetTemperatureHigh) {
historyConfig.numericAttributesToShow.add("target_temp_high");
}
if (supportTargetTemperatureLow) {
historyConfig.numericAttributesToShow.add("target_temp_low");
}
}

View File

@ -0,0 +1,99 @@
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';
}
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.none;
String holdNavigationPath;
String holdService;
Map<String, dynamic> holdServiceData;
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"];
}
}
}
}
}
class CardType {
static const horizontalStack = "horizontal-stack";
static const verticalStack = "vertical-stack";
static const entities = "entities";
static const glance = "glance";
static const mediaControl = "media-control";
static const weatherForecast = "weather-forecast";
static const thermostat = "thermostat";
static const sensor = "sensor";
static const plantStatus = "plant-status";
static const pictureEntity = "picture-entity";
static const pictureElements = "picture-elements";
static const picture = "picture";
static const map = "map";
static const iframe = "iframe";
static const gauge = "gauge";
static const entityButton = "entity-button";
static const conditional = "conditional";
static const alarmPanel = "alarm-panel";
static const markdown = "markdown";
}

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../main.dart';
class CoverEntity extends Entity {
@ -11,8 +11,6 @@ class CoverEntity extends Entity {
static const SUPPORT_STOP_TILT = 64;
static const SUPPORT_SET_TILT_POSITION = 128;
CoverEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportOpen => ((supportedFeatures &
CoverEntity.SUPPORT_OPEN) ==
CoverEntity.SUPPORT_OPEN);
@ -40,13 +38,15 @@ class CoverEntity extends Entity {
CoverEntity.SUPPORT_SET_TILT_POSITION);
double get currentPosition => _getDoubleAttributeValue('current_position') ?? 0;
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position') ?? 0;
double get currentPosition => _getDoubleAttributeValue('current_position');
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0);
bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed));
bool get canTiltBeOpened => currentTiltPosition < 100;
bool get canTiltBeClosed => currentTiltPosition > 0;
CoverEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return CoverStateWidget();

View File

@ -1,8 +1,6 @@
part of '../../main.dart';
part of '../main.dart';
class DateTimeEntity extends Entity {
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get hasDate => attributes["has_date"] ?? false;
bool get hasTime => attributes["has_time"] ?? false;
int get year => attributes["year"] ?? 1970;
@ -14,6 +12,8 @@ class DateTimeEntity extends Entity {
String get formattedState => _getFormattedState();
DateTime get dateTimeState => _getDateTimeState();
DateTimeEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return DateTimeStateWidget();
@ -35,7 +35,8 @@ class DateTimeEntity extends Entity {
return formattedState;
}
void setNewState(Map newValue) {
ConnectionManager().callService(domain: domain, service: "set_datetime", entityId: entityId, data: newValue);
void setNewState(newValue) {
eventBus
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
}
}

View File

@ -1,6 +1,13 @@
part of '../main.dart';
enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink}
class StatelessEntityType {
static const NONE = 0;
static const MISSED = 1;
static const DIVIDER = 2;
static const SECTION = 3;
static const CALL_SERVICE = 4;
static const WEBLINK = 5;
}
class Entity {
@ -20,7 +27,7 @@ class Entity {
"cold.on": "Cold",
"cold.off": "Normal",
"connectivity.on": "Connected",
"connectivity.off": "Disconnected",
"connectivity.off": "Diconnected",
"door.on": "Open",
"door.off": "Closed",
"garage_door.on": "Open",
@ -66,11 +73,10 @@ class Entity {
Map attributes;
String domain;
String entityId;
String entityPicture;
String state;
String displayState;
DateTime lastUpdatedTimestamp;
StatelessEntityType statelessType = StatelessEntityType.none;
DateTime _lastUpdated;
int statelessType = 0;
List<Entity> childEntities = [];
String deviceClass;
@ -78,21 +84,8 @@ class Entity {
chartType: EntityHistoryWidgetType.simple
);
String get displayName {
if (attributes.containsKey('friendly_name')) {
return attributes['friendly_name'];
}
if (attributes.containsKey('name')) {
return attributes['name'];
}
if (entityId == null) {
return "";
}
if (entityId.contains(".")) {
return entityId.split(".")[1].replaceAll("_", " ");
}
return entityId;
}
String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
bool get isView =>
(domain == "group") &&
@ -101,6 +94,7 @@ class Entity {
bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? "";
bool get isOn => state == EntityState.on;
String get entityPicture => _getEntityPictureUrl();
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted();
@ -108,66 +102,60 @@ class Entity {
double get doubleState => double.tryParse(state) ?? 0.0;
int get supportedFeatures => attributes["supported_features"] ?? 0;
String _getEntityPictureUrl(String webHost) {
String _getEntityPictureUrl() {
String result = attributes["entity_picture"];
if (result == null) return result;
if (!result.startsWith("http")) {
if (result.startsWith("/")) {
result = "$webHost$result";
result = "$homeAssistantWebHost$result";
} else {
result = "$webHost/$result";
result = "$homeAssistantWebHost/$result";
}
}
return result;
}
Entity(Map rawData, String webHost) {
update(rawData, webHost);
Entity(Map rawData) {
update(rawData);
}
Entity.missed(String entityId) {
statelessType = StatelessEntityType.missed;
statelessType = StatelessEntityType.MISSED;
attributes = {"hidden": false};
this.entityId = entityId;
}
Entity.divider() {
statelessType = StatelessEntityType.divider;
statelessType = StatelessEntityType.DIVIDER;
attributes = {"hidden": false};
}
Entity.section(String label) {
statelessType = StatelessEntityType.section;
statelessType = StatelessEntityType.SECTION;
attributes = {"hidden": false, "friendly_name": "$label"};
}
Entity.ghost(String name, String icon) {
statelessType = StatelessEntityType.ghost;
attributes = {"icon": icon, "hidden": false, "friendly_name": name};
}
Entity.callService({String icon, String name, String service, String actionName}) {
statelessType = StatelessEntityType.callService;
statelessType = StatelessEntityType.CALL_SERVICE;
entityId = service;
displayState = actionName?.toUpperCase() ?? "RUN";
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
}
Entity.weblink({String url, String name, String icon}) {
statelessType = StatelessEntityType.webLink;
entityId = "custom.custom";
statelessType = StatelessEntityType.WEBLINK;
entityId = "custom.custom"; //TODO wtf??
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
}
void update(Map rawData, String webHost) {
void update(Map rawData) {
attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"] != null ? rawData["entity_id"].split(".")[0] : null;
domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
}
double _getDoubleAttributeValue(String attributeName) {
@ -222,19 +210,52 @@ class Entity {
);
}
Widget buildEntityPageWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding),
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
),
LastUpdatedWidget(),
Divider(),
_buildAdditionalControlsForPage(context),
Divider(),
buildHistoryWidget(),
EntityAttributesList()
]),
handleTap: false,
);
}
Widget buildHistoryWidget() {
return EntityHistoryWidget(
config: historyConfig,
);
}
Widget buildBadgeWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: BadgeWidget(),
handleTap: true,
);
}
String getAttribute(String attributeName) {
if (attributes != null) {
return attributes["$attributeName"].toString();
return attributes["$attributeName"];
}
return null;
}
String _getLastUpdatedFormatted() {
if (lastUpdatedTimestamp == null) {
if (_lastUpdated == null) {
return "-";
} else {
DateTime now = DateTime.now();
Duration d = now.difference(lastUpdatedTimestamp);
Duration d = now.difference(_lastUpdated);
String text;
int v;
if (d.inDays == 0) {

View File

@ -0,0 +1,114 @@
part of '../main.dart';
class EntityWrapper {
String displayName;
String icon;
String entityPicture;
EntityUIAction uiAction;
Entity entity;
EntityWrapper({
this.entity,
String icon,
String displayName,
this.uiAction
}) {
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
this.icon = icon ?? entity.icon;
if (icon == null) {
entityPicture = entity.entityPicture;
}
this.displayName = displayName ?? entity.displayName;
if (uiAction == null) {
uiAction = EntityUIAction();
}
}
}
void handleTap() {
switch (uiAction.tapAction) {
case EntityUIAction.toggle: {
eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break;
}
case EntityUIAction.callService: {
if (uiAction.tapService != null) {
eventBus.fire(
ServiceCallEvent(uiAction.tapService.split(".")[0],
uiAction.tapService.split(".")[1], null,
uiAction.tapServiceData));
}
break;
}
case EntityUIAction.none: {
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.tapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.tapService);
}
break;
}
default: {
break;
}
}
}
void handleHold() {
switch (uiAction.holdAction) {
case EntityUIAction.toggle: {
eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break;
}
case EntityUIAction.callService: {
if (uiAction.holdService != null) {
eventBus.fire(
ServiceCallEvent(uiAction.holdService.split(".")[0],
uiAction.holdService.split(".")[1], null,
uiAction.holdServiceData));
}
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.holdService);
}
break;
}
default: {
break;
}
}
}
}

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../main.dart';
class FanEntity extends Entity {
@ -6,7 +6,7 @@ class FanEntity extends Entity {
static const SUPPORT_OSCILLATE = 2;
static const SUPPORT_DIRECTION = 4;
FanEntity(Map rawData, String webHost) : super(rawData, webHost);
FanEntity(Map rawData) : super(rawData);
bool get supportSetSpeed => ((supportedFeatures &
FanEntity.SUPPORT_SET_SPEED) ==

Some files were not shown because too many files have changed in this diff Show More