Compare commits

..

1 Commits

Author SHA1 Message Date
3234ffc20c build 713 2019-10-29 18:14:56 +00:00
151 changed files with 5516 additions and 5188 deletions

View File

@ -7,16 +7,35 @@ assignees: ''
--- ---
**HA Client version:** [Main menu -> About HA Client] <!--
Please provide as much information as possible.
-->
**HA Client version:** <!-- Main app menu => About HA Client -->
**Home Assistant version:** **Home Assistant version:** <!-- 0.94.1 for example -->
**Device name:** **Device name:** <!-- Pixel 2 for example -->
**Android version:** **Android version:** <!-- 8.1 for example -->
**Connection type:** <!-- For example "Local IP" or "Remote UI" or "Own domain"-->
**Login type:** <!-- For example "HA Login" or "Manual token"-->
**Description** **Description**
[Replace with description] <!--
Describe your issue here
-->
**Screenshots** **Screenshots**
[Replace with screenshots] <!--
Please provide screenshots if it is a UI issue. Also you can attach screenshot from Home Assistant web UI as an expected result
-->
**Logs**
<!--
Right after issue reproduced go to app menu and tap "Log". Copy log with a "Copy" button in the upper-right corner and post it below
-->
```
[Replace this text with your logs]
```

6
.gitignore vendored
View File

@ -15,9 +15,7 @@ build/
.settings/ .settings/
flutter_export_environment.sh flutter_export_environment.sh
.flutter-plugins-dependencies
key.properties key.properties
.secrets.dart premium_features_manager.class.dart
pubspec.lock pubspec.lock
google-services.json

View File

@ -4,5 +4,9 @@ ENV ANDROID_HOME=/workspace/android-sdk \
FLUTTER_ROOT=/workspace/flutter \ FLUTTER_ROOT=/workspace/flutter \
FLUTTER_HOME=/workspace/flutter FLUTTER_HOME=/workspace/flutter
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \ USER root
&& sdk install java 8.0.242.j9-adpt"
RUN apt-get update && \
apt-get -y install build-essential libkrb5-dev gcc make gradle openjdk-8-jdk && \
apt-get clean && \
apt-get -y autoremove

View File

@ -3,12 +3,12 @@ image:
tasks: tasks:
- before: | - before: |
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
mkdir -p /home/gitpod/.android mkdir -p /home/gitpod/.android
touch /home/gitpod/.android/repositories.cfg touch /home/gitpod/.android/repositories.cfg
init: | init: |
echo "Installing Flutter SDK..." echo "Installing Flutter SDK..."
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.12.13+hotfix.7-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.9.1+hotfix.4-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
echo "Installing Android SDK..." echo "Installing Android SDK..."
mkdir -p /workspace/android-sdk && cd /workspace/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip mkdir -p /workspace/android-sdk && cd /workspace/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip
/workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3" /workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
@ -18,6 +18,7 @@ tasks:
flutter doctor --android-licenses flutter doctor --android-licenses
flutter pub get flutter pub get
command: | command: |
flutter pub upgrade
echo "Ready to go!" echo "Ready to go!"
flutter doctor flutter doctor
vscode: vscode:

View File

@ -1,18 +1,15 @@
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
# HA Client # HA Client
## Native Android client for Home Assistant ## Native Android client for Home Assistant
### With notifications and Lovelace UI support ### With notifications and Lovelace UI support
Visit [ha-client.app](http://ha-client.app/) for more info. Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android) Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn)
[![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)
#### Stable CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/latest_build)
#### Beta CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
#### Pre-release CI build #### Pre-release CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build) [![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
#### Beta CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)

View File

@ -78,11 +78,10 @@ flutter {
} }
dependencies { dependencies {
implementation 'com.google.firebase:firebase-analytics:17.2.2' implementation 'com.google.firebase:firebase-core:16.0.8'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
} }
apply plugin: 'io.fabric'
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'

View File

@ -0,0 +1,64 @@
{
"project_info": {
"project_number": "441874387819",
"firebase_url": "https://ha-client-c73c4.firebaseio.com",
"project_id": "ha-client-c73c4",
"storage_bucket": "ha-client-c73c4.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45",
"android_client_info": {
"package_name": "com.keyboardcrumbs.haclient"
}
},
"oauth_client": [
{
"client_id": "441874387819-uqmkibhf361828od1982o2jhl0n3m0ov.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.keyboardcrumbs.haclient",
"certificate_hash": "bebe4d970fbebf0bff2c93244fdc7fcbcefb3470"
}
},
{
"client_id": "441874387819-5q7vmimci4s2jl3v0ncugv1ocp4m48nb.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.keyboardcrumbs.haclient",
"certificate_hash": "0ea12348468be44bc2aa5792ee7e8924c633da81"
}
},
{
"client_id": "441874387819-joi8plo5345ebt8i1dug27u2aenv5tg7.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.keyboardcrumbs.haclient",
"certificate_hash": "fcbc805d965ccf6a4d5417398d191edc9c9890b0"
}
},
{
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -3,29 +3,29 @@
<uses-feature android:name="android.hardware.touchscreen" <uses-feature android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<!-- The INTERNET permission is required for development. Specifically,
flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- io.flutter.app.FlutterApplication is an android.app.Application that <!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method. calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. --> FlutterApplication and put your custom class here. -->
<application <application
android:name="io.flutter.app.FlutterApplication" android:name=".Application"
android:label="HA Client" android:label="HA Client"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id" android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="ha_notify" /> android:value="ha_notify" />
@ -37,12 +37,13 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme).
<meta-data <meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable" android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:resource="@drawable/launch_background" /> android:value="true" />-->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter> <intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" /> <action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -51,6 +52,14 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="haclient"
android:host="auth" />
</intent-filter>
</activity> </activity>
<service <service

View File

@ -0,0 +1,20 @@
package com.keyboardcrumbs.hassclient;
import io.flutter.app.FlutterApplication;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
import io.flutter.plugins.GeneratedPluginRegistrant;
import be.tramckrijte.workmanager.WorkmanagerPlugin;
public class Application extends FlutterApplication implements PluginRegistrantCallback {
@Override
public void onCreate() {
super.onCreate();
WorkmanagerPlugin.setPluginRegistrantCallback(this);
}
@Override
public void registerWith(PluginRegistry registry) {
GeneratedPluginRegistrant.registerWith(registry);
}
}

View File

@ -1,15 +1,15 @@
package com.keyboardcrumbs.hassclient; package com.keyboardcrumbs.hassclient;
import androidx.annotation.NonNull; import android.os.Bundle;
import io.flutter.embedding.android.FlutterActivity; import io.flutter.app.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant; import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.share.FlutterShareReceiverActivity;
public class MainActivity extends FlutterActivity { public class MainActivity extends FlutterShareReceiverActivity {
@Override @Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { protected void onCreate(Bundle savedInstanceState) {
GeneratedPluginRegistrant.registerWith(flutterEngine); super.onCreate(savedInstanceState);
} GeneratedPluginRegistrant.registerWith(this);
}
} }

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"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen --> <!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/main_color" /> <item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here --> <!-- You can insert your own image assets here -->
<!-- <item> <!-- <item>

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

View File

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

View File

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

1232
android/hs_err_pid766.log Normal file

File diff suppressed because it is too large Load Diff

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

@ -13,23 +13,4 @@ window.externalApp.getExternalAuth = function(options) {
window[options.callback](true, responseData); window[options.callback](true, responseData);
}, 500); }, 500);
} }
};
window.externalApp.externalBus = function(message) {
console.log("External bus message: " + message);
var messageObj = JSON.parse(message);
if (messageObj.type == "config/get") {
var responseData = {
id: messageObj.id,
type: "result",
success: true,
result: {
hasSettingsScreen: true
}
};
setTimeout(function(){
window.externalBus(responseData);
}, 500);
} else if (messageObj.type == "config_screen/show") {
HAClient.postMessage('show-settings');
}
}; };

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,548 +1,64 @@
part of '../main.dart'; part of '../main.dart';
class CardData { class HACard {
String type;
List<EntityWrapper> entities = []; List<EntityWrapper> entities = [];
List conditions; List<HACard> childCards = [];
EntityWrapper linkedEntityWrapper;
String name;
String id;
String type;
bool showName;
bool showState;
bool showEmpty; bool showEmpty;
int columnsCount;
List stateFilter; List stateFilter;
bool stateColor = true; List states;
List conditions;
String content;
String unit;
int min;
int max;
Map severity;
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null; HACard({
this.name,
factory CardData.parse(rawData) { this.id,
try { this.linkedEntityWrapper,
switch (rawData['type']) { this.columnsCount: 4,
case CardType.ENTITIES: this.showName: true,
return EntitiesCardData(rawData); this.showState: true,
break; this.stateFilter: const [],
case CardType.ALARM_PANEL: this.showEmpty: true,
return AlarmPanelCardData(rawData); this.content,
break; this.states,
case CardType.BUTTON: this.conditions: const [],
return ButtonCardData(rawData); this.unit,
break; this.min,
case CardType.ENTITY_BUTTON: this.max,
return ButtonCardData(rawData); this.severity,
break; @required this.type
case CardType.CONDITIONAL: }) {
return CardData.parse(rawData['card']); if (this.columnsCount <= 0) {
break; this.columnsCount = 4;
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:
return GlanceCardData(rawData);
break;
case CardType.HORIZONTAL_STACK:
return HorizontalStackCardData(rawData);
break;
case CardType.VERTICAL_STACK:
return VerticalStackCardData(rawData);
break;
case CardType.MARKDOWN:
return MarkdownCardData(rawData);
break;
case CardType.MEDIA_CONTROL:
return MediaControlCardData(rawData);
break;
default:
if (rawData.containsKey('entities')) {
return EntitiesCardData(rawData);
} else if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
return EntitiesCardData(rawData);
}
return CardData(rawData);
}
} 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'] ?? CardType.ENTITIES;
conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true;
stateFilter = rawData['state_filter'] ?? [];
} else {
type = CardType.UNKNOWN;
conditions = [];
showEmpty = true;
stateFilter = [];
}
}
Widget buildCardWidget() {
return UnsupportedCard(card: this);
}
List<EntityWrapper> getEntitiesToShow() { List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) { return entities.where((entityWrapper) {
if (entityWrapper.entity.isHidden) { if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) {
return false; return false;
} }
List currentStateFilter; if (stateFilter.isNotEmpty) {
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) { return stateFilter.contains(entityWrapper.entity.state);
currentStateFilter = entityWrapper.stateFilter;
} else {
currentStateFilter = stateFilter;
} }
bool showByFilter = currentStateFilter.isEmpty; return true;
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(); }).toList();
} }
} Widget build(BuildContext context) {
return CardWidget(
class EntitiesCardData extends CardData { card: this,
);
String title;
String icon;
bool showHeaderToggle;
@override
Widget buildCardWidget() {
return EntitiesCard(card: this);
}
EntitiesCardData(rawData) : super(rawData) {
//Parsing card data
title = rawData["title"];
icon = rawData['icon'];
stateColor = rawData['state_color'] ?? false;
showHeaderToggle = rawData['show_header_toggle'] ?? false;
//Parsing entities
var rawEntities = rawData["entities"] ?? [];
rawEntities.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (rawEntity["type"] == "divider") {
entities.add(EntityWrapper(entity: Entity.divider()));
} else if (rawEntity["type"] == "section") {
entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
} else if (rawEntity["type"] == "call-service") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.callService,
"service": rawEntity["service"],
"service_data": rawEntity["service_data"]
},
"hold_action": EntityUIAction.none
};
entities.add(
EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
stateColor: rawEntity["state_color"] ?? stateColor,
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (rawEntity["type"] == "weblink") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.navigate,
"service": rawEntity["url"]
},
"hold_action": EntityUIAction.none
};
entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
url: rawEntity["url"]
),
stateColor: rawEntity["state_color"] ?? stateColor,
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
entities.add(
EntityWrapper(
entity: e,
stateColor: rawEntity["state_color"] ?? stateColor,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
} }
} }
class AlarmPanelCardData extends CardData {
String name;
List<dynamic> states;
@override
Widget buildCardWidget() {
return AlarmPanelCard(card: this);
}
AlarmPanelCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
states = rawData['states'];
//Parsing entity
var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(entitiId),
stateColor: true,
overrideName: name
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
}
}
}
class ButtonCardData extends CardData {
String name;
String icon;
bool showName;
bool showIcon;
double iconHeightPx = 0;
double iconHeightRem = 0;
@override
Widget buildCardWidget() {
return EntityButtonCard(card: this);
}
ButtonCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
icon = rawData['icon'];
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
stateColor = rawData['state_color'] ?? true;
var rawHeight = rawData['icon_height'];
if (rawHeight != null && rawHeight is String) {
if (rawHeight.contains('px')) {
iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0;
} else if (rawHeight.contains('rem')) {
iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;
} else if (rawHeight.contains('em')) {
iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0;
}
}
//Parsing entity
var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(entitiId),
overrideName: name,
overrideIcon: icon,
stateColor: stateColor,
uiAction: EntityUIAction(
rawEntityData: rawData
)
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
} else if (entitiId == null) {
entities.add(
EntityWrapper(
entity: Entity.ghost(
name,
icon,
),
stateColor: stateColor,
uiAction: EntityUIAction(
rawEntityData: rawData
)
)
);
}
}
}
class GaugeCardData extends CardData {
String name;
String unit;
double min;
double max;
Map severity;
@override
Widget buildCardWidget() {
return GaugeCard(card: this);
}
GaugeCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
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"];
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"];
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
showState = rawData['show_state'] ?? true;
stateColor = rawData['state_color'] ?? true;
columnsCount = rawData['columns'] ?? 4;
//Parsing entities
var rawEntities = rawData["entities"] ?? [];
rawEntities.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
entities.add(
EntityWrapper(
entity: e,
stateColor: stateColor,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
}
}
class HorizontalStackCardData extends CardData {
List<CardData> childCards;
@override
Widget buildCardWidget() {
return HorizontalStackCard(card: this);
}
HorizontalStackCardData(rawData) : super(rawData) {
if (rawData.containsKey('cards')) {
childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard);
}).toList();
} else {
childCards = [];
}
}
}
class VerticalStackCardData extends CardData {
List<CardData> childCards;
@override
Widget buildCardWidget() {
return VerticalStackCard(card: this);
}
VerticalStackCardData(rawData) : super(rawData) {
if (rawData.containsKey('cards')) {
childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard);
}).toList();
} else {
childCards = [];
}
}
}
class MarkdownCardData extends CardData {
String title;
String content;
@override
Widget buildCardWidget() {
return MarkdownCard(card: this);
}
MarkdownCardData(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';
}
}

361
lib/cards/card_widget.dart Normal file
View File

@ -0,0 +1,361 @@
part of '../main.dart';
class CardWidget extends StatelessWidget {
final HACard card;
const CardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.linkedEntityWrapper!= null) {
if (card.linkedEntityWrapper.entity.isHidden) {
return Container(width: 0.0, height: 0.0,);
}
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: MissedEntityWidget(),
handleTap: false,
);
}
}
if (card.conditions.isNotEmpty) {
bool showCardByConditions = true;
for (var condition in card.conditions) {
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
if (conditionEntity != null &&
((condition['state'] != null && conditionEntity.state != condition['state']) ||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
) {
showCardByConditions = false;
break;
}
}
if (!showCardByConditions) {
return Container(width: 0.0, height: 0.0,);
}
}
switch (card.type) {
case CardType.ENTITIES: {
return _buildEntitiesCard(context);
}
case CardType.GLANCE: {
return _buildGlanceCard(context);
}
case CardType.MEDIA_CONTROL: {
return _buildMediaControlsCard(context);
}
case CardType.ENTITY_BUTTON: {
return _buildEntityButtonCard(context);
}
case CardType.GAUGE: {
return _buildGaugeCard(context);
}
/* case CardType.LIGHT: {
return _buildLightCard(context);
}*/
case CardType.MARKDOWN: {
return _buildMarkdownCard(context);
}
case CardType.ALARM_PANEL: {
return _buildAlarmPanelCard(context);
}
case CardType.HORIZONTAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
children.add(
Flexible(
fit: FlexFit.tight,
child: card.build(context),
)
);
}
});
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
case CardType.VERTICAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
children.add(
card.build(context)
);
});
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
default: {
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
return _buildEntitiesCard(context);
} else {
return _buildUnsupportedCard(context);
}
}
}
}
Widget _buildEntitiesCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeader(name: card.name));
entitiesToShow.forEach((EntityWrapper entity) {
body.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
child: entity.entity.buildDefaultWidget(context)
),
));
});
return Card(
child: Padding(
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
child: Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildMarkdownCard(BuildContext context) {
if (card.content == null) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeader(name: card.name));
body.add(MarkdownBody(data: card.content));
return Card(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildAlarmPanelCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeader(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle(
color: Colors.grey
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
EntityIcon(
size: 50.0,
),
Container(
width: 26.0,
child: IconButton(
padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
)
)
]
),
));
body.add(
AlarmControlPanelControlsWidget(
extended: true,
states: card.states,
)
);
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: body
)
)
);
}
Widget _buildGlanceCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> rows = [];
rows.add(CardHeader(name: card.name));
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
rows.add(
Padding(
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
child: FractionallySizedBox(
widthFactor: 1,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
List<Widget> buttons = [];
double buttonWidth = constraints.maxWidth / columnsCount;
entitiesToShow.forEach((EntityWrapper entity) {
buttons.add(
SizedBox(
width: buttonWidth,
child: EntityModel(
entityWrapper: entity,
child: GlanceCardEntityContainer(
showName: card.showName,
showState: card.showState,
),
handleTap: true
),
)
);
});
return Wrap(
//spacing: 5.0,
//alignment: WrapAlignment.spaceEvenly,
runSpacing: Sizes.doubleRowPadding,
children: buttons,
);
}
),
),
)
);
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: rows
)
);
}
Widget _buildMediaControlsCard(BuildContext context) {
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: MediaPlayerWidget()
)
);
}
Widget _buildEntityButtonCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName.toUpperCase();
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: EntityButtonCardBody(),
handleTap: true
)
);
}
Widget _buildGaugeCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name ??
card.linkedEntityWrapper.displayName;
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
card.linkedEntityWrapper.unitOfMeasurement;
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: GaugeCardBody(
min: card.min,
max: card.max,
severity: card.severity,
),
handleTap: true
)
);
}
Widget _buildLightCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name ??
card.linkedEntityWrapper.displayName;
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: LightCardBody(
min: card.min,
max: card.max,
severity: card.severity,
),
handleTap: true
)
);
}
Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeader(name: card.name ?? ""));
List<Widget> result = [];
if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: true,
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
),
)
]);
} else {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
),
]);
}
body.addAll(result);
return Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: body
)
);
}
}

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,93 +0,0 @@
part of '../main.dart';
class EntityButtonCard extends StatelessWidget {
final ButtonCardData card;
EntityButtonCard({
Key key, this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = card.entity;
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
} else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) {
return Container(width: 0.0, height: 0.0,);
}
double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize);
Widget buttonIcon;
if (!card.showIcon) {
buttonIcon = Container(height: Sizes.rowPadding, width: 10);
} else if (iconSize > 0) {
buttonIcon = SizedBox(
height: iconSize,
child: FractionallySizedBox(
widthFactor: 0.5,
child: FittedBox(
fit: BoxFit.contain,
child: EntityIcon(
//padding: EdgeInsets.only(top: 6),
),
)
),
);
} else {
buttonIcon = AspectRatio(
aspectRatio: 2,
child: FractionallySizedBox(
widthFactor: 0.5,
child: FittedBox(
fit: BoxFit.fitWidth,
child: EntityIcon(
//padding: EdgeInsets.only(top: 6),
),
)
),
);
}
return CardWrapper(
child: EntityModel(
entityWrapper: card.entity,
child: InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buttonIcon,
_buildName(context)
],
)
),
),
handleTap: true
)
);
}
Widget _buildName(BuildContext context) {
if (card.showName) {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
textStyle: Theme.of(context).textTheme.subhead,
wordsWrap: true,
textAlign: TextAlign.center
);
}
return Container(width: 0, height: 0);
}
}

View File

@ -1,38 +0,0 @@
part of '../main.dart';
class ErrorCard extends StatelessWidget {
final ErrorCardData card;
const ErrorCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
return CardWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
textAlign: TextAlign.center,
),
RaisedButton(
onPressed: () {
Clipboard.setData(new ClipboardData(text: card.cardConfig));
},
child: Text('Copy card config'),
),
RaisedButton(
onPressed: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
},
child: Text('Report issue'),
)
],
),
)
);
}
}

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,120 +0,0 @@
part of '../main.dart';
class GlanceCard extends StatelessWidget {
final GlanceCardData card;
const GlanceCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
int length = entitiesToShow.length;
int rowsCount;
int columnsCount;
if (length == 0) {
columnsCount = 0;
rowsCount = 0;
} else {
columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
rowsCount = (length / columnsCount).round();
}
List<TableRow> rows = [];
for (int i = 0; i < rowsCount; i++) {
int start = i*columnsCount;
int end = start + math.min(columnsCount, length - start);
List<Widget> rowChildren = [];
rowChildren.addAll(entitiesToShow.sublist(
start, end
).map(
(EntityWrapper entity){
return EntityModel(
entityWrapper: entity,
child: _buildEntityContainer(context, entity),
handleTap: true
);
}
).toList()
);
while (rowChildren.length < columnsCount) {
rowChildren.add(
Container()
);
}
rows.add(
TableRow(
children: rowChildren
)
);
}
return CardWrapper(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardHeader(name: card.title),
Padding(
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
child: Table(
children: rows
)
)
],
)
);
}
Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) {
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return MissedEntityWidget();
} else if (entityWrapper.entity.statelessType != StatelessEntityType.none) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = [];
if (card.showName) {
result.add(_buildName(context));
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
size: Sizes.iconSize,
)
);
if (card.showState) {
result.add(_buildState());
}
return Center(
child: InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
),
);
}
Widget _buildName(BuildContext context) {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: false,
textAlign: TextAlign.center,
textStyle: Theme.of(context).textTheme.body1,
);
}
Widget _buildState() {
return SimpleEntityState(
textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
padding: EdgeInsets.only(top: Sizes.rowPadding),
);
}
}

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,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,17 +0,0 @@
part of '../main.dart';
class UnsupportedCard extends StatelessWidget {
final CardData card;
const UnsupportedCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
return CardWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
)
);
}
}

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

@ -4,11 +4,9 @@ class CardHeader extends StatelessWidget {
final String name; final String name;
final Widget trailing; final Widget trailing;
final Widget leading;
final Widget subtitle; final Widget subtitle;
final double emptyPadding;
const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key); const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -16,15 +14,14 @@ class CardHeader extends StatelessWidget {
if ((name != null) && (name.trim().length > 0)) { if ((name != null) && (name.trim().length > 0)) {
result = new ListTile( result = new ListTile(
trailing: trailing, trailing: trailing,
leading: leading,
subtitle: subtitle, subtitle: subtitle,
title: Text("$name", title: Text("$name",
textAlign: TextAlign.left, textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headline), style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
); );
} else { } else {
result = new Container(width: 0.0, height: emptyPadding); result = new Container(width: 0.0, height: 0.0);
} }
return result; return result;
} }

View File

@ -1,21 +0,0 @@
part of '../../main.dart';
class CardWrapper extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: padding,
child: child
),
);
}
}

View File

@ -0,0 +1,51 @@
part of '../../main.dart';
class EntityButtonCardBody extends StatelessWidget {
EntityButtonCardBody({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: FractionallySizedBox(
widthFactor: 1,
child: Column(
children: <Widget>[
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
size: constraints.maxWidth / 2.5,
);
}
),
_buildName()
],
),
),
);
}
Widget _buildName() {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
wordsWrap: true,
textAlign: TextAlign.center,
fontSize: Sizes.nameFontSize,
);
}
}

View File

@ -0,0 +1,153 @@
part of '../../main.dart';
class GaugeCardBody extends StatefulWidget {
final int min;
final int max;
final Map severity;
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
_GaugeCardBodyState createState() => _GaugeCardBodyState();
}
class _GaugeCardBodyState extends State<GaugeCardBody> {
List<charts.Series> seriesList;
List<charts.Series<GaugeSegment, String>> _createData(double value) {
double fixedValue;
if (value > widget.max) {
fixedValue = widget.max.toDouble();
} else if (value < widget.min) {
fixedValue = widget.min.toDouble();
} else {
fixedValue = value;
}
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
Color mainColor;
if (widget.severity != null) {
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
mainColor = Colors.red;
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
mainColor = Colors.amber;
} else {
mainColor = Colors.green;
}
} else {
mainColor = Colors.green;
}
final data = [
GaugeSegment('Main', toShow, mainColor),
GaugeSegment('Rest', 100 - toShow, Colors.black45),
];
return [
charts.Series<GaugeSegment, String>(
id: 'Segments',
domainFn: (GaugeSegment segment, _) => segment.segment,
measureFn: (GaugeSegment segment, _) => segment.value,
colorFn: (GaugeSegment segment, _) => segment.color,
// Set a label accessor to control the text of the arc label.
labelAccessorFn: (GaugeSegment segment, _) =>
segment.segment == 'Main' ? '${segment.value}' : null,
data: data,
)
];
}
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: AspectRatio(
aspectRatio: 1.5,
child: Stack(
fit: StackFit.expand,
overflow: Overflow.clip,
children: [
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double verticalOffset;
if(constraints.maxWidth > 150.0) {
verticalOffset = 0.2;
} else if (constraints.maxWidth > 100.0) {
verticalOffset = 0.3;
} else {
verticalOffset = 0.3;
}
return FractionallySizedBox(
heightFactor: 2,
widthFactor: 1,
alignment: FractionalOffset(0,verticalOffset),
child: charts.PieChart(
_createData(entityWrapper.entity.doubleState),
animate: false,
defaultRenderer: charts.ArcRendererConfig(
arcRatio: 0.4,
startAngle: pi,
arcLength: pi,
),
),
);
}
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: 2*fontSize),
child: SimpleEntityState(
//textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
bold: true,
textAlign: TextAlign.center,
padding: EdgeInsets.all(0.0),
fontSize: fontSize,
//padding: EdgeInsets.only(top: Sizes.rowPadding),
),
);
}
),
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: fontSize),
child: EntityName(
fontSize: fontSize,
maxLines: 1,
padding: EdgeInsets.all(0.0),
textAlign: TextAlign.center,
textOverflow: TextOverflow.ellipsis,
),
);
}
),
)
]
)
),
);
}
}
class GaugeSegment {
final String segment;
final double value;
final charts.Color color;
GaugeSegment(this.segment, this.value, Color color)
: this.color = charts.Color(
r: color.red, g: color.green, b: color.blue, a: color.alpha);
}

View File

@ -0,0 +1,85 @@
part of '../../main.dart';
class GlanceCardEntityContainer extends StatelessWidget {
final bool showName;
final bool showState;
final bool nameInTheBottom;
final double iconSize;
final double nameFontSize;
final bool wordsWrapInName;
GlanceCardEntityContainer({
Key key,
@required this.showName,
@required this.showState,
this.nameInTheBottom: false,
this.iconSize: Sizes.iconSize,
this.nameFontSize: Sizes.smallFontSize,
this.wordsWrapInName: false
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = [];
if (!nameInTheBottom) {
if (showName) {
result.add(_buildName());
}
} else {
if (showState) {
result.add(_buildState());
}
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
)
);
if (!nameInTheBottom) {
if (showState) {
result.add(_buildState());
}
} else {
result.add(_buildName());
}
return Center(
child: InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
),
);
}
Widget _buildName() {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: wordsWrapInName,
textAlign: TextAlign.center,
fontSize: nameFontSize,
);
}
Widget _buildState() {
return SimpleEntityState(
textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
padding: EdgeInsets.only(top: Sizes.rowPadding),
);
}
}

View File

@ -0,0 +1,90 @@
part of '../../main.dart';
class LightCardBody extends StatefulWidget {
final int min;
final int max;
final Map severity;
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
_LightCardBodyState createState() => _LightCardBodyState();
}
class _LightCardBodyState extends State<LightCardBody> {
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
LightEntity entity = entityWrapper.entity;
Logger.d("Light brightness: ${entity.brightness}");
return FractionallySizedBox(
widthFactor: 0.5,
child: Container(
//color: Colors.redAccent,
child: SingleCircularSlider(
255,
entity.brightness ?? 0,
baseColor: Colors.white,
handlerColor: Colors.blue[200],
selectionColor: Colors.blue[100],
),
),
);
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: AspectRatio(
aspectRatio: 1.5,
child: Stack(
fit: StackFit.expand,
overflow: Overflow.clip,
children: [
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: 2*fontSize),
child: SimpleEntityState(
//textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
bold: true,
textAlign: TextAlign.center,
padding: EdgeInsets.all(0.0),
fontSize: fontSize,
//padding: EdgeInsets.only(top: Sizes.rowPadding),
),
);
}
),
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: fontSize),
child: EntityName(
fontSize: fontSize,
maxLines: 1,
padding: EdgeInsets.all(0.0),
textAlign: TextAlign.center,
textOverflow: TextOverflow.ellipsis,
),
);
}
),
)
]
)
),
);
}
}

View File

@ -51,10 +51,6 @@ class EntityUIAction {
String holdNavigationPath; String holdNavigationPath;
String holdService; String holdService;
Map<String, dynamic> holdServiceData; Map<String, dynamic> holdServiceData;
String doubleTapAction = EntityUIAction.none;
String doubleTapNavigationPath;
String doubleTapService;
Map<String, dynamic> doubleTapServiceData;
EntityUIAction({rawEntityData}) { EntityUIAction({rawEntityData}) {
if (rawEntityData != null) { if (rawEntityData != null) {
@ -80,17 +76,6 @@ class EntityUIAction {
holdServiceData = rawEntityData["hold_action"]["service_data"]; holdServiceData = rawEntityData["hold_action"]["service_data"];
} }
} }
if (rawEntityData["double_tap_action"] != null) {
if (rawEntityData["double_tap_action"] is String) {
doubleTapAction = rawEntityData["double_tap_action"];
} else {
doubleTapAction =
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
doubleTapService = rawEntityData["double_tap_action"]["service"];
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
}
}
} }
} }
@ -113,13 +98,10 @@ class CardType {
static const IFRAME = "iframe"; static const IFRAME = "iframe";
static const GAUGE = "gauge"; static const GAUGE = "gauge";
static const ENTITY_BUTTON = "entity-button"; static const ENTITY_BUTTON = "entity-button";
static const BUTTON = "button";
static const CONDITIONAL = "conditional"; static const CONDITIONAL = "conditional";
static const ALARM_PANEL = "alarm-panel"; static const ALARM_PANEL = "alarm-panel";
static const MARKDOWN = "markdown"; static const MARKDOWN = "markdown";
static const LIGHT = "light"; static const LIGHT = "light";
static const ENTITY_FILTER = "entity-filter";
static const UNKNOWN = "unknown";
} }
class Sizes { class Sizes {
@ -129,10 +111,10 @@ class Sizes {
static const extendedWidgetHeight = 50.0; static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0; static const iconSize = 28.0;
static const largeIconSize = 46.0; static const largeIconSize = 46.0;
//static const stateFontSize = 15.0; static const stateFontSize = 15.0;
//static const nameFontSize = 15.0; static const nameFontSize = 15.0;
//static const smallFontSize = 14.0; static const smallFontSize = 14.0;
//static const largeFontSize = 24.0; static const largeFontSize = 24.0;
static const inputWidth = 160.0; static const inputWidth = 160.0;
static const rowPadding = 10.0; static const rowPadding = 10.0;
static const doubleRowPadding = rowPadding*2; static const doubleRowPadding = rowPadding*2;

View File

@ -25,12 +25,9 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
void _callService(AlarmControlPanelEntity entity, String service) { void _callService(AlarmControlPanelEntity entity, String service) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, entity.domain, service, entity.entityId,
service: service, {"code": "$code"}));
entityId: entity.entityId,
data: {"code": "$code"}
);
setState(() { setState(() {
code = ""; code = "";
}); });
@ -61,11 +58,7 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
FlatButton( FlatButton(
child: new Text("Yes"), child: new Text("Yes"),
onPressed: () { onPressed: () {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null));
domain: entity.domain,
service: "alarm_trigger",
entityId: entity.entityId
);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
@ -248,9 +241,7 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
FlatButton( FlatButton(
child: Text( child: Text(
"TRIGGER", "TRIGGER",
style: Theme.of(context).textTheme.subhead.copyWith( style: TextStyle(color: Colors.redAccent)
color: Theme.of(context).errorColor
)
), ),
onPressed: () => _askToTrigger(entity), onPressed: () => _askToTrigger(entity),
) )

View File

@ -7,7 +7,8 @@ class BadgeWidget extends StatelessWidget {
double iconSize = 26.0; double iconSize = 26.0;
Widget badgeIcon; Widget badgeIcon;
String onBadgeTextValue; String onBadgeTextValue;
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain); Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
EntityColor.badgeColors["default"];
switch (entityModel.entityWrapper.entity.domain) { switch (entityModel.entityWrapper.entity.domain) {
case "sun": case "sun":
{ {
@ -29,7 +30,7 @@ class BadgeWidget extends StatelessWidget {
badgeIcon = EntityIcon( badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
size: iconSize, size: iconSize,
color: Theme.of(context).textTheme.body1.color color: Colors.black
); );
break; break;
} }
@ -39,7 +40,7 @@ class BadgeWidget extends StatelessWidget {
badgeIcon = EntityIcon( badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
size: iconSize, size: iconSize,
color: Theme.of(context).textTheme.body1.color color: Colors.black
); );
onBadgeTextValue = entityModel.entityWrapper.entity.displayState; onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
break; break;
@ -63,9 +64,7 @@ class BadgeWidget extends StatelessWidget {
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
softWrap: false, softWrap: false,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.body1.copyWith( style: TextStyle(fontSize: stateFontSize),
fontSize: stateFontSize
)
), ),
); );
break; break;
@ -78,9 +77,7 @@ class BadgeWidget extends StatelessWidget {
onBadgeText = Container( onBadgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0), padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$onBadgeTextValue", child: Text("$onBadgeTextValue",
style: Theme.of(context).textTheme.overline.copyWith( style: TextStyle(fontSize: 12.0, color: Colors.white),
color: HAClientTheme().getOnBadgeTextColor()
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
softWrap: false, softWrap: false,
overflow: TextOverflow.fade), overflow: TextOverflow.fade),
@ -101,7 +98,7 @@ class BadgeWidget extends StatelessWidget {
decoration: new BoxDecoration( decoration: new BoxDecoration(
// Circle shape // Circle shape
shape: BoxShape.circle, shape: BoxShape.circle,
color: Theme.of(context).cardColor, color: Colors.white,
// The border you want // The border you want
border: new Border.all( border: new Border.all(
width: 2.0, width: 2.0,
@ -134,7 +131,7 @@ class BadgeWidget extends StatelessWidget {
child: Text( child: Text(
"${entityModel.entityWrapper.displayName}", "${entityModel.entityWrapper.displayName}",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.caption, style: TextStyle(fontSize: 12.0),
softWrap: true, softWrap: true,
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -143,6 +140,6 @@ class BadgeWidget extends StatelessWidget {
], ],
), ),
onTap: () => onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entityId: entityModel.entityWrapper.entity.entityId))); eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity)));
} }
} }

View File

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

View File

@ -2,9 +2,7 @@ part of '../../../main.dart';
class CameraStreamView extends StatefulWidget { class CameraStreamView extends StatefulWidget {
final bool withControls; CameraStreamView({Key key}) : super(key: key);
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
@override @override
_CameraStreamViewState createState() => _CameraStreamViewState(); _CameraStreamViewState createState() => _CameraStreamViewState();
@ -12,175 +10,45 @@ class CameraStreamView extends StatefulWidget {
class _CameraStreamViewState extends State<CameraStreamView> { class _CameraStreamViewState extends State<CameraStreamView> {
CameraEntity _entity;
String _streamUrl = "";
bool _isLoaded = false;
double _aspectRatio = 1.33;
String _webViewHtml;
String _jsMessageChannelName = 'unknown';
Completer _loading;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
} }
Future _loadResources() { CameraEntity _entity;
if (_loading != null && !_loading.isCompleted) { bool started = false;
Logger.d("[Camera Player] Resources loading is not finished yet"); String streamUrl = "";
return _loading.future;
}
Logger.d("[Camera Player] Loading resources");
_loading = Completer();
_entity = EntityModel
.of(context)
.entityWrapper
.entity;
if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
HomeAssistant().getCameraStream(_entity.entityId)
.then((data) {
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
_webViewHtml = Uri.dataFromString(
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8')
).toString();
_loading.complete();
});
})
.catchError((e) {
if (e == 'start_stream_failed') {
Logger.e("[Camera Player] Home Assistant failed starting stream. Forcing MJPEG: $e");
_loadMJPEG().then((_) {
_loading.complete();
});
} else {
_loading.completeError(e);
Logger.e("[Camera Player] Error loading stream: $e");
}
});
} else {
_loadMJPEG().then((_) {
_loading.complete();
});
}
return _loading.future;
}
Future _loadMJPEG() async { launchStream() {
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity Launcher.launchURLInCustomTab(
.entityId}?token=${_entity.attributes['access_token']}'; context: context,
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}'; url: streamUrl
var file = await rootBundle.loadString('assets/html/cameraView.html');
_webViewHtml = Uri.dataFromString(
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8')
).toString();
}
Widget _buildScreen() {
Widget screenWidget;
if (!_isLoaded) {
screenWidget = Center(
child: EntityPicture(
fit: BoxFit.contain,
)
);
} else {
screenWidget = WebView(
initialUrl: _webViewHtml,
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
debuggingEnabled: Logger.isInDebugMode,
gestureNavigationEnabled: false,
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: {
JavascriptChannel(
name: _jsMessageChannelName,
onMessageReceived: ((message) {
Logger.d('[Camera Player] Message from page: $message');
setState((){
_aspectRatio = double.tryParse(message.message) ?? 1.33;
});
})
)
}
);
}
return AspectRatio(
aspectRatio: _aspectRatio,
child: screenWidget
); );
} }
Widget _buildControls() {
return Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
iconSize: 40,
color: Theme.of(context).accentColor,
onPressed: _isLoaded ? () {
setState(() {
_isLoaded = false;
});
} : null,
),
Expanded(
child: Container(),
),
IconButton(
icon: Icon(Icons.fullscreen),
iconSize: 40,
color: Theme.of(context).accentColor,
onPressed: _isLoaded ? () {
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!_isLoaded && (_loading == null || _loading.isCompleted)) { if (!started) {
_loadResources().then((_) => setState((){ _isLoaded = true; })); _entity = EntityModel
.of(context)
.entityWrapper
.entity;
started = true;
} }
if (widget.withControls) { streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
return Card( .entityId}?token=${_entity.attributes['access_token']}';
child: Column( return Column(
mainAxisSize: MainAxisSize.min, children: <Widget>[
children: <Widget>[ Container(
_buildScreen(), padding: const EdgeInsets.all(20.0),
_buildControls() child: IconButton(
], icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
), iconSize: 50.0,
); onPressed: () => launchStream(),
} else { )
return _buildScreen(); )
} ],
);
} }
@override @override

View File

@ -10,8 +10,9 @@ class ClimateControlWidget extends StatefulWidget {
class _ClimateControlWidgetState extends State<ClimateControlWidget> { class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _temperaturePending = false; bool _showPending = false;
bool _changedHere = false; bool _changedHere = false;
Timer _resetTimer;
Timer _tempThrottleTimer; Timer _tempThrottleTimer;
Timer _targetTempThrottleTimer; Timer _targetTempThrottleTimer;
double _tmpTemperature = 0.0; double _tmpTemperature = 0.0;
@ -26,11 +27,9 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _tmpAuxHeat = false; bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) { void _resetVars(ClimateEntity entity) {
if (!_temperaturePending) { _tmpTemperature = entity.temperature;
_tmpTemperature = entity.temperature; _tmpTargetHigh = entity.targetHigh;
_tmpTargetHigh = entity.targetHigh; _tmpTargetLow = entity.targetLow;
_tmpTargetLow = entity.targetLow;
}
_tmpHVACMode = entity.state; _tmpHVACMode = entity.state;
_tmpFanMode = entity.fanMode; _tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode; _tmpSwingMode = entity.swingMode;
@ -39,6 +38,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_tmpAuxHeat = entity.auxHeat; _tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity; _tmpTargetHumidity = entity.targetHumidity;
_showPending = false;
_changedHere = false; _changedHere = false;
} }
@ -73,44 +73,36 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
void _setTemperature(ClimateEntity entity) { void _setTemperature(ClimateEntity entity) {
_tempThrottleTimer?.cancel(); if (_tempThrottleTimer!=null) {
_tempThrottleTimer.cancel();
}
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_temperaturePending = true;
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1)); _tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
}); });
_tempThrottleTimer = Timer(Duration(seconds: 2), () { _tempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_temperaturePending = false; eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
ConnectionManager().callService( _resetStateTimer(entity);
domain: entity.domain,
service: "set_temperature",
entityId: entity.entityId,
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
);
}); });
}); });
} }
void _setTargetTemp(ClimateEntity entity) { void _setTargetTemp(ClimateEntity entity) {
_targetTempThrottleTimer?.cancel(); if (_targetTempThrottleTimer!=null) {
_targetTempThrottleTimer.cancel();
}
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_temperaturePending = true;
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1)); _tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1)); _tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
}); });
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () { _targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_temperaturePending = false; eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
ConnectionManager().callService( _resetStateTimer(entity);
domain: entity.domain,
service: "set_temperature",
entityId: entity.entityId,
data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
);
}); });
}); });
} }
@ -119,12 +111,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpTargetHumidity = value.roundToDouble(); _tmpTargetHumidity = value.roundToDouble();
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
domain: entity.domain, _resetStateTimer(entity);
service: "set_humidity",
entityId: entity.entityId,
data: {"humidity": "$_tmpTargetHumidity"}
);
}); });
} }
@ -132,12 +120,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpHVACMode = value; _tmpHVACMode = value;
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"}));
domain: entity.domain, _resetStateTimer(entity);
service: "set_hvac_mode",
entityId: entity.entityId,
data: {"hvac_mode": "$_tmpHVACMode"}
);
}); });
} }
@ -145,12 +129,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpSwingMode = value; _tmpSwingMode = value;
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
domain: entity.domain, _resetStateTimer(entity);
service: "set_swing_mode",
entityId: entity.entityId,
data: {"swing_mode": "$_tmpSwingMode"}
);
}); });
} }
@ -158,7 +138,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpFanMode = value; _tmpFanMode = value;
_changedHere = true; _changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"}); eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
_resetStateTimer(entity);
}); });
} }
@ -166,7 +147,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpPresetMode = value; _tmpPresetMode = value;
_changedHere = true; _changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"}); eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"}));
_resetStateTimer(entity);
}); });
} }
@ -183,7 +165,18 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpAuxHeat = value; _tmpAuxHeat = value;
_changedHere = true; _changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"}); eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
_resetStateTimer(entity);
});
}
void _resetStateTimer(ClimateEntity entity) {
if (_resetTimer!=null) {
_resetTimer.cancel();
}
_resetTimer = Timer(Duration(seconds: 3), () {
setState(() {});
_resetVars(entity);
}); });
} }
@ -192,9 +185,10 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity; final ClimateEntity entity = entityModel.entityWrapper.entity;
if (_changedHere) { if (_changedHere) {
//_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow); _showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
_changedHere = false; _changedHere = false;
} else { } else {
_resetTimer?.cancel();
_resetVars(entity); _resetVars(entity);
} }
return Padding( return Padding(
@ -203,20 +197,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
//_buildOnOffControl(entity), //_buildOnOffControl(entity),
_buildTemperatureControls(entity, context), _buildTemperatureControls(entity),
_buildTargetTemperatureControls(entity, context), _buildTargetTemperatureControls(entity),
_buildHumidityControls(entity, context), _buildHumidityControls(entity),
_buildOperationControl(entity, context), _buildOperationControl(entity),
_buildFanControl(entity, context), _buildFanControl(entity),
_buildSwingControl(entity, context), _buildSwingControl(entity),
_buildPresetModeControl(entity, context), _buildPresetModeControl(entity),
_buildAuxHeatControl(entity, context) _buildAuxHeatControl(entity)
], ],
), ),
); );
} }
Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) { Widget _buildPresetModeControl(ClimateEntity entity) {
if (entity.supportPresetMode) { if (entity.supportPresetMode) {
return ModeSelectorWidget( return ModeSelectorWidget(
options: entity.presetModes, options: entity.presetModes,
@ -241,7 +235,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
}*/ }*/
Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) { Widget _buildAuxHeatControl(ClimateEntity entity) {
if (entity.supportAuxHeat ) { if (entity.supportAuxHeat ) {
return ModeSwitchWidget( return ModeSwitchWidget(
caption: "Aux heat", caption: "Aux heat",
@ -253,7 +247,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
} }
Widget _buildOperationControl(ClimateEntity entity, BuildContext context) { Widget _buildOperationControl(ClimateEntity entity) {
if (entity.hvacModes != null) { if (entity.hvacModes != null) {
return ModeSelectorWidget( return ModeSelectorWidget(
onChange: (mode) => _setHVACMode(entity, mode), onChange: (mode) => _setHVACMode(entity, mode),
@ -266,7 +260,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
} }
Widget _buildFanControl(ClimateEntity entity, BuildContext context) { Widget _buildFanControl(ClimateEntity entity) {
if (entity.supportFanMode) { if (entity.supportFanMode) {
return ModeSelectorWidget( return ModeSelectorWidget(
options: entity.fanModes, options: entity.fanModes,
@ -279,7 +273,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
} }
Widget _buildSwingControl(ClimateEntity entity, BuildContext context) { Widget _buildSwingControl(ClimateEntity entity) {
if (entity.supportSwingMode) { if (entity.supportSwingMode) {
return ModeSelectorWidget( return ModeSelectorWidget(
onChange: (mode) => _setSwingMode(entity, mode), onChange: (mode) => _setSwingMode(entity, mode),
@ -292,15 +286,17 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
} }
Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) { Widget _buildTemperatureControls(ClimateEntity entity) {
if ((entity.supportTargetTemperature) && (entity.temperature != null)) { if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text("Target temperature", style: Theme.of(context).textTheme.body1), Text("Target temperature", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
TemperatureControlWidget( TemperatureControlWidget(
value: _tmpTemperature, value: _tmpTemperature,
active: _temperaturePending, fontColor: _showPending ? Colors.red : Colors.black,
onDec: () => _temperatureDown(entity), onDec: () => _temperatureDown(entity),
onInc: () => _temperatureUp(entity), onInc: () => _temperatureUp(entity),
) )
@ -311,13 +307,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
} }
Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) { Widget _buildTargetTemperatureControls(ClimateEntity entity) {
List<Widget> controls = []; List<Widget> controls = [];
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) { if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
controls.addAll(<Widget>[ controls.addAll(<Widget>[
TemperatureControlWidget( TemperatureControlWidget(
value: _tmpTargetLow, value: _tmpTargetLow,
active: _temperaturePending, fontColor: _showPending ? Colors.red : Colors.black,
onDec: () => _targetLowDown(entity), onDec: () => _targetLowDown(entity),
onInc: () => _targetLowUp(entity), onInc: () => _targetLowUp(entity),
), ),
@ -330,7 +326,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
controls.add( controls.add(
TemperatureControlWidget( TemperatureControlWidget(
value: _tmpTargetHigh, value: _tmpTargetHigh,
active: _temperaturePending, fontColor: _showPending ? Colors.red : Colors.black,
onDec: () => _targetHighDown(entity), onDec: () => _targetHighDown(entity),
onInc: () => _targetHighUp(entity), onInc: () => _targetHighUp(entity),
) )
@ -340,7 +336,9 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text("Target temperature range", style: Theme.of(context).textTheme.body1), Text("Target temperature range", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
Row( Row(
children: controls, children: controls,
) )
@ -351,20 +349,16 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} }
} }
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) { Widget _buildHumidityControls(ClimateEntity entity) {
List<Widget> result = [];
if (entity.supportTargetHumidity) { if (entity.supportTargetHumidity) {
return Column( result.addAll(<Widget>[
crossAxisAlignment: CrossAxisAlignment.start, Text(
children: <Widget>[ "$_tmpTargetHumidity%",
Padding( style: TextStyle(fontSize: Sizes.largeFontSize),
padding: EdgeInsets.only(top: Sizes.rowPadding), ),
child: Text("Target humidity", style: Theme.of(context).textTheme.body1), Expanded(
), child: Slider(
UniversalSlider(
leading: Text(
"$_tmpTargetHumidity%",
style: Theme.of(context).textTheme.display1,
),
value: _tmpTargetHumidity, value: _tmpTargetHumidity,
max: entity.maxHumidity, max: entity.maxHumidity,
min: entity.minHumidity, min: entity.minHumidity,
@ -376,6 +370,24 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}), }),
onChangeEnd: (double v) => _setTargetHumidity(entity, v), onChangeEnd: (double v) => _setTargetHumidity(entity, v),
), ),
)
]);
}
if (result.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Target humidity", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
Container( Container(
height: Sizes.rowPadding, height: Sizes.rowPadding,
) )
@ -392,6 +404,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
@override @override
void dispose() { void dispose() {
_resetTimer?.cancel();
super.dispose(); super.dispose();
} }

View File

@ -33,16 +33,23 @@ class ClimateStateWidget extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Text("$displayState", Text("$displayState",
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: Theme.of(context).textTheme.body2), style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: Sizes.stateFontSize,
)),
Text(" $targetTemp", Text(" $targetTemp",
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: Theme.of(context).textTheme.body1) style: new TextStyle(
fontSize: Sizes.stateFontSize,
))
], ],
), ),
entity.currentTemperature != null ? entity.currentTemperature != null ?
Text("Currently: ${entity.currentTemperature}", Text("Currently: ${entity.currentTemperature}",
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: Theme.of(context).textTheme.subtitle style: new TextStyle(
fontSize: Sizes.stateFontSize,
color: Colors.black45)
) : ) :
Container(height: 0.0,) Container(height: 0.0,)
], ],

View File

@ -3,8 +3,10 @@ part of '../../../main.dart';
class ModeSelectorWidget extends StatelessWidget { class ModeSelectorWidget extends StatelessWidget {
final String caption; final String caption;
final List options; final List<String> options;
final String value; final String value;
final double captionFontSize;
final double valueFontSize;
final onChange; final onChange;
final EdgeInsets padding; final EdgeInsets padding;
@ -14,6 +16,8 @@ class ModeSelectorWidget extends StatelessWidget {
@required this.options, @required this.options,
this.value, this.value,
@required this.onChange, @required this.onChange,
this.captionFontSize,
this.valueFontSize,
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
}) : super(key: key); }) : super(key: key);
@ -24,7 +28,9 @@ class ModeSelectorWidget extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text("$caption", style: Theme.of(context).textTheme.body1), Text("$caption", style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
)),
Row( Row(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
@ -34,12 +40,15 @@ class ModeSelectorWidget extends StatelessWidget {
value: value, value: value,
iconSize: 30.0, iconSize: 30.0,
isExpanded: true, isExpanded: true,
style: Theme.of(context).textTheme.title, style: TextStyle(
fontSize: valueFontSize ?? Sizes.largeFontSize,
color: Colors.black,
),
hint: Text("Select ${caption.toLowerCase()}"), hint: Text("Select ${caption.toLowerCase()}"),
items: options.map((value) { items: options.map((String value) {
return new DropdownMenuItem<String>( return new DropdownMenuItem<String>(
value: '$value', value: value,
child: Text('$value'), child: Text(value),
); );
}).toList(), }).toList(),
onChanged: (mode) => onChange(mode), onChanged: (mode) => onChange(mode),

View File

@ -4,6 +4,7 @@ class ModeSwitchWidget extends StatelessWidget {
final String caption; final String caption;
final onChange; final onChange;
final double captionFontSize;
final bool value; final bool value;
final bool expanded; final bool expanded;
final EdgeInsets padding; final EdgeInsets padding;
@ -12,6 +13,7 @@ class ModeSwitchWidget extends StatelessWidget {
Key key, Key key,
@required this.caption, @required this.caption,
@required this.onChange, @required this.onChange,
this.captionFontSize,
this.value, this.value,
this.expanded: true, this.expanded: true,
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding) this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
@ -23,7 +25,7 @@ class ModeSwitchWidget extends StatelessWidget {
padding: this.padding, padding: this.padding,
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
_buildCaption(context), _buildCaption(),
Switch( Switch(
onChanged: (value) => onChange(value), onChanged: (value) => onChange(value),
value: value ?? false, value: value ?? false,
@ -33,10 +35,12 @@ class ModeSwitchWidget extends StatelessWidget {
); );
} }
Widget _buildCaption(BuildContext context) { Widget _buildCaption() {
Widget captionWidget = Text( Widget captionWidget = Text(
"$caption", "$caption",
style: Theme.of(context).textTheme.body1, style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
),
); );
if (expanded) { if (expanded) {
return Expanded( return Expanded(

View File

@ -2,7 +2,8 @@ part of '../../../main.dart';
class TemperatureControlWidget extends StatelessWidget { class TemperatureControlWidget extends StatelessWidget {
final double value; final double value;
final bool active; final double fontSize;
final Color fontColor;
final onInc; final onInc;
final onDec; final onDec;
@ -11,9 +12,8 @@ class TemperatureControlWidget extends StatelessWidget {
@required this.value, @required this.value,
@required this.onInc, @required this.onInc,
@required this.onDec, @required this.onDec,
//this.fontSize, this.fontSize,
this.active: false this.fontColor})
})
: super(key: key); : super(key: key);
@override @override
@ -23,7 +23,10 @@ class TemperatureControlWidget extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Text( Text(
"$value", "$value",
style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1, style: TextStyle(
fontSize: fontSize ?? 24.0,
color: fontColor ?? Colors.black
),
), ),
Column( Column(
children: <Widget>[ children: <Widget>[

View File

@ -18,7 +18,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
setState(() { setState(() {
_tmpPosition = position.roundToDouble(); _tmpPosition = position.roundToDouble();
_changedHere = true; _changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_cover_position", entityId: entity.entityId, data: {"position": _tmpPosition.round()}); eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
}); });
} }
@ -26,7 +26,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
setState(() { setState(() {
_tmpTiltPosition = position.roundToDouble(); _tmpTiltPosition = position.roundToDouble();
_changedHere = true; _changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_cover_tilt_position", entityId: entity.entityId, data: {"tilt_position": _tmpTiltPosition.round()}); eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
}); });
} }
@ -62,10 +62,13 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding), padding: EdgeInsets.fromLTRB(
child: Text("Position"), 0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
), ),
UniversalSlider( Slider(
value: _tmpPosition, value: _tmpPosition,
min: 0.0, min: 0.0,
max: 100.0, max: 100.0,
@ -77,7 +80,8 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
}); });
}, },
onChangeEnd: (double value) => _setNewPosition(entity, value), onChangeEnd: (double value) => _setNewPosition(entity, value),
) ),
Container(height: Sizes.rowPadding,)
], ],
); );
} else { } else {
@ -94,7 +98,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
} }
if (entity.supportSetTiltPosition) { if (entity.supportSetTiltPosition) {
controls.addAll(<Widget>[ controls.addAll(<Widget>[
UniversalSlider( Slider(
value: _tmpTiltPosition, value: _tmpTiltPosition,
min: 0.0, min: 0.0,
max: 100.0, max: 100.0,
@ -113,8 +117,10 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
if (controls.isNotEmpty) { if (controls.isNotEmpty) {
controls.insert(0, Padding( controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, 0), 0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Tilt position"), child: Text("Tilt position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
)); ));
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -129,18 +135,18 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
class CoverTiltControlsWidget extends StatelessWidget { class CoverTiltControlsWidget extends StatelessWidget {
void _open(CoverEntity entity) { void _open(CoverEntity entity) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, service: "open_cover_tilt", entityId: entity.entityId); entity.domain, "open_cover_tilt", entity.entityId, null));
} }
void _close(CoverEntity entity) { void _close(CoverEntity entity) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, service: "close_cover_tilt", entityId: entity.entityId); entity.domain, "close_cover_tilt", entity.entityId, null));
} }
void _stop(CoverEntity entity) { void _stop(CoverEntity entity) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, service: "stop_cover_tilt", entityId: entity.entityId); entity.domain, "stop_cover_tilt", entity.entityId, null));
} }
@override @override

View File

@ -2,27 +2,18 @@ part of '../../../main.dart';
class CoverStateWidget extends StatelessWidget { class CoverStateWidget extends StatelessWidget {
void _open(CoverEntity entity) { void _open(CoverEntity entity) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, entity.domain, "open_cover", entity.entityId, null));
service: "open_cover",
entityId: entity.entityId
);
} }
void _close(CoverEntity entity) { void _close(CoverEntity entity) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, entity.domain, "close_cover", entity.entityId, null));
service: "close_cover",
entityId: entity.entityId
);
} }
void _stop(CoverEntity entity) { void _stop(CoverEntity entity) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, entity.domain, "stop_cover", entity.entityId, null));
service: "stop_cover",
entityId: entity.entityId
);
} }
@override @override

View File

@ -35,7 +35,8 @@ class DateTimeEntity extends Entity {
return formattedState; return formattedState;
} }
void setNewState(Map newValue) { void setNewState(newValue) {
ConnectionManager().callService(domain: domain, service: "set_datetime", entityId: entityId, data: newValue); eventBus
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
} }
} }

View File

@ -9,8 +9,10 @@ class DateTimeStateWidget extends StatelessWidget {
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
child: GestureDetector( child: GestureDetector(
child: Text("${entity.formattedState}", child: Text("${entity.formattedState}",
textAlign: TextAlign.right textAlign: TextAlign.right,
), style: new TextStyle(
fontSize: Sizes.stateFontSize,
)),
onTap: () => _handleStateTap(context, entity), onTap: () => _handleStateTap(context, entity),
)); ));
} }

View File

@ -11,23 +11,25 @@ class DefaultEntityContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context); final EntityModel entityModel = EntityModel.of(context);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) { if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget(); return MissedEntityWidget();
} }
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.divider) { if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
return Divider(); return Divider(
color: Colors.black45,
);
} }
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) { if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Divider(), Divider(
color: Colors.black45,
),
Text( Text(
"${entityModel.entityWrapper.entity.displayName}", "${entityModel.entityWrapper.entity.displayName}",
style: HAClientTheme().getLinkTextStyle(context).copyWith( style: TextStyle(color: Colors.blue),
decoration: TextDecoration.none
)
) )
], ],
); );
@ -36,6 +38,7 @@ class DefaultEntityContainer extends StatelessWidget {
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: <Widget>[ children: <Widget>[
EntityIcon(), EntityIcon(),
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 3, flex: 3,
@ -58,11 +61,6 @@ class DefaultEntityContainer extends StatelessWidget {
entityModel.entityWrapper.handleTap(); entityModel.entityWrapper.handleTap();
} }
}, },
onDoubleTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleDoubleTap();
}
},
child: result, child: result,
); );
} else { } else {

View File

@ -1,6 +1,13 @@
part of '../main.dart'; part of '../main.dart';
enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink} class StatelessEntityType {
static const NONE = 0;
static const MISSED = 1;
static const DIVIDER = 2;
static const SECTION = 3;
static const CALL_SERVICE = 4;
static const WEBLINK = 5;
}
class Entity { class Entity {
@ -69,8 +76,8 @@ class Entity {
String entityPicture; String entityPicture;
String state; String state;
String displayState; String displayState;
DateTime lastUpdatedTimestamp; DateTime _lastUpdated;
StatelessEntityType statelessType = StatelessEntityType.none; int statelessType = 0;
List<Entity> childEntities = []; List<Entity> childEntities = [];
String deviceClass; String deviceClass;
@ -113,47 +120,42 @@ class Entity {
} }
Entity.missed(String entityId) { Entity.missed(String entityId) {
statelessType = StatelessEntityType.missed; statelessType = StatelessEntityType.MISSED;
attributes = {"hidden": false}; attributes = {"hidden": false};
this.entityId = entityId; this.entityId = entityId;
} }
Entity.divider() { Entity.divider() {
statelessType = StatelessEntityType.divider; statelessType = StatelessEntityType.DIVIDER;
attributes = {"hidden": false}; attributes = {"hidden": false};
} }
Entity.section(String label) { Entity.section(String label) {
statelessType = StatelessEntityType.section; statelessType = StatelessEntityType.SECTION;
attributes = {"hidden": false, "friendly_name": "$label"}; attributes = {"hidden": false, "friendly_name": "$label"};
} }
Entity.ghost(String name, String icon) {
statelessType = StatelessEntityType.ghost;
attributes = {"icon": icon, "hidden": false, "friendly_name": name};
}
Entity.callService({String icon, String name, String service, String actionName}) { Entity.callService({String icon, String name, String service, String actionName}) {
statelessType = StatelessEntityType.callService; statelessType = StatelessEntityType.CALL_SERVICE;
entityId = service; entityId = service;
displayState = actionName?.toUpperCase() ?? "RUN"; displayState = actionName?.toUpperCase() ?? "RUN";
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"}; attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
} }
Entity.weblink({String url, String name, String icon}) { Entity.weblink({String url, String name, String icon}) {
statelessType = StatelessEntityType.webLink; statelessType = StatelessEntityType.WEBLINK;
entityId = "custom.custom"; entityId = "custom.custom"; //TODO wtf??
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
} }
void update(Map rawData, String webHost) { void update(Map rawData, String webHost) {
attributes = rawData["attributes"] ?? {}; attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"] != null ? rawData["entity_id"].split(".")[0] : null; domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"]; entityId = rawData["entity_id"];
deviceClass = attributes["device_class"]; deviceClass = attributes["device_class"];
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"]; state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state); displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]); _lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost); entityPicture = _getEntityPictureUrl(webHost);
} }
@ -219,17 +221,17 @@ class Entity {
String getAttribute(String attributeName) { String getAttribute(String attributeName) {
if (attributes != null) { if (attributes != null) {
return attributes["$attributeName"].toString(); return attributes["$attributeName"];
} }
return null; return null;
} }
String _getLastUpdatedFormatted() { String _getLastUpdatedFormatted() {
if (lastUpdatedTimestamp == null) { if (_lastUpdated == null) {
return "-"; return "-";
} else { } else {
DateTime now = DateTime.now(); DateTime now = DateTime.now();
Duration d = now.difference(lastUpdatedTimestamp); Duration d = now.difference(_lastUpdated);
String text; String text;
int v; int v;
if (d.inDays == 0) { if (d.inDays == 0) {

View File

@ -0,0 +1,77 @@
part of '../main.dart';
class EntityColor {
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
};
static const _stateColors = {
EntityState.on: Colors.amber,
"auto": Colors.amber,
EntityState.active: Colors.amber,
EntityState.playing: Colors.amber,
EntityState.paused: Colors.amber,
"above_horizon": Colors.amber,
EntityState.home: Colors.amber,
EntityState.open: Colors.amber,
EntityState.cleaning: Colors.amber,
EntityState.returning: Colors.amber,
EntityState.off: defaultStateColor,
EntityState.closed: defaultStateColor,
"below_horizon": defaultStateColor,
"default": defaultStateColor,
EntityState.idle: defaultStateColor,
"heat": Colors.redAccent,
"cool": Colors.lightBlue,
EntityState.unavailable: Colors.black26,
EntityState.unknown: Colors.black26,
EntityState.alarm_disarmed: Colors.green,
EntityState.alarm_armed_away: Colors.redAccent,
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
EntityState.alarm_armed_home: Colors.redAccent,
EntityState.alarm_armed_night: Colors.redAccent,
EntityState.alarm_triggered: Colors.redAccent,
EntityState.alarm_arming: Colors.amber,
EntityState.alarm_disarming: Colors.amber,
EntityState.alarm_pending: Colors.amber,
};
static Color stateColor(String state) {
return _stateColors[state] ?? _stateColors["default"];
}
static charts.Color chartHistoryStateColor(String state, int id) {
Color c = _stateColors[state];
if (c != null) {
return charts.Color(
r: c.red,
g: c.green,
b: c.blue,
a: c.alpha
);
} else {
double r = id.toDouble() % 10;
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
}
}
static Color historyStateColor(String state, int id) {
Color c = _stateColors[state];
if (c != null) {
return c;
} else {
if (id > -1) {
double r = id.toDouble() % 10;
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
} else {
return _stateColors[EntityState.on];
}
}
}
}

View File

@ -9,9 +9,6 @@ class EntityIcon extends StatelessWidget {
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key); const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
if (entityId == null) {
return 0;
}
String domain = entityId.split(".")[0]; String domain = entityId.split(".")[0];
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"]; String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
String iconNameByDeviceClass; String iconNameByDeviceClass;
@ -26,13 +23,12 @@ class EntityIcon extends StatelessWidget {
} }
} }
Widget buildIcon(BuildContext context, EntityWrapper data, Color color) { Widget buildIcon(EntityWrapper data, Color color) {
Widget result;
if (data == null) { if (data == null) {
return null; return null;
} }
if (data.entityPicture != null) { if (data.entityPicture != null) {
result = Container( return Container(
height: size+12, height: size+12,
width: size+12, width: size+12,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -45,74 +41,33 @@ class EntityIcon extends StatelessWidget {
) )
), ),
); );
} else {
String iconName = data.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
result = Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
);
if (data.entity is LightEntity &&
(data.entity as LightEntity).supportColor &&
(data.entity as LightEntity).color != null
) {
Color lightColor = (data.entity as LightEntity).color.toColor();
if (lightColor == Colors.white) {
return result;
}
result = Stack(
children: <Widget>[
result,
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)
)
]
),
),
)
],
);
}
} }
return result; String iconName = data.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
return Padding(
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
)
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
Color iconColor;
if (color != null) {
iconColor = color;
} else if (entityWrapper.stateColor) {
iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context);
} else {
iconColor = HAClientTheme().getOffStateColor(context);
}
return Padding( return Padding(
padding: padding, padding: padding,
child: buildIcon( child: buildIcon(
context, entityWrapper,
entityWrapper, color ?? EntityColor.stateColor(entityWrapper.entity.state)
iconColor
), ),
); );
} }

View File

@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget {
final bool handleTap; final bool handleTap;
static EntityModel of(BuildContext context) { static EntityModel of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<EntityModel>(); return context.inheritFromWidgetOfExactType(EntityModel);
} }
@override @override
bool updateShouldNotify(EntityModel oldWidget) { bool updateShouldNotify(InheritedWidget oldWidget) {
return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp; return true;
} }
} }

View File

@ -5,24 +5,18 @@ class EntityName extends StatelessWidget {
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final TextOverflow textOverflow; final TextOverflow textOverflow;
final bool wordsWrap; final bool wordsWrap;
final double fontSize;
final TextAlign textAlign; final TextAlign textAlign;
final int maxLines; final int maxLines;
final TextStyle textStyle;
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.textStyle, this.wordsWrap: true, this.textAlign: TextAlign.left}) : super(key: key); const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
TextStyle tStyle; TextStyle textStyle = TextStyle(fontSize: fontSize);
if (textStyle == null) { if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) { textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
tStyle = HAClientTheme().getLinkTextStyle(context);
} else {
tStyle = Theme.of(context).textTheme.body1;
}
} else {
tStyle = textStyle;
} }
return Padding( return Padding(
padding: padding, padding: padding,
@ -31,7 +25,7 @@ class EntityName extends StatelessWidget {
overflow: textOverflow, overflow: textOverflow,
softWrap: wordsWrap, softWrap: wordsWrap,
maxLines: maxLines, maxLines: maxLines,
style: tStyle, style: textStyle,
textAlign: textAlign, textAlign: textAlign,
), ),
); );

View File

@ -2,9 +2,10 @@ part of '../main.dart';
class EntityPageLayout extends StatelessWidget { class EntityPageLayout extends StatelessWidget {
final bool showClose;
final Entity entity; final Entity entity;
EntityPageLayout({Key key, this.entity}) : super(key: key); EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -13,6 +14,38 @@ class EntityPageLayout extends StatelessWidget {
child: ListView( child: ListView(
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
children: <Widget>[ children: <Widget>[
showClose ?
Container(
color: Colors.blue[300],
height: 36,
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8),
child: Text(
entity.displayName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 22
),
),
),
),
IconButton(
padding: EdgeInsets.all(0),
icon: Icon(Icons.close),
color: Colors.white,
iconSize: 30.0,
onPressed: () {
eventBus.fire(ShowEntityPageEvent());
},
)
],
),
) :
Container(height: 0, width: 0,),
Padding( Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding), padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)), child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),

View File

@ -1,71 +0,0 @@
part of '../main.dart';
class EntityPicture extends StatelessWidget {
final EdgeInsetsGeometry padding;
final BoxFit fit;
const EntityPicture({Key key, this.padding: const EdgeInsets.all(0.0), this.fit: BoxFit.cover}) : super(key: key);
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
String domain = entityId.split(".")[0];
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
String iconNameByDeviceClass;
if (deviceClass != null) {
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
}
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
if (iconName != null) {
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
} else {
return 0;
}
}
Widget buildIcon(EntityWrapper data, BuildContext context) {
if (data == null) {
return null;
}
String iconName = data.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
Widget iconPicture = Container(
child: Center(
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: Sizes.largeIconSize,
color: HAClientTheme().getOffStateColor(context),
)
)
);
if (data.entityPicture != null) {
return CachedNetworkImage(
imageUrl: data.entityPicture,
fit: this.fit,
errorWidget: (context, _, __) => iconPicture,
placeholder: (context, _) => iconPicture,
);
}
return iconPicture;
}
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: buildIcon(
entityWrapper,
context
),
);
}
}

View File

@ -2,48 +2,47 @@ part of '../main.dart';
class EntityWrapper { class EntityWrapper {
String overrideName; String displayName;
final String overrideIcon; String icon;
final bool stateColor; String unitOfMeasurement;
String entityPicture;
EntityUIAction uiAction; EntityUIAction uiAction;
Entity entity; Entity entity;
String unitOfMeasurementOverride;
final List stateFilter;
String get icon => overrideIcon ?? entity.icon;
String get entityPicture => entity.entityPicture;
String get displayName => overrideName ?? entity.displayName;
String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement;
EntityWrapper({ EntityWrapper({
this.entity, this.entity,
this.overrideIcon, String icon,
this.overrideName, String displayName,
this.stateColor: true, this.uiAction
this.uiAction,
this.stateFilter
}) { }) {
if (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || entity.statelessType == StatelessEntityType.webLink) { if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
this.icon = icon ?? entity.icon;
if (icon == null) {
entityPicture = entity.entityPicture;
}
this.displayName = displayName ?? entity.displayName;
if (uiAction == null) { if (uiAction == null) {
uiAction = EntityUIAction(); uiAction = EntityUIAction();
} }
unitOfMeasurement = entity.unitOfMeasurement;
} }
} }
void handleTap() { void handleTap() {
switch (uiAction.tapAction) { switch (uiAction.tapAction) {
case EntityUIAction.toggle: { case EntityUIAction.toggle: {
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId); eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break; break;
} }
case EntityUIAction.callService: { case EntityUIAction.callService: {
if (uiAction.tapService != null) { if (uiAction.tapService != null) {
ConnectionManager().callService( eventBus.fire(
domain: uiAction.tapService.split(".")[0], ServiceCallEvent(uiAction.tapService.split(".")[0],
service: uiAction.tapService.split(".")[1], uiAction.tapService.split(".")[1], null,
data: uiAction.tapServiceData uiAction.tapServiceData));
);
} }
break; break;
} }
@ -54,16 +53,16 @@ class EntityWrapper {
case EntityUIAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entityId: entity.entityId)); new ShowEntityPageEvent(entity: entity));
break; break;
} }
case EntityUIAction.navigate: { case EntityUIAction.navigate: {
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) { if (uiAction.tapService.startsWith("/")) {
//TODO handle local urls //TODO handle local urls
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.tapService}'); Logger.w("Local urls is not supported yet");
} else { } else {
Launcher.launchURLInBrowser(uiAction.tapService); Launcher.launchURL(uiAction.tapService);
} }
break; break;
} }
@ -77,73 +76,33 @@ class EntityWrapper {
void handleHold() { void handleHold() {
switch (uiAction.holdAction) { switch (uiAction.holdAction) {
case EntityUIAction.toggle: { case EntityUIAction.toggle: {
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId); eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break; break;
} }
case EntityUIAction.callService: { case EntityUIAction.callService: {
if (uiAction.holdService != null) { if (uiAction.holdService != null) {
ConnectionManager().callService( eventBus.fire(
domain: uiAction.holdService.split(".")[0], ServiceCallEvent(uiAction.holdService.split(".")[0],
service: uiAction.holdService.split(".")[1], uiAction.holdService.split(".")[1], null,
data: uiAction.holdServiceData uiAction.holdServiceData));
);
} }
break; break;
} }
case EntityUIAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entityId: entity.entityId)); new ShowEntityPageEvent(entity: entity));
break; break;
} }
case EntityUIAction.navigate: { case EntityUIAction.navigate: {
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) { if (uiAction.holdService.startsWith("/")) {
//TODO handle local urls //TODO handle local urls
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.holdService}'); Logger.w("Local urls is not supported yet");
} else { } else {
Launcher.launchURLInBrowser(uiAction.holdService); Launcher.launchURL(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('${ConnectionManager().httpWebHost}${uiAction.doubleTapService}');
} else {
Launcher.launchURLInBrowser(uiAction.doubleTapService);
} }
break; break;
} }

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

@ -24,12 +24,9 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
setState(() { setState(() {
_tmpOscillate = oscillate; _tmpOscillate = oscillate;
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: "fan", "fan", "oscillate", entity.entityId,
service: "oscillate", {"oscillating": oscillate}));
entityId: entity.entityId,
data: {"oscillating": oscillate}
);
}); });
} }
@ -37,12 +34,9 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
setState(() { setState(() {
_tmpDirectionForward = forward; _tmpDirectionForward = forward;
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: "fan", "fan", "set_direction", entity.entityId,
service: "set_direction", {"direction": forward ? "forward" : "reverse"}));
entityId: entity.entityId,
data: {"direction": forward ? "forward" : "reverse"}
);
}); });
} }
@ -50,12 +44,9 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
setState(() { setState(() {
_tmpSpeed = value; _tmpSpeed = value;
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: "fan", "fan", "set_speed", entity.entityId,
service: "set_speed", {"speed": value}));
entityId: entity.entityId,
data: {"speed": value}
);
}); });
} }

View File

@ -6,6 +6,7 @@ class FlatServiceButton extends StatelessWidget {
final String serviceName; final String serviceName;
final String entityId; final String entityId;
final String text; final String text;
final double fontSize;
FlatServiceButton({ FlatServiceButton({
Key key, Key key,
@ -13,16 +14,17 @@ class FlatServiceButton extends StatelessWidget {
@required this.serviceName, @required this.serviceName,
@required this.entityId, @required this.entityId,
@required this.text, @required this.text,
this.fontSize: Sizes.stateFontSize
}) : super(key: key); }) : super(key: key);
void _setNewState() { void _setNewState() {
ConnectionManager().callService(domain: serviceDomain, service: serviceName, entityId: entityId); eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null));
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: Theme.of(context).textTheme.subhead.fontSize*2.5, height: fontSize*2.5,
child: FlatButton( child: FlatButton(
onPressed: (() { onPressed: (() {
_setNewState(); _setNewState();
@ -30,7 +32,8 @@ class FlatServiceButton extends StatelessWidget {
child: Text( child: Text(
text, text,
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: HAClientTheme().getActionTextStyle(context), style:
new TextStyle(fontSize: fontSize, color: Colors.blue),
), ),
) )
); );

View File

@ -28,12 +28,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpBrightness = value.round(); _tmpBrightness = value.round();
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, entity.domain, "turn_on", entity.entityId,
service: "turn_on", {"brightness": _tmpBrightness}));
entityId: entity.entityId,
data: {"brightness": _tmpBrightness}
);
}); });
} }
@ -41,12 +38,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpWhiteValue = value.round(); _tmpWhiteValue = value.round();
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, entity.domain, "turn_on", entity.entityId,
service: "turn_on", {"white_value": _tmpWhiteValue}));
entityId: entity.entityId,
data: {"white_value": _tmpWhiteValue}
);
}); });
} }
@ -55,12 +49,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpColorTemp = value.round(); _tmpColorTemp = value.round();
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, entity.domain, "turn_on", entity.entityId,
service: "turn_on", {"color_temp": _tmpColorTemp}));
entityId: entity.entityId,
data: {"color_temp": _tmpColorTemp}
);
}); });
} }
@ -68,12 +59,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpColor = color; _tmpColor = color;
_changedHere = true; _changedHere = true;
ConnectionManager().callService( Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
domain: entity.domain, eventBus.fire(new ServiceCallEvent(
service: "turn_on", entity.domain, "turn_on", entity.entityId,
entityId: entity.entityId, {"hs_color": [color.hue, color.saturation*100]}));
data: {"hs_color": [color.hue, color.saturation*100]}
);
}); });
} }
@ -82,12 +71,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpEffect = value; _tmpEffect = value;
_changedHere = true; _changedHere = true;
if (_tmpEffect != null) { if (_tmpEffect != null) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, entity.domain, "turn_on", entity.entityId,
service: "turn_on", {"effect": "$value"}));
entityId: entity.entityId,
data: {"effect": "$value"}
);
} }
}); });
} }
@ -183,7 +169,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
} }
return UniversalSlider( return UniversalSlider(
title: "Color temperature", title: "Color temperature",
leading: Text("Cold", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.lightBlue)), leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
value: val, value: val,
onChangeEnd: (value) => _setColorTemp(entity, value), onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds, max: entity.maxMireds,
@ -194,7 +180,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpColorTemp = value.round(); _tmpColorTemp = value.round();
}); });
}, },
closing: Text("Warm", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.amberAccent),), closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
); );
} else { } else {
return Container(width: 0.0, height: 0.0); return Container(width: 0.0, height: 0.0);
@ -224,7 +210,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}, },
), ),
FlatButton( FlatButton(
color: savedColor?.toColor() ?? Theme.of(context).backgroundColor, color: savedColor?.toColor() ?? Colors.transparent,
child: Text('Paste color'), child: Text('Paste color'),
onPressed: savedColor == null ? null : () { onPressed: savedColor == null ? null : () {
_setColor(entity, savedColor); _setColor(entity, savedColor);
@ -241,6 +227,8 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildEffectControl(LightEntity entity) { Widget _buildEffectControl(LightEntity entity) {
if ((entity.supportEffect) && (entity.effectList != null)) { if ((entity.supportEffect) && (entity.effectList != null)) {
Logger.d("[LIGHT] entity effects: ${entity.effectList}");
Logger.d("[LIGHT] current effect: $_tmpEffect");
List<String> list = List.from(entity.effectList); List<String> list = List.from(entity.effectList);
if (_tmpEffect!= null && !list.contains(_tmpEffect)) { if (_tmpEffect!= null && !list.contains(_tmpEffect)) {
list.insert(0, _tmpEffect); list.insert(0, _tmpEffect);

View File

@ -7,11 +7,11 @@ class LockStateWidget extends StatelessWidget {
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key); const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
void _lock(Entity entity) { void _lock(Entity entity) {
ConnectionManager().callService(domain: "lock", service: "lock", entityId: entity.entityId); eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
} }
void _unlock(Entity entity) { void _unlock(Entity entity) {
ConnectionManager().callService(domain: "lock", service: "unlock", entityId: entity.entityId); eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null));
} }
@override @override
@ -28,7 +28,8 @@ class LockStateWidget extends StatelessWidget {
onPressed: () => _unlock(entity), onPressed: () => _unlock(entity),
child: Text("UNLOCK", child: Text("UNLOCK",
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: HAClientTheme().getActionTextStyle(context) style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
), ),
) )
), ),
@ -38,7 +39,8 @@ class LockStateWidget extends StatelessWidget {
onPressed: () => _lock(entity), onPressed: () => _lock(entity),
child: Text("LOCK", child: Text("LOCK",
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: HAClientTheme().getActionTextStyle(context), style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
), ),
) )
) )
@ -54,7 +56,8 @@ class LockStateWidget extends StatelessWidget {
child: Text( child: Text(
entity.isLocked ? "UNLOCK" : "LOCK", entity.isLocked ? "UNLOCK" : "LOCK",
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: HAClientTheme().getActionTextStyle(context), style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
), ),
) )
); );

View File

@ -84,22 +84,25 @@ class MediaPlayerEntity extends Entity {
} }
bool canCalculateActualPosition() { bool canCalculateActualPosition() {
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds > 0; return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds >= 0;
} }
double getActualPosition() { double getActualPosition() {
double result = 0; double result = 0;
Duration durationD; if (canCalculateActualPosition()) {
Duration positionD; Duration durationD;
durationD = Duration(seconds: durationSeconds); Duration positionD;
positionD = Duration( durationD = Duration(seconds: durationSeconds);
positionD = Duration(
seconds: positionSeconds); seconds: positionSeconds);
result = positionD.inSeconds.toDouble(); result = positionD.inSeconds.toDouble();
int differenceInSeconds = DateTime int differenceInSeconds = DateTime
.now() .now()
.difference(positionLastUpdated) .difference(positionLastUpdated)
.inSeconds; .inSeconds;
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble(); result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
}
return result; return result;
} }

View File

@ -22,18 +22,18 @@ class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context); final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity; final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress = 0; double progress;
int currentPosition; int currentPosition;
if (entity.canCalculateActualPosition()) { if (entity.canCalculateActualPosition()) {
currentPosition = entity.getActualPosition().toInt(); currentPosition = entity.getActualPosition().toInt();
if (currentPosition > 0) { progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100; } else {
} progress = 0;
} }
return LinearProgressIndicator( return LinearProgressIndicator(
value: progress, value: progress,
backgroundColor: Colors.black45, backgroundColor: Colors.black45,
valueColor: AlwaysStoppedAnimation<Color>(HAClientTheme().getOnStateColor(context)), valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
); );
} }

View File

@ -13,6 +13,12 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
double _currentPosition = 0; double _currentPosition = 0;
int _savedPosition = 0; int _savedPosition = 0;
final TextStyle _seekTextStyle = TextStyle(
fontSize: 20,
color: Colors.blue,
fontWeight: FontWeight.bold
);
@override @override
initState() { initState() {
super.initState(); super.initState();
@ -47,14 +53,15 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
buttons.add( buttons.add(
RaisedButton( RaisedButton(
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"), child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
color: Theme.of(context).accentColor, color: Colors.orange,
focusColor: Colors.white,
onPressed: () { onPressed: () {
ConnectionManager().callService( eventBus.fire(ServiceCallEvent(
domain: "media_player", "media_player",
service: "media_seek", "media_seek",
entityId: entity.entityId, "${entity.entityId}",
data: {"seek_position": _savedPosition} {"seek_position": _savedPosition}
); ));
setState(() { setState(() {
_savedPosition = 0; _savedPosition = 0;
}); });
@ -72,20 +79,16 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
children: <Widget>[ children: <Widget>[
Text("00:00"), Text("00:00"),
Expanded( Expanded(
child: Text( child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle),
"${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.title.copyWith(
color: Colors.blue
)
),
), ),
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}") Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
], ],
), ),
UniversalSlider( Container(height: 10,),
Slider(
min: 0, min: 0,
activeColor: Theme.of(context).accentColor, activeColor: Colors.amber,
inactiveColor: Colors.black26,
max: entity.durationSeconds.toDouble(), max: entity.durationSeconds.toDouble(),
value: _currentPosition, value: _currentPosition,
onChangeStart: (val) { onChangeStart: (val) {
@ -100,12 +103,12 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
_seekStarted = false; _seekStarted = false;
Timer(Duration(milliseconds: 500), () { Timer(Duration(milliseconds: 500), () {
if (!_seekStarted) { if (!_seekStarted) {
ConnectionManager().callService( eventBus.fire(ServiceCallEvent(
domain: "media_player", "media_player",
service: "media_seek", "media_seek",
entityId: entity.entityId, "${entity.entityId}",
data: {"seek_position": val} {"seek_position": val}
); ));
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_currentPosition = val; _currentPosition = val;

View File

@ -1,7 +1,7 @@
part of '../../../main.dart'; part of '../../../main.dart';
class MediaPlayerWidget extends StatelessWidget { class MediaPlayerWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context); final EntityModel entityModel = EntityModel.of(context);
@ -12,14 +12,14 @@ class MediaPlayerWidget extends StatelessWidget {
Stack( Stack(
alignment: AlignmentDirectional.topEnd, alignment: AlignmentDirectional.topEnd,
children: <Widget>[ children: <Widget>[
_buildImage(entity, context), _buildImage(entity),
Positioned( Positioned(
bottom: 0.0, bottom: 0.0,
left: 0.0, left: 0.0,
right: 0.0, right: 0.0,
child: Container( child: Container(
color: Colors.black45, color: Colors.black45,
child: _buildState(entity, context), child: _buildState(entity),
), ),
), ),
Positioned( Positioned(
@ -35,9 +35,12 @@ class MediaPlayerWidget extends StatelessWidget {
); );
} }
Widget _buildState(MediaPlayerEntity entity, BuildContext context) { Widget _buildState(MediaPlayerEntity entity) {
TextStyle style = Theme.of(context).textTheme.body1.copyWith( TextStyle style = TextStyle(
color: Colors.white fontSize: 14.0,
color: Colors.white,
fontWeight: FontWeight.normal,
height: 1.2
); );
List<Widget> states = []; List<Widget> states = [];
states.add(Text("${entity.displayName}", style: style)); states.add(Text("${entity.displayName}", style: style));
@ -68,7 +71,7 @@ class MediaPlayerWidget extends StatelessWidget {
); );
} }
Widget _buildImage(MediaPlayerEntity entity, BuildContext context) { Widget _buildImage(MediaPlayerEntity entity) {
String state = entity.state; String state = entity.state;
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) { if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
return Container( return Container(
@ -94,7 +97,7 @@ class MediaPlayerWidget extends StatelessWidget {
Icon( Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"), MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
size: 150.0, size: 150.0,
color: HAClientTheme().getColorByEntityState("$state", context), color: EntityColor.stateColor("$state"),
) )
], ],
); );
@ -115,28 +118,26 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
void _setPower(MediaPlayerEntity entity) { void _setPower(MediaPlayerEntity entity) {
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
if (entity.state == EntityState.off) { if (entity.state == EntityState.off) {
ConnectionManager().callService( Logger.d("${entity.entityId} turn_on");
domain: entity.domain, eventBus.fire(new ServiceCallEvent(
service: "turn_on", entity.domain, "turn_on", entity.entityId,
entityId: entity.entityId null));
);
} else { } else {
ConnectionManager().callService( Logger.d("${entity.entityId} turn_off");
domain: entity.domain, eventBus.fire(new ServiceCallEvent(
service: "turn_off", entity.domain, "turn_off", entity.entityId,
entityId: entity.entityId null));
);
} }
}
} }
void _callAction(MediaPlayerEntity entity, String action) { void _callAction(MediaPlayerEntity entity, String action) {
Logger.d("${entity.entityId} $action"); Logger.d("${entity.entityId} $action");
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: entity.domain, entity.domain, "$action", entity.entityId,
service: "$action", null));
entityId: entity.entityId
);
} }
@override @override
@ -228,7 +229,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: entity.entityId)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: entity))
) )
); );
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) { } else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
@ -263,50 +264,27 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_newVolumeLevel = value; _newVolumeLevel = value;
ConnectionManager().callService( eventBus.fire(ServiceCallEvent("media_player", "volume_set", entityId, {"volume_level": value}));
domain: "media_player",
service: "volume_set",
entityId: entityId,
data: {"volume_level": value}
);
}); });
} }
void _setVolumeMute(bool isMuted, String entityId) { void _setVolumeMute(bool isMuted, String entityId) {
ConnectionManager().callService( eventBus.fire(ServiceCallEvent("media_player", "volume_mute", entityId, {"is_volume_muted": isMuted}));
domain: "media_player",
service: "volume_mute",
entityId: entityId,
data: {"is_volume_muted": isMuted}
);
} }
void _setVolumeUp(String entityId) { void _setVolumeUp(String entityId) {
ConnectionManager().callService( eventBus.fire(ServiceCallEvent("media_player", "volume_up", entityId, null));
domain: "media_player",
service: "volume_up",
entityId: entityId
);
} }
void _setVolumeDown(String entityId) { void _setVolumeDown(String entityId) {
ConnectionManager().callService( eventBus.fire(ServiceCallEvent("media_player", "volume_down", entityId, null));
domain: "media_player",
service: "volume_down",
entityId: entityId
);
} }
void _setSoundMode(String value, String entityId) { void _setSoundMode(String value, String entityId) {
setState(() { setState(() {
_newSoundMode = value; _newSoundMode = value;
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(ServiceCallEvent("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"}));
domain: "media_player",
service: "select_sound_mode",
entityId: entityId,
data: {"sound_mode": "$value"}
);
}); });
} }
@ -314,12 +292,7 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
setState(() { setState(() {
_newSource = source; _newSource = source;
_changedHere = true; _changedHere = true;
ConnectionManager().callService( eventBus.fire(ServiceCallEvent("media_player", "select_source", entityId, {"source": "$source"}));
domain: "media_player",
service: "select_source",
entityId: entityId,
data: {"source": "$source"}
);
}); });
} }
@ -353,13 +326,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
volumeStepWidget = Row( volumeStepWidget = Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
onPressed: () => _setVolumeDown(entity.entityId)
),
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId) onPressed: () => _setVolumeUp(entity.entityId)
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
onPressed: () => _setVolumeDown(entity.entityId)
) )
], ],
); );
@ -457,15 +430,15 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
} }
void _duplicateTo(entity) { void _duplicateTo(entity) {
if (entity.canCalculateActualPosition()) { HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt(); if (MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
Navigator.of(context).popAndPushNamed("/play-media", arguments: {"url": entity.attributes["media_content_id"], "type": entity.attributes["media_content_type"]});
} else { } else {
HomeAssistant().savedPlayerPosition = 0; Navigator.of(context).pushNamed("/play-media", arguments: {
}
Navigator.of(context).pushNamed("/play-media", arguments: {
"url": entity.attributes["media_content_id"], "url": entity.attributes["media_content_id"],
"type": entity.attributes["media_content_type"] "type": entity.attributes["media_content_type"]
}); });
}
} }
void _switchTo(entity) { void _switchTo(entity) {

View File

@ -11,12 +11,8 @@ class SelectStateWidget extends StatefulWidget {
class _SelectStateWidgetState extends State<SelectStateWidget> { class _SelectStateWidgetState extends State<SelectStateWidget> {
void setNewState(domain, entityId, newValue) { void setNewState(domain, entityId, newValue) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
domain: domain, {"option": "$newValue"}));
service: "select_option",
entityId: entityId,
data: {"option": "$newValue"}
);
} }
@override @override

View File

@ -7,10 +7,10 @@ class SimpleEntityState extends StatelessWidget {
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final int maxLines; final int maxLines;
final String customValue; final String customValue;
final TextStyle textStyle; final double fontSize;
//final bool bold; final bool bold;
const SimpleEntityState({Key key,/*this.bold: false,*/ this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.textStyle, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key); const SimpleEntityState({Key key,this.bold: false, this.maxLines: 10, this.fontSize: Sizes.stateFontSize, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -22,19 +22,16 @@ class SimpleEntityState extends StatelessWidget {
} else { } else {
state = customValue; state = customValue;
} }
TextStyle tStyle; TextStyle textStyle = TextStyle(
if (textStyle != null) { fontSize: this.fontSize,
tStyle = textStyle; fontWeight: FontWeight.normal
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) { );
tStyle = Theme.of(context).textTheme.subhead.copyWith( if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
color: HAClientTheme().getLinkTextStyle(context).color textStyle = textStyle.apply(color: Colors.blue);
);
} else {
tStyle = Theme.of(context).textTheme.body1;
} }
/*if (this.bold) { if (this.bold) {
textStyle = textStyle.apply(fontWeightDelta: 100); textStyle = textStyle.apply(fontWeightDelta: 100);
}*/ }
while (state.contains(" ")){ while (state.contains(" ")){
state = state.replaceAll(" ", " "); state = state.replaceAll(" ", " ");
} }
@ -46,7 +43,7 @@ class SimpleEntityState extends StatelessWidget {
maxLines: maxLines, maxLines: maxLines,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
softWrap: true, softWrap: true,
style: tStyle style: textStyle
) )
); );
if (expanded) { if (expanded) {

View File

@ -18,12 +18,8 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
_newValue = newValue; _newValue = newValue;
_changedHere = true; _changedHere = true;
}); });
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
domain: domain, {"value": "${newValue.toString()}"}));
service: "set_value",
entityId: entityId,
data: {"value": "${newValue.toString()}"}
);
} }
@override @override
@ -40,7 +36,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
} else { } else {
_changedHere = false; _changedHere = false;
} }
Widget slider = UniversalSlider( Widget slider = Slider(
min: entity.minValue * _multiplier, min: entity.minValue * _multiplier,
max: entity.maxValue * _multiplier, max: entity.maxValue * _multiplier,
value: (_newValue <= entity.maxValue) && value: (_newValue <= entity.maxValue) &&
@ -62,7 +58,8 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
children: <Widget>[ children: <Widget>[
Text( Text(
"$_newValue", "$_newValue",
style: Theme.of(context).textTheme.display1.copyWith( style: TextStyle(
fontSize: Sizes.largeFontSize,
color: Colors.blue color: Colors.blue
), ),
), ),

View File

@ -38,11 +38,8 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
} else { } else {
domain = entity.domain; domain = entity.domain;
} }
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(
domain: domain, domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
service: (newValue as bool) ? "turn_on" : "turn_off",
entityId: entity.entityId
);
} }
@override @override

View File

@ -26,12 +26,8 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
void setNewState(newValue, domain, entityId) { void setNewState(newValue, domain, entityId) {
if (validate(newValue, _minLength, _maxLength)) { if (validate(newValue, _minLength, _maxLength)) {
ConnectionManager().callService( eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
domain: domain, {"value": "$newValue"}));
service: "set_value",
entityId: entityId,
data: {"value": "$newValue"}
);
} else { } else {
setState(() { setState(() {
_tmpValue = _entityState; _tmpValue = _entityState;

View File

@ -29,8 +29,8 @@ class TimerEntity extends Entity {
Logger.e("Strange $entityId duration format: $durationSource"); Logger.e("Strange $entityId duration format: $durationSource");
duration = Duration(seconds: 0); duration = Duration(seconds: 0);
} }
} catch (e, stacktrace) { } catch (e) {
Logger.e("Error parsing duration for $entityId: $e", stacktrace: stacktrace); Logger.e("Error parsing duration for $entityId: ${e.toString()}");
duration = Duration(seconds: 0); duration = Duration(seconds: 0);
} }
} else { } else {

View File

@ -27,11 +27,11 @@ class _TimerStateState extends State<TimerState> {
try { try {
int passed = DateTime int passed = DateTime
.now() .now()
.difference(entity.lastUpdatedTimestamp) .difference(entity._lastUpdated)
.inSeconds; .inSeconds;
remaining = Duration(seconds: entity.duration.inSeconds - passed); remaining = Duration(seconds: entity.duration.inSeconds - passed);
} catch (e, stacktrace) { } catch (e) {
Logger.e("Error calculating ${entity.entityId} remaining time: $e", stacktrace: stacktrace); Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}");
remaining = Duration(seconds: 0); remaining = Duration(seconds: 0);
} }
}); });

View File

@ -1,102 +1,58 @@
part of '../main.dart'; part of '../main.dart';
class UniversalSlider extends StatefulWidget { class UniversalSlider extends StatelessWidget {
final Function onChanged; final onChanged;
final Function onChangeEnd; final onChangeEnd;
final Function onChangeStart;
final Widget leading; final Widget leading;
final Widget closing; final Widget closing;
final String title; final String title;
final double min; final double min;
final Color activeColor;
final double max; final double max;
final double value; final double value;
final int divisions;
final EdgeInsets padding; final EdgeInsets padding;
const UniversalSlider({Key key, this.onChanged, 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); const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
@override
State<StatefulWidget> createState() {
return UniversalSliderState();
}
}
class UniversalSliderState extends State<UniversalSlider> {
double _value;
bool _changeStarted = false;
@override
void initState() {
_value = widget.value;
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List <Widget> row = []; List <Widget> row = [];
List <Widget> col = []; if (leading != null) {
if (widget.leading != null) { row.add(leading);
row.add(widget.leading);
} }
row.add( row.add(
Flexible( Flexible(
child: Slider( child: Slider(
value: _value ?? math.max(widget.max ?? 100, _value ?? 0), value: value,
min: widget.min ?? 0, min: min,
max: widget.max ?? 100, max: max,
activeColor: widget.activeColor, onChanged: (value) => onChanged(value),
onChangeStart: (value) { onChangeEnd: (value) => onChangeEnd(value),
_changeStarted = true; ),
widget.onChangeStart?.call(value); )
},
divisions: widget.divisions,
onChanged: (value) {
setState(() {
_value = value;
});
widget.onChanged?.call(value);
},
onChangeEnd: (value) {
_changeStarted = false;
setState(() {
_value = value;
});
Timer(Duration(milliseconds: 500), () {
if (!_changeStarted) {
widget.onChangeEnd?.call(value);
}
});
}
),
)
); );
if (widget.closing != null) { if (closing != null) {
row.add(widget.closing); row.add(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( return Padding(
padding: widget.padding, padding: padding,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: col, children: <Widget>[
Container(height: Sizes.rowPadding,),
Text(
"$title",
style: TextStyle(fontSize: Sizes.stateFontSize),
),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
Container(height: Sizes.rowPadding,)
],
), ),
); );
} }
} }

View File

@ -10,7 +10,7 @@ class VacuumControls extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
_buildStatusAndBattery(entity, context), _buildStatusAndBattery(entity),
_buildCommands(entity), _buildCommands(entity),
_buildFanSpeed(entity), _buildFanSpeed(entity),
_buildAdditionalInfo(entity) _buildAdditionalInfo(entity)
@ -19,12 +19,12 @@ class VacuumControls extends StatelessWidget {
); );
} }
Widget _buildStatusAndBattery(VacuumEntity entity, BuildContext context) { Widget _buildStatusAndBattery(VacuumEntity entity) {
List<Widget> result = []; List<Widget> result = [];
if (entity.supportStatus) { if (entity.supportStatus) {
result.addAll( result.addAll(
<Widget>[ <Widget>[
Text("Status:"), Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),),
Container(width: 6,), Container(width: 6,),
Expanded( Expanded(
//flex: 1, //flex: 1,
@ -33,7 +33,10 @@ class VacuumControls extends StatelessWidget {
maxLines: 1, maxLines: 1,
softWrap: true, softWrap: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.body2, style: TextStyle(
fontSize: Sizes.stateFontSize,
fontWeight: FontWeight.bold
),
), ),
), ),
] ]
@ -45,7 +48,7 @@ class VacuumControls extends StatelessWidget {
result.addAll(<Widget>[ result.addAll(<Widget>[
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)), Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
Container(width: 6,), Container(width: 6,),
Text("$batteryLevel %") Text("$batteryLevel %", style: TextStyle(fontSize: Sizes.stateFontSize))
] ]
); );
} }
@ -169,7 +172,7 @@ class VacuumControls extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text("Vacuum cleaner commands:"), Text("Vacuum cleaner commands:", style: TextStyle(fontSize: Sizes.stateFontSize)),
Container(height: Sizes.rowPadding,), Container(height: Sizes.rowPadding,),
Row( Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@ -194,7 +197,7 @@ class VacuumControls extends StatelessWidget {
domain: "vacuum", domain: "vacuum",
entityId: entity.entityId, entityId: entity.entityId,
service: "set_fan_speed", service: "set_fan_speed",
data: {"fan_speed": val} additionalServiceData: {"fan_speed": val}
) )
), ),
); );

View File

@ -27,7 +27,10 @@ class VacuumStateButton extends StatelessWidget {
text: "RETURN TO DOCK" text: "RETURN TO DOCK"
); );
} else { } else {
result = Text(entity.state.toUpperCase(), style: Theme.of(context).textTheme.subhead); result = Text(entity.state.toUpperCase(), style: TextStyle(
fontSize: 16,
color: Colors.grey
));
} }
return Padding( return Padding(
padding: EdgeInsets.only(right: 15), padding: EdgeInsets.only(right: 15),

View File

@ -152,12 +152,39 @@ class EntityCollection {
return _allEntities[entityId] != null; return _allEntities[entityId] != null;
} }
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) { List<Entity> getByDomains({List<String> domains, List<String> stateFiler}) {
return _allEntities.values.where((entity) { return _allEntities.values.where((entity) {
return return domains.contains(entity.domain) &&
(excludeDomains.isEmpty || !excludeDomains.contains(entity.domain)) && ((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
(includeDomains.isEmpty || includeDomains.contains(entity.domain)) &&
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
}).toList(); }).toList();
} }
List<Entity> filterEntitiesForDefaultView() {
List<Entity> result = [];
List<Entity> groups = [];
List<Entity> nonGroupEntities = [];
_allEntities.forEach((id, entity){
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
groups.add(entity);
}
if (!entity.isGroup) {
nonGroupEntities.add(entity);
}
});
nonGroupEntities.forEach((entity) {
bool foundInGroup = false;
groups.forEach((groupEntity) {
if (groupEntity.childEntityIds.contains(entity.entityId)) {
foundInGroup = true;
}
});
if (!foundInGroup) {
result.add(entity);
}
});
result.insertAll(0, groups);
return result;
}
} }

View File

@ -2,8 +2,6 @@ part of 'main.dart';
class HomeAssistant { class HomeAssistant {
static const DEFAULT_DASHBOARD = 'lovelace';
static final HomeAssistant _instance = HomeAssistant._internal(); static final HomeAssistant _instance = HomeAssistant._internal();
factory HomeAssistant() { factory HomeAssistant() {
@ -13,73 +11,69 @@ class HomeAssistant {
EntityCollection entities; EntityCollection entities;
HomeAssistantUI ui; HomeAssistantUI ui;
Map _instanceConfig = {}; Map _instanceConfig = {};
Map services;
String _userName; String _userName;
String _lovelaceDashbordUrl; bool childMode;
HSVColor savedColor; HSVColor savedColor;
int savedPlayerPosition; int savedPlayerPosition;
String sendToPlayerId; String sendToPlayerId;
String sendFromPlayerId; String sendFromPlayerId;
Map services;
bool autoUi = false;
String fcmToken; String fcmToken;
Map _rawLovelaceData; Map _rawLovelaceData;
var _rawStates;
var _rawUserInfo;
var _rawPanels;
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
List<Panel> panels = []; List<Panel> panels = [];
Duration fetchTimeout = Duration(seconds: 30); Duration fetchTimeout = Duration(seconds: 30);
String get locationName { String get locationName {
if (!autoUi) { if (ConnectionManager().useLovelace) {
return ui?.title ?? "Home"; return ui?.title ?? "";
} else { } else {
return _instanceConfig["location_name"] ?? "Home"; return _instanceConfig["location_name"] ?? "";
} }
} }
String get userName => _userName ?? ''; String get userName => _userName ?? locationName;
String get userAvatarText => userName.length > 0 ? userName[0] : ""; String get userAvatarText => userName.length > 0 ? userName[0] : "";
bool get isNoEntities => entities == null || entities.isEmpty; bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty; bool get isNoViews => ui == null || ui.isEmpty;
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
HomeAssistant._internal() { HomeAssistant._internal() {
ConnectionManager().onStateChangeCallback = _handleEntityStateChange; ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
ConnectionManager().onLovelaceUpdatedCallback = _handleLovelaceUpdate;
DeviceInfoManager().loadDeviceInfo(); DeviceInfoManager().loadDeviceInfo();
} }
Completer _fetchCompleter; Completer _fetchCompleter;
Future fetchData(bool uiOnly) { Future fetchData() {
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) { if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
Logger.w("Previous data fetch is not completed yet"); Logger.w("Previous data fetch is not completed yet");
return _fetchCompleter.future; return _fetchCompleter.future;
} }
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
_fetchCompleter = Completer(); _fetchCompleter = Completer();
List<Future> futures = []; List<Future> futures = [];
if (!uiOnly) { futures.add(_getStates());
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost); if (ConnectionManager().useLovelace) {
futures.add(_getStates(null)); futures.add(_getLovelace());
futures.add(_getConfig(null));
futures.add(_getUserInfo(null));
futures.add(_getPanels(null));
futures.add(_getServices(null));
}
if (!autoUi) {
futures.add(_getLovelace(null));
} }
futures.add(_getConfig());
futures.add(_getServices());
futures.add(_getUserInfo());
futures.add(_getPanels());
futures.add(ConnectionManager().sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
));
Future.wait(futures).then((_) { Future.wait(futures).then((_) {
if (isComponentEnabled('mobile_app')) { if (isMobileAppEnabled) {
_createUI(); if (!childMode) _createUI();
_fetchCompleter.complete(); _fetchCompleter.complete();
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration(); MobileAppIntegrationManager.checkAppRegistration();
} else { } else {
_fetchCompleter.completeError(HACException("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")])); _fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app-integration")]));
} }
}).catchError((e) { }).catchError((e) {
_fetchCompleter.completeError(e); _fetchCompleter.completeError(e);
@ -87,48 +81,6 @@ class HomeAssistant {
return _fetchCompleter.future; return _fetchCompleter.future;
} }
Future<void> fetchDataFromCache() async {
Logger.d('Loading cached data');
SharedPreferences prefs = await SharedPreferences.getInstance();
bool cached = prefs.getBool('cached');
if (cached != null && cached) {
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
try {
_getStates(prefs);
if (!autoUi) {
_getLovelace(prefs);
}
_getConfig(prefs);
_getUserInfo(prefs);
_getPanels(prefs);
_getServices(prefs);
if (isComponentEnabled('mobile_app')) {
_createUI();
}
} catch (e) {
Logger.d('Didnt get cached data: $e');
}
}
}
void saveCache() async {
Logger.d('Saving data to cache...');
SharedPreferences prefs = await SharedPreferences.getInstance();
try {
await prefs.setString('cached_states', json.encode(_rawStates));
await prefs.setString('cached_lovelace', json.encode(_rawLovelaceData));
await prefs.setString('cached_user', json.encode(_rawUserInfo));
await prefs.setString('cached_config', json.encode(_instanceConfig));
await prefs.setString('cached_panels', json.encode(_rawPanels));
await prefs.setString('cached_services', json.encode(services));
await prefs.setBool('cached', true);
} catch (e, stacktrace) {
await prefs.setBool('cached', false);
Logger.e('Error saving cache: $e', stacktrace: stacktrace);
}
Logger.d('Done saving cache');
}
Future logout() async { Future logout() async {
Logger.d("Logging out..."); Logger.d("Logging out...");
await ConnectionManager().logout().then((_) { await ConnectionManager().logout().then((_) {
@ -138,202 +90,71 @@ class HomeAssistant {
}); });
} }
Future _getConfig(SharedPreferences sharedPrefs) async { Future _getConfig() async {
_instanceConfig?.clear(); await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_config')) { _instanceConfig = Map.from(data);
try {
var data = json.decode(sharedPrefs.getString('cached_config'));
_parseConfig(data);
} catch (e, stacktrace) {
Logger.e('Error gettong config from cache: $e', stacktrace: stacktrace);
}
} else {
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
Logger.e('get_config error: $e');
throw HACException("Error getting config: $e");
});
}
}
void _parseConfig(data) {
_instanceConfig = data;
}
Future _getStates(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) {
try {
var data = json.decode(sharedPrefs.getString('cached_states'));
_parseStates(data);
} catch (e, stacktrace) {
Logger.e('Error getting cached states: $e', stacktrace: stacktrace);
}
} else {
await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => _parseStates(data)
).catchError((e) {
Logger.e('get_states error: $e');
throw HACException("Error getting states: $e");
});
}
}
void _parseStates(data) {
_rawStates = data;
entities.parse(data);
}
Future _getLovelace(SharedPreferences sharedPrefs) {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_lovelace')) {
try {
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
_rawLovelaceData = data;
} catch (e) {
autoUi = true;
}
return Future.value();
} else {
Completer completer = Completer();
var additionalData;
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
additionalData = {
'url_path': _lovelaceDashbordUrl
};
}
ConnectionManager().sendSocketMessage(
type: 'lovelace/config',
additionalData: additionalData
).then((data) {
_rawLovelaceData = data;
completer.complete();
}).catchError((e) {
if ("$e".contains("config_not_found")) {
autoUi = true;
_rawLovelaceData = null;
completer.complete();
} else {
Logger.e('lovelace/config error: $e');
completer.completeError(HACException("Error getting lovelace config: $e"));
}
});
return completer.future;
}
}
Future _getServices(SharedPreferences prefs) async {
services?.clear();
if (prefs != null && prefs.containsKey('cached_services')) {
try {
var data = json.decode(prefs.getString('cached_services'));
_parseServices(data);
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
}
}
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
Logger.e('get_services error: $e');
});
}
void _parseServices(data) {
services = data;
}
Future _getUserInfo(SharedPreferences sharedPrefs) async {
_userName = null;
if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) {
try {
var data = json.decode(sharedPrefs.getString('cached_user'));
_parseUserInfo(data);
} catch (e, stacktrace) {
Logger.e('Error getting cached user info: $e', stacktrace: stacktrace);
}
return Future.value();
} else {
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
Logger.e('auth/current_user error: $e');
});
}
}
void _parseUserInfo(data) {
_rawUserInfo = data;
_userName = data["name"];
}
Future _getPanels(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) {
try {
var data = json.decode(sharedPrefs.getString('cached_panels'));
_parsePanels(data);
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
panels.clear();
}
} else {
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e, stacktrace) {
Logger.e('get_panels error: $e', stacktrace: stacktrace);
panels.clear();
throw HACException('Can\'t get panels: $e');
});
}
}
void _parsePanels(data) {
panels.clear();
_rawPanels = data;
List<Panel> dashboards = [];
data.forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
if (v['component_name'] != null && v['component_name'] == 'lovelace') {
dashboards.add(
Panel(
id: k,
componentName: v['component_name'],
title: title,
urlPath: v['url_path'],
config: v['config'],
icon: (v['icon'] == null || v['icon'] == 'hass:view-dashboard') ? 'mdi:view-dashboard' : v['icon']
)
);
} else {
panels.add(
Panel(
id: k,
componentName: v['component_name'],
title: title,
urlPath: v['url_path'],
config: v['config'],
icon: v['icon']
)
);
}
});
panels.insertAll(0, dashboards);
}
Future getCameraStream(String entityId) {
Completer completer = Completer();
ConnectionManager().sendSocketMessage(type: "camera/stream", additionalData: {"entity_id": entityId}).then((data) {
completer.complete(data);
}).catchError((e) { }).catchError((e) {
completer.completeError(e); throw HAError("Error getting config: ${e}");
}); });
return completer.future;
} }
bool isComponentEnabled(String name) { Future _getStates() async {
return _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("$name"); await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => entities.parse(data)
).catchError((e) {
throw HAError("Error getting states: $e");
});
} }
void _handleLovelaceUpdate() { Future _getLovelace() async {
if (_fetchCompleter != null && _fetchCompleter.isCompleted) { await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
eventBus.fire(new LovelaceChangedEvent()); throw HAError("Error getting lovelace config: $e");
} });
}
Future _getUserInfo() async {
_userName = null;
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) {
_userName = data["name"];
childMode = _userName.startsWith("[child]");
}).catchError((e) {
Logger.w("Can't get user info: $e");
});
}
Future _getServices() async {
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
Logger.d("Got ${data.length} services");
Logger.d("Media extractor: ${data["media_extractor"]}");
services = data;
}).catchError((e) {
Logger.w("Can't get services: $e");
});
}
Future _getPanels() async {
panels.clear();
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
data.forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
panels.add(Panel(
id: k,
type: v["component_name"],
title: title,
urlPath: v["url_path"],
config: v["config"],
icon: v["icon"]
)
);
});
}).catchError((e) {
throw HAError("Error getting panels list: $e");
});
} }
void _handleEntityStateChange(Map eventData) { void _handleEntityStateChange(Map eventData) {
if (_fetchCompleter != null && _fetchCompleter.isCompleted) { //TheLogger.debug( "New state for ${eventData['entity_id']}");
if (_fetchCompleter.isCompleted) {
Map data = Map.from(eventData); Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent( eventBus.fire(new StateChangedEvent(
entityId: data["entity_id"], entityId: data["entity_id"],
@ -342,14 +163,243 @@ class HomeAssistant {
} }
} }
bool isServiceExist(String service) { void _parseLovelace() {
return services != null && Logger.d("--Title: ${_rawLovelaceData["title"]}");
services.isNotEmpty && ui.title = _rawLovelaceData["title"];
services.containsKey(service); int viewCounter = 0;
Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
_rawLovelaceData["views"].forEach((rawView){
Logger.d("----view id: ${rawView['id']}");
HAView view = HAView(
count: viewCounter,
id: "${rawView['id']}",
name: rawView['title'],
iconName: rawView['icon'],
panel: rawView['panel'] ?? false,
);
if (rawView['badges'] != null && rawView['badges'] is List) {
rawView['badges'].forEach((entity) {
if (entities.isExist(entity)) {
Entity e = entities.get(entity);
view.badges.add(e);
}
});
}
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
ui.views.add(
view
);
viewCounter += 1;
});
}
List<HACard> _createLovelaceCards(List rawCards) {
List<HACard> result = [];
rawCards.forEach((rawCard){
try {
//bool isThereCardOptionsInside = rawCard["card"] != null;
var rawCardInfo = rawCard["card"] ?? rawCard;
HACard card = HACard(
id: "card",
name: rawCardInfo["title"] ?? rawCardInfo["name"],
type: rawCardInfo['type'] ?? CardType.ENTITIES,
columnsCount: rawCardInfo['columns'] ?? 4,
showName: rawCardInfo['show_name'] ?? true,
showState: rawCardInfo['show_state'] ?? true,
showEmpty: rawCardInfo['show_empty'] ?? true,
stateFilter: rawCardInfo['state_filter'] ?? [],
states: rawCardInfo['states'],
conditions: rawCard['conditions'] ?? [],
content: rawCardInfo['content'],
min: rawCardInfo['min'] ?? 0,
max: rawCardInfo['max'] ?? 100,
unit: rawCardInfo['unit'],
severity: rawCardInfo['severity']
);
if (rawCardInfo["cards"] != null) {
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
}
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
rawEntities?.forEach((rawEntity) {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (rawEntity["type"] == "divider") {
card.entities.add(EntityWrapper(entity: Entity.divider()));
} else if (rawEntity["type"] == "section") {
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
} else if (rawEntity["type"] == "call-service") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.callService,
"service": rawEntity["service"],
"service_data": rawEntity["service_data"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (rawEntity["type"] == "weblink") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.navigate,
"service": rawEntity["url"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
url: rawEntity["url"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (entities.isExist(rawEntity["entity"])) {
Entity e = entities.get(rawEntity["entity"]);
card.entities.add(
EntityWrapper(
entity: e,
displayName: rawEntity["name"],
icon: rawEntity["icon"],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
if (rawSingleEntity != null) {
var en = rawSingleEntity;
if (en is String) {
if (entities.isExist(en)) {
Entity e = entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: rawCardInfo["icon"],
displayName: rawCardInfo["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
}
} else {
if (entities.isExist(en["entity"])) {
Entity e = entities.get(en["entity"]);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: en["icon"],
displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
}
}
}
result.add(card);
} catch (e) {
Logger.e("There was an error parsing card: ${e.toString()}");
}
});
return result;
} }
void _createUI() { void _createUI() {
Logger.d("Creating Lovelace UI"); ui = HomeAssistantUI();
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData); if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
Logger.d("Creating Lovelace UI");
_parseLovelace();
} else {
Logger.d("Creating group-based UI");
int viewCounter = 0;
if (!entities.hasDefaultView) {
HAView view = HAView(
count: viewCounter,
id: "group.default_view",
name: "Home",
childEntities: entities.filterEntitiesForDefaultView()
);
ui.views.add(
view
);
viewCounter += 1;
}
entities.viewEntities.forEach((viewEntity) {
HAView view = HAView(
count: viewCounter,
id: viewEntity.entityId,
name: viewEntity.displayName,
childEntities: viewEntity.childEntities
);
view.linkedEntity = viewEntity;
ui.views.add(
view
);
viewCounter += 1;
});
}
}
Widget buildViews(BuildContext context, TabController tabController) {
return ui.build(context, tabController);
} }
} }
/*
class SendMessageQueue {
int _messageTimeout;
List<HAMessage> _queue = [];
SendMessageQueue(this._messageTimeout);
void add(String message) {
_queue.add(HAMessage(_messageTimeout, message));
}
List<String> getActualMessages() {
_queue.removeWhere((item) => item.isExpired());
List<String> result = [];
_queue.forEach((haMessage){
result.add(haMessage.message);
});
this.clear();
return result;
}
void clear() {
_queue.clear();
}
}
class HAMessage {
DateTime _timeStamp;
int _messageTimeout;
String message;
HAMessage(this._messageTimeout, this.message) {
_timeStamp = DateTime.now();
}
bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
}
}*/

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