Compare commits

...

135 Commits

Author SHA1 Message Date
139533d2ca 0.7.2 2019-11-01 14:00:13 +00:00
889682f771 Resolves #491 Lovelace badges parse issue 2019-11-01 13:54:35 +00:00
f16c98057f Location fixes 2019-11-01 13:44:51 +00:00
26ec807c25 Resolves #490 Prevent fused coarse location 2019-10-30 16:54:25 +00:00
45af6cbe3c Fix play_media call 2019-10-30 15:04:23 +00:00
5dd9cde12d Entity page fixes 2019-10-30 14:25:30 +00:00
472fb1d367 Merge pull request #489 from estevez-dev/hotfix/0.7.1
Hotfix/0.7.1
2019-10-29 19:47:33 +02:00
8b372fbc0b Merge branch 'master' into hotfix/0.7.1 2019-10-29 19:47:24 +02:00
40d72eb6e1 0.7.1 2019-10-29 17:46:16 +00:00
ced008a7c1 Resolves #486 Fix for very small screens 2019-10-29 17:44:18 +00:00
d1f652282a Merge pull request #488 from estevez-dev/pre-release/0.7.1
Pre release/0.7.1
2019-10-29 19:35:10 +02:00
f656528d5b 711 2019-10-28 20:42:05 +00:00
bcdb2a648c Fix entity page linking after app resumed 2019-10-28 20:40:30 +00:00
8a78745aa7 State change event log 2019-10-28 20:34:43 +00:00
2a3eaabbe4 Remove some logs 2019-10-28 20:32:23 +00:00
bcd175fbfb Fix service calls 2019-10-28 20:31:45 +00:00
f9f013636d 710 2019-10-28 18:02:03 +00:00
b34cc97080 Show entity page on main page 2019-10-28 17:59:47 +00:00
327f623ef7 Remove HA url from main manu 2019-10-28 16:39:47 +00:00
4d0877e5ae Remove Profile link 2019-10-28 16:36:20 +00:00
0eac217399 Set minimum location update intervhals to 15 minute 2019-10-28 16:28:01 +00:00
9c42ad687d 0.7.0 2019-10-28 10:43:10 +00:00
5cda98da46 whats new key update 2019-10-24 19:48:06 +00:00
958f545f65 Link to location tracking documentation 2019-10-24 19:41:58 +00:00
44165993b4 Integration setting improvement 2019-10-24 19:18:27 +00:00
283ae6cfd4 Integration settings improvements 2019-10-24 18:58:48 +00:00
4068b295bd Battery level for device_tracker 2019-10-24 18:34:38 +00:00
e36b33dcec Update README.md 2019-10-24 14:09:42 +03:00
4b12912697 build 704 2019-10-23 18:24:10 +00:00
49a21967cc Integration settings 2019-10-23 18:22:52 +00:00
cf36406f2a Update README.md 2019-10-22 22:05:15 +03:00
872ad044f1 Update README.md 2019-10-22 22:00:47 +03:00
345301c03a Merge pull request #481 from estevez-dev/pre-release/702
Pre release/702
2019-10-22 21:57:43 +03:00
117923413d Do not use static debug key for CI 2019-10-22 18:48:28 +00:00
24ccbc58c4 703 2019-10-22 18:43:40 +00:00
89c91b4b01 Location tracking improvements 2 2019-10-22 18:42:30 +00:00
4494da1f4f 702 2019-10-21 18:26:12 +00:00
c263542c54 Location tracking improvements 2019-10-21 18:22:25 +00:00
c70f52a73d Merge pull request #480 from estevez-dev/pre-release/0.7.0
build 701
2019-10-21 00:35:22 +03:00
423813d6fb Update README.md 2019-10-21 00:33:53 +03:00
ec6a86f4b0 build 701 2019-10-21 00:13:34 +03:00
64cf18cb23 Debug key 2019-10-20 20:04:53 +00:00
e0e064bc67 build number 700 2019-10-20 18:28:07 +00:00
5cee6cbd9c Remove auth code from logs 2019-10-20 18:22:51 +00:00
43659b26f7 Fix workmanager init 2019-10-20 18:21:51 +00:00
98e15ad429 Resolves #360 Update material design icons to version 4.5.95 2019-10-20 21:14:27 +03:00
90728cdf8b WIP #260 Update MDI parser to 4.5.95 2019-10-20 21:02:57 +03:00
d1ec4f36cc Resolves #49 Location tracking 2019-10-20 17:54:29 +00:00
079070071e Remove webview plugin 2019-10-20 17:18:23 +00:00
520fd6bc38 Migrate athentication from webview to deep linking 2019-10-20 16:45:44 +00:00
085aead36b Add files via upload 2019-10-20 14:30:59 +03:00
fcbaf298cc Delete google-services.json 2019-10-20 14:30:45 +03:00
eedc0c9b22 More gitignore 2019-10-20 10:51:53 +00:00
f70c1e12ff Update gitignore 2019-10-20 10:46:38 +00:00
ec094a4362 Merge pull request #478 from estevez-dev/gitpod
Gitpod configuration
2019-10-18 21:14:11 +03:00
11646c840e Merge branch 'master' into gitpod 2019-10-18 21:13:58 +03:00
86987c57c9 Gitpod configuration 2019-10-18 18:11:06 +00:00
e4d6e842f5 Gitpod configuration 2019-10-18 16:44:01 +00:00
cfe4dd1c59 Vacuum state colors update 2019-10-16 19:39:14 +03:00
3387ab2850 Resolves #416 Vacuum support 2019-10-16 19:34:29 +03:00
abd23e27ea WIP #416 Vacuum support 2019-10-14 15:02:49 +03:00
2f110b20bb Resolves #365 Fix missed group entities 2019-10-14 13:39:00 +03:00
f88e6f9b61 Fix light controls inconsistance 2019-10-14 12:51:20 +03:00
2836973dca Fix light controls issues 2019-10-09 20:45:46 +03:00
a4477e9f83 Resolves #472 entity-filter fix 2019-09-30 21:21:16 +03:00
96fa7ece25 Resolves #444 connection fix 2019-09-30 21:11:37 +03:00
b84caa4cc3 Resolves #469 fix zero position when player paused 2019-09-30 21:05:14 +03:00
49c212632e Fix app restore when on entity page 2019-09-30 20:58:04 +03:00
92165aa7ed Remove intents 2019-09-30 20:30:52 +03:00
cbbdb754aa Packages update 2019-09-26 22:46:52 +03:00
7e3fe0608d Deep links native handler 2019-09-26 13:31:09 +03:00
781f39f281 Deep linking manifest preparation 2019-09-26 13:24:27 +03:00
bfb80f6f8c Resolves #457 Don't send media to unavailable players 2019-09-20 16:51:57 +03:00
801b8f9288 Resolves #459 Send media to the same player 2019-09-20 16:47:02 +03:00
b988fcfcdd Resolves #461 Hide media switch buttons if nothing playing 2019-09-20 16:42:50 +03:00
dff6457cb2 Resolves #462 seek bar for idle players 2019-09-20 16:39:21 +03:00
f50f68f318 Fix video with no duration 2019-09-20 16:15:26 +03:00
c869ad41d9 Packadges update 2019-09-18 21:42:46 +03:00
cd41f9a236 Fix 'Switch to' button 2019-09-18 21:41:02 +03:00
1dbe162bf0 Merge pull request #467 from estevez-dev/release/0.6.7
Release/0.6.7
2019-09-18 21:35:55 +03:00
1a52203bd7 Merge branch 'master' into release/0.6.7 2019-09-18 21:35:46 +03:00
753df3c724 v.0.6.7 2019-09-18 21:18:24 +03:00
dc62a08da3 Fix issue with unnamed view 2019-09-18 21:14:21 +03:00
0c26aff498 672 2019-09-15 20:56:23 +03:00
6323f8f2e6 Whats new url with app version 2019-09-15 20:55:28 +03:00
885c0b1316 Fix stop player when switching to another 2019-09-15 20:27:49 +03:00
14958d9165 Whats new page 2019-09-15 20:23:03 +03:00
bf6a52e0b9 Improve media switching 2019-09-15 18:38:02 +03:00
72aad5cc16 Turn off source player when swicthing media 2019-09-15 17:38:29 +03:00
340e8569cc Switch media to another player 2019-09-15 17:29:49 +03:00
8fc7d0b61e Fix entity state non updated on entity page 2019-09-15 14:34:00 +03:00
5dcb27ada7 Fix no duration crash on media player 2019-09-15 11:10:19 +03:00
db1a076132 Fix media popup menu 2019-09-15 10:48:35 +03:00
6707201e23 Media player controls improvements 2019-09-15 10:39:08 +03:00
b8b92171a8 Media player seek 2019-09-15 01:50:03 +03:00
3dd7069292 Resolves #450 Quick access to active media players 2019-09-15 00:49:49 +03:00
7177419472 Call service with POST instead of waiting for socket 2019-09-14 20:08:10 +03:00
c37313cf07 Some refactoring 2019-09-14 19:53:39 +03:00
a65f42d0fd Hide entity history and attributes under expandepble card 2019-09-14 19:37:52 +03:00
78dd7df686 Entity class refactoring 2019-09-14 19:12:11 +03:00
2ea7d9440c Entity class refactoring 2019-09-14 19:07:21 +03:00
abdcd49368 Fix window resize crash on Chrome OS 2019-09-14 18:54:31 +03:00
6da7a5ab90 Resolves #224 Main UI tablet support 2019-09-14 18:32:44 +03:00
20ffe03139 View widget improvements 2019-09-14 12:31:50 +03:00
a71213c589 Merge pull request #452 from estevez-dev/feature/tablet_ui
Feature/tablet ui
2019-09-14 12:20:13 +03:00
d61103ac42 WIP #224 Dynamic multi column view 2019-09-14 12:18:37 +03:00
298a64b7ae Packages update 2019-09-14 12:18:37 +03:00
9e2c673966 Delete lovelace-card-implementation-request.md 2019-09-14 12:18:37 +03:00
092469d668 Update issue templates 2019-09-14 12:18:37 +03:00
bcf3dab0e2 Update issue templates 2019-09-14 12:18:37 +03:00
7ecfc8a9ff Update issue templates 2019-09-14 12:18:37 +03:00
ecf0a696f7 Create no-response.yml 2019-09-14 12:18:37 +03:00
dc5db28e01 Packages update 2019-09-12 17:08:40 +03:00
555f305c22 Delete lovelace-card-implementation-request.md 2019-09-12 14:07:24 +03:00
76bf07cfcd Update issue templates 2019-09-12 14:06:44 +03:00
c4663576d1 Update issue templates 2019-09-12 14:05:45 +03:00
a64aa73aae Update issue templates 2019-09-12 14:00:16 +03:00
a3a60dd707 Create no-response.yml 2019-09-12 13:36:37 +03:00
9c28b0085b Tablet UI in progress 2019-09-10 15:40:49 +03:00
d5baabdd53 Project structure changes 2019-09-09 18:50:35 +03:00
56a333a852 v.0.6.6 2019-09-09 14:25:27 +03:00
c5922368de Add whars new user message 2019-09-09 14:24:54 +03:00
8c2316a51a Resolves #446 Fix conditional crads 2019-09-09 13:36:33 +03:00
e2e6c015de Fix state color for paused media_player 2019-09-09 12:28:32 +03:00
0a6ff4586d Share media url to HA CLient to play on media_player 2019-09-09 12:25:13 +03:00
fc228d85ae Disable wip card 2019-09-08 19:06:41 +03:00
61823cb43b Merge pull request #445 from estevez-dev/feature/light_card_support
WIP #212 Light card support
2019-09-08 19:05:18 +03:00
127e0b8182 WIP #212 Light card support 2019-09-08 19:04:12 +03:00
38c37fa212 Launch camera view in Chrome custom tab 2019-09-07 19:26:00 +03:00
dfaf2a2924 Project structure change 2019-09-07 18:23:04 +03:00
c90c40c046 Resolves #443 Lovelace view as panel support 2019-09-07 17:58:00 +03:00
d2049b726a Resolves #348 Button entity refactoring 2019-09-07 17:27:23 +03:00
6508f109f7 Minor gauge fixes 2019-09-07 17:04:40 +03:00
37e63637a7 Resolves #348 Glance card improvements 2019-09-07 16:46:41 +03:00
6650c5c145 Resolves #208 Gauge card 2019-09-07 15:47:09 +03:00
110 changed files with 8671 additions and 5158 deletions

41
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,41 @@
---
name: Bug report
about: Create a report to help improve HA Client
title: ''
labels: ''
assignees: ''
---
<!--
Please provide as much information as possible.
-->
**HA Client version:** <!-- Main app menu => About HA Client -->
**Home Assistant version:** <!-- 0.94.1 for example -->
**Device name:** <!-- Pixel 2 for example -->
**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**
<!--
Describe your issue here
-->
**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]
```

View File

@ -0,0 +1,12 @@
---
name: Entity support request
about: Suggest to add support of any entity type
title: ''
labels: ENTITY, feature/improvement
assignees: ''
---
**Entity type:**
**Link to documentation:**

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for HA Client if it is not a card or entity support
title: ''
labels: feature/improvement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,12 @@
---
name: Lovelace Card support request
about: Suggest to add any Lovelace card support
title: ''
labels: CARD, feature/improvement
assignees: ''
---
**Card name:**
**Link to card repository or web page:**

11
.github/no-response.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 14
# Label requiring a response
responseRequiredLabel: more info needed
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. If the issue still relevant
feel free to reopen this issue and add more information, or report a new one.

6
.gitignore vendored
View File

@ -9,6 +9,12 @@ build/
.flutter-plugins .flutter-plugins
.idea/ .idea/
.vscode/
.theia/
.project/
.settings/
flutter_export_environment.sh
key.properties key.properties
premium_features_manager.class.dart premium_features_manager.class.dart

12
.gitpod.dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM gitpod/workspace-full:latest
ENV ANDROID_HOME=/workspace/android-sdk \
FLUTTER_ROOT=/workspace/flutter \
FLUTTER_HOME=/workspace/flutter
USER root
RUN apt-get update && \
apt-get -y install build-essential libkrb5-dev gcc make gradle openjdk-8-jdk && \
apt-get clean && \
apt-get -y autoremove

27
.gitpod.yml Normal file
View File

@ -0,0 +1,27 @@
image:
file: .gitpod.dockerfile
tasks:
- before: |
export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
mkdir -p /home/gitpod/.android
touch /home/gitpod/.android/repositories.cfg
init: |
echo "Installing Flutter SDK..."
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.9.1+hotfix.4-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
echo "Installing Android SDK..."
mkdir -p /workspace/android-sdk && cd /workspace/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip
/workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
echo "Init Flutter..."
cd /workspace/ha_client
flutter upgrade
flutter doctor --android-licenses
flutter pub get
command: |
flutter pub upgrade
echo "Ready to go!"
flutter doctor
vscode:
extensions:
- Dart-Code.dart-code@3.5.0-beta.1:Wg2nTABftVR/Dry4tqeY1w==
- Dart-Code.flutter@3.5.0:/kOacEWdiDRLyN/idUiM4A==

View File

@ -1,9 +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 Lovelace UI support ### With notifications and Lovelace UI support
Visit [homemade.systems](http://ha-client.homemade.systems/) 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 in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn) Discuss it 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)
#### 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)
#### Beta CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)

1
android/.gitignore vendored
View File

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

17
android/.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>android</name>
<comment>Project android created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

6
android/app/.classpath Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

23
android/app/.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View File

@ -50,6 +50,14 @@ android {
} }
signingConfigs { signingConfigs {
if (!System.getenv()["CI"]) {
debug {
keyAlias keystoreProperties['debugKeyAlias']
keyPassword keystoreProperties['debugKeyPassword']
storeFile file(keystoreProperties['debugStoreFile'])
storePassword keystoreProperties['debugStorePassword']
}
}
release { release {
keyAlias keystoreProperties['keyAlias'] keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword'] keyPassword keystoreProperties['keyPassword']

View File

@ -14,6 +14,30 @@
} }
}, },
"oauth_client": [ "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_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"client_type": 3 "client_type": 3
@ -25,15 +49,13 @@
} }
], ],
"services": { "services": {
"analytics_service": {
"status": 1
},
"appinvite_service": { "appinvite_service": {
"status": 1, "other_platform_oauth_client": [
"other_platform_oauth_client": [] {
}, "client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"ads_service": { "client_type": 3
"status": 2 }
]
} }
} }
} }

View File

@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keyboardcrumbs.hassclient"> package="com.keyboardcrumbs.hassclient">
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
flutter needs it to communicate with the running application flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
@ -49,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

@ -4,11 +4,13 @@ import io.flutter.app.FlutterApplication;
import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback; import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
import io.flutter.plugins.GeneratedPluginRegistrant; import io.flutter.plugins.GeneratedPluginRegistrant;
import be.tramckrijte.workmanager.WorkmanagerPlugin;
public class Application extends FlutterApplication implements PluginRegistrantCallback { public class Application extends FlutterApplication implements PluginRegistrantCallback {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
WorkmanagerPlugin.setPluginRegistrantCallback(this);
} }
@Override @Override

View File

@ -3,8 +3,10 @@ package com.keyboardcrumbs.hassclient;
import android.os.Bundle; import android.os.Bundle;
import io.flutter.app.FlutterActivity; import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant; import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.share.FlutterShareReceiverActivity;
public class MainActivity extends FlutterShareReceiverActivity {
public class MainActivity extends FlutterActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);

Binary file not shown.

View File

@ -15,6 +15,10 @@ class HACard {
List states; List states;
List conditions; List conditions;
String content; String content;
String unit;
int min;
int max;
Map severity;
HACard({ HACard({
this.name, this.name,
@ -28,6 +32,10 @@ class HACard {
this.content, this.content,
this.states, this.states,
this.conditions: const [], this.conditions: const [],
this.unit,
this.min,
this.max,
this.severity,
@required this.type @required this.type
}) { }) {
if (this.columnsCount <= 0) { if (this.columnsCount <= 0) {
@ -37,7 +45,7 @@ class HACard {
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;
} }
if (stateFilter.isNotEmpty) { if (stateFilter.isNotEmpty) {

View File

@ -25,14 +25,15 @@ class CardWidget extends StatelessWidget {
} }
if (card.conditions.isNotEmpty) { if (card.conditions.isNotEmpty) {
bool showCardByConditions = false; bool showCardByConditions = true;
for (var condition in card.conditions) { for (var condition in card.conditions) {
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']); Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
if (conditionEntity != null && if (conditionEntity != null &&
(condition['state'] != null && conditionEntity.state == condition['state']) || ((condition['state'] != null && conditionEntity.state != condition['state']) ||
(condition['state_not'] != null && conditionEntity.state != condition['state_not']) (condition['state_not'] != null && conditionEntity.state == condition['state_not']))
) { ) {
showCardByConditions = true; showCardByConditions = false;
break;
} }
} }
if (!showCardByConditions) { if (!showCardByConditions) {
@ -42,31 +43,39 @@ class CardWidget extends StatelessWidget {
switch (card.type) { switch (card.type) {
case CardType.entities: { case CardType.ENTITIES: {
return _buildEntitiesCard(context); return _buildEntitiesCard(context);
} }
case CardType.glance: { case CardType.GLANCE: {
return _buildGlanceCard(context); return _buildGlanceCard(context);
} }
case CardType.mediaControl: { case CardType.MEDIA_CONTROL: {
return _buildMediaControlsCard(context); return _buildMediaControlsCard(context);
} }
case CardType.entityButton: { case CardType.ENTITY_BUTTON: {
return _buildEntityButtonCard(context); return _buildEntityButtonCard(context);
} }
case CardType.markdown: { case CardType.GAUGE: {
return _buildGaugeCard(context);
}
/* case CardType.LIGHT: {
return _buildLightCard(context);
}*/
case CardType.MARKDOWN: {
return _buildMarkdownCard(context); return _buildMarkdownCard(context);
} }
case CardType.alarmPanel: { case CardType.ALARM_PANEL: {
return _buildAlarmPanelCard(context); return _buildAlarmPanelCard(context);
} }
case CardType.horizontalStack: { case CardType.HORIZONTAL_STACK: {
if (card.childCards.isNotEmpty) { if (card.childCards.isNotEmpty) {
List<Widget> children = []; List<Widget> children = [];
card.childCards.forEach((card) { card.childCards.forEach((card) {
@ -89,7 +98,7 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,); return Container(height: 0.0, width: 0.0,);
} }
case CardType.verticalStack: { case CardType.VERTICAL_STACK: {
if (card.childCards.isNotEmpty) { if (card.childCards.isNotEmpty) {
List<Widget> children = []; List<Widget> children = [];
card.childCards.forEach((card) { card.childCards.forEach((card) {
@ -123,19 +132,17 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,); return Container(height: 0.0, width: 0.0,);
} }
List<Widget> body = []; List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name)); body.add(CardHeader(name: card.name));
entitiesToShow.forEach((EntityWrapper entity) { entitiesToShow.forEach((EntityWrapper entity) {
if (!entity.entity.isHidden) { body.add(
body.add( Padding(
Padding( padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0), child: EntityModel(
child: EntityModel( entityWrapper: entity,
entityWrapper: entity, handleTap: true,
handleTap: true, child: entity.entity.buildDefaultWidget(context)
child: entity.entity.buildDefaultWidget(context) ),
), ));
));
}
}); });
return Card( return Card(
child: Padding( child: Padding(
@ -150,7 +157,7 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,); return Container(height: 0.0, width: 0.0,);
} }
List<Widget> body = []; List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name)); body.add(CardHeader(name: card.name));
body.add(MarkdownBody(data: card.content)); body.add(MarkdownBody(data: card.content));
return Card( return Card(
child: Padding( child: Padding(
@ -162,7 +169,7 @@ class CardWidget extends StatelessWidget {
Widget _buildAlarmPanelCard(BuildContext context) { Widget _buildAlarmPanelCard(BuildContext context) {
List<Widget> body = []; List<Widget> body = [];
body.add(CardHeaderWidget( body.add(CardHeader(
name: card.name ?? "", name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}", subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle( style: TextStyle(
@ -183,7 +190,7 @@ class CardWidget extends StatelessWidget {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
) )
) )
] ]
@ -214,39 +221,51 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,); return Container(height: 0.0, width: 0.0,);
} }
List<Widget> rows = []; List<Widget> rows = [];
rows.add(CardHeaderWidget(name: card.name)); rows.add(CardHeader(name: card.name));
List<Widget> result = [];
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length; int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
entitiesToShow.forEach((EntityWrapper entity) {
result.add(
FractionallySizedBox(
widthFactor: 1/columnsCount,
child: EntityModel(
entityWrapper: entity,
child: GlanceEntityContainer(
showName: card.showName,
showState: card.showState,
),
handleTap: true
),
)
);
});
rows.add( rows.add(
Padding( Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding), padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
child: Wrap( child: FractionallySizedBox(
//alignment: WrapAlignment.spaceAround, widthFactor: 1,
runSpacing: Sizes.rowPadding*2, child: LayoutBuilder(
children: result, 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( return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: rows) child: Column(
mainAxisSize: MainAxisSize.min,
children: rows
)
); );
} }
@ -266,7 +285,41 @@ class CardWidget extends StatelessWidget {
return Card( return Card(
child: EntityModel( child: EntityModel(
entityWrapper: card.linkedEntityWrapper, entityWrapper: card.linkedEntityWrapper,
child: ButtonEntityContainer(), 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 handleTap: true
) )
); );
@ -274,7 +327,7 @@ class CardWidget extends StatelessWidget {
Widget _buildUnsupportedCard(BuildContext context) { Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = []; List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name ?? "")); body.add(CardHeader(name: card.name ?? ""));
List<Widget> result = []; List<Widget> result = [];
if (card.linkedEntityWrapper != null) { if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[ result.addAll(<Widget>[

View File

@ -1,12 +1,12 @@
part of '../main.dart'; part of '../../main.dart';
class CardHeaderWidget extends StatelessWidget { class CardHeader extends StatelessWidget {
final String name; final String name;
final Widget trailing; final Widget trailing;
final Widget subtitle; final Widget subtitle;
const CardHeaderWidget({Key key, this.name, 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) {

View File

@ -1,8 +1,8 @@
part of '../main.dart'; part of '../../main.dart';
class ButtonEntityContainer extends StatelessWidget { class EntityButtonCardBody extends StatelessWidget {
ButtonEntityContainer({ EntityButtonCardBody({
Key key, Key key,
}) : super(key: key); }) : super(key: key);
@ -15,24 +15,25 @@ class ButtonEntityContainer extends StatelessWidget {
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) { if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,); return Container(width: 0.0, height: 0.0,);
} }
return InkWell( return InkWell(
onTap: () => entityWrapper.handleTap(), onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(), onLongPress: () => entityWrapper.handleHold(),
child: Column( child: FractionallySizedBox(
mainAxisSize: MainAxisSize.min, widthFactor: 1,
children: <Widget>[ child: Column(
FractionallySizedBox( children: <Widget>[
widthFactor: 0.4, LayoutBuilder(
child: FittedBox( builder: (BuildContext context, BoxConstraints constraints) {
fit: BoxFit.fitHeight, return EntityIcon(
child: EntityIcon( padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0), size: constraints.maxWidth / 2.5,
size: Sizes.iconSize, );
) }
), ),
), _buildName()
_buildName() ],
], ),
), ),
); );
} }

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

@ -1,6 +1,6 @@
part of '../main.dart'; part of '../../main.dart';
class GlanceEntityContainer extends StatelessWidget { class GlanceCardEntityContainer extends StatelessWidget {
final bool showName; final bool showName;
final bool showState; final bool showState;
@ -9,7 +9,7 @@ class GlanceEntityContainer extends StatelessWidget {
final double nameFontSize; final double nameFontSize;
final bool wordsWrapInName; final bool wordsWrapInName;
GlanceEntityContainer({ GlanceCardEntityContainer({
Key key, Key key,
@required this.showName, @required this.showName,
@required this.showState, @required this.showState,
@ -39,10 +39,10 @@ class GlanceEntityContainer extends StatelessWidget {
} }
} }
result.add( result.add(
EntityIcon( EntityIcon(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
size: iconSize, size: iconSize,
) )
); );
if (!nameInTheBottom) { if (!nameInTheBottom) {
if (showState) { if (showState) {
@ -54,14 +54,9 @@ class GlanceEntityContainer extends StatelessWidget {
return Center( return Center(
child: InkResponse( child: InkResponse(
child: ConstrainedBox( child: Column(
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2), mainAxisSize: MainAxisSize.min,
child: Column( children: result,
mainAxisSize: MainAxisSize.min,
//mainAxisAlignment: MainAxisAlignment.start,
//crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
), ),
onTap: () => entityWrapper.handleTap(), onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(), onLongPress: () => entityWrapper.handleHold(),

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

@ -29,6 +29,11 @@ class EntityState {
static const ok = 'ok'; static const ok = 'ok';
static const problem = 'problem'; static const problem = 'problem';
static const active = 'active'; static const active = 'active';
static const cleaning = 'cleaning';
static const docked = 'docked';
static const returning = 'returning';
static const error = 'error';
} }
class EntityUIAction { class EntityUIAction {
@ -77,23 +82,44 @@ class EntityUIAction {
} }
class CardType { class CardType {
static const horizontalStack = "horizontal-stack"; static const HORIZONTAL_STACK = "horizontal-stack";
static const verticalStack = "vertical-stack"; static const VERTICAL_STACK = "vertical-stack";
static const entities = "entities"; static const ENTITIES = "entities";
static const glance = "glance"; static const GLANCE = "glance";
static const mediaControl = "media-control"; static const MEDIA_CONTROL = "media-control";
static const weatherForecast = "weather-forecast"; static const WEATHER_FORECAST = "weather-forecast";
static const thermostat = "thermostat"; static const THERMOSTAT = "thermostat";
static const sensor = "sensor"; static const SENSOR = "sensor";
static const plantStatus = "plant-status"; static const PLANT_STATUS = "plant-status";
static const pictureEntity = "picture-entity"; static const PICTURE_ENTITY = "picture-entity";
static const pictureElements = "picture-elements"; static const PICTURE_ELEMENTS = "picture-elements";
static const picture = "picture"; static const PICTURE = "picture";
static const map = "map"; static const MAP = "map";
static const iframe = "iframe"; static const IFRAME = "iframe";
static const gauge = "gauge"; static const GAUGE = "gauge";
static const entityButton = "entity-button"; static const ENTITY_BUTTON = "entity-button";
static const conditional = "conditional"; static const CONDITIONAL = "conditional";
static const alarmPanel = "alarm-panel"; static const ALARM_PANEL = "alarm-panel";
static const markdown = "markdown"; static const MARKDOWN = "markdown";
static const LIGHT = "light";
}
class Sizes {
static const rightWidgetPadding = 10.0;
static const leftWidgetPadding = 10.0;
static const buttonPadding = 4.0;
static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0;
static const largeIconSize = 46.0;
static const stateFontSize = 15.0;
static const nameFontSize = 15.0;
static const smallFontSize = 14.0;
static const largeFontSize = 24.0;
static const inputWidth = 160.0;
static const rowPadding = 10.0;
static const doubleRowPadding = rowPadding*2;
static const minViewColumnWidth = 350;
static const entityPageMaxWidth = 400.0;
static const mainPageScreenSeparatorWidth = 5.0;
static const tabletMinWidth = minViewColumnWidth + entityPageMaxWidth + 5;
} }

View File

@ -25,9 +25,12 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
void _callService(AlarmControlPanelEntity entity, String service) { void _callService(AlarmControlPanelEntity entity, String service) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, service, entity.entityId, entity.domain,
{"code": "$code"})); service,
entity.entityId,
{"code": "$code"}
);
setState(() { setState(() {
code = ""; code = "";
}); });
@ -58,7 +61,12 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
FlatButton( FlatButton(
child: new Text("Yes"), child: new Text("Yes"),
onPressed: () { onPressed: () {
eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null)); ConnectionManager().callService(
entity.domain,
"alarm_trigger",
entity.entityId,
null
);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../main.dart';
class BadgeWidget extends StatelessWidget { class BadgeWidget extends StatelessWidget {
@override @override
@ -57,7 +57,7 @@ class BadgeWidget extends StatelessWidget {
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) { } else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
stateFontSize = 8.0; stateFontSize = 8.0;
} }
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement; onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
badgeIcon = Center( badgeIcon = Center(
child: Text( child: Text(
"${entityModel.entityWrapper.entity.displayState}", "${entityModel.entityWrapper.entity.displayState}",
@ -140,6 +140,6 @@ class BadgeWidget extends StatelessWidget {
], ],
), ),
onTap: () => onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity))); eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity)));
} }
} }

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../../../main.dart';
class CameraStreamView extends StatefulWidget { class CameraStreamView extends StatefulWidget {
@ -20,21 +20,9 @@ class _CameraStreamViewState extends State<CameraStreamView> {
String streamUrl = ""; String streamUrl = "";
launchStream() { launchStream() {
Navigator.push( Launcher.launchURLInCustomTab(
context, context: context,
MaterialPageRoute( url: streamUrl
builder: (context) => WebviewScaffold(
url: "$streamUrl",
withZoom: true,
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.close),
onPressed: () => Navigator.pop(context)
),
title: new Text("${_entity.displayName}"),
),
),
)
); );
} }

View File

@ -83,7 +83,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_tempThrottleTimer = Timer(Duration(seconds: 2), () { _tempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() { setState(() {
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"})); ConnectionManager().callService(
entity.domain,
"set_temperature",
entity.entityId,
{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
);
_resetStateTimer(entity); _resetStateTimer(entity);
}); });
}); });
@ -101,7 +106,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () { _targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() { setState(() {
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"})); ConnectionManager().callService(
entity.domain,
"set_temperature",
entity.entityId,
{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
);
_resetStateTimer(entity); _resetStateTimer(entity);
}); });
}); });
@ -111,7 +121,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpTargetHumidity = value.roundToDouble(); _tmpTargetHumidity = value.roundToDouble();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"})); ConnectionManager().callService(entity.domain, "set_humidity", entity.entityId, {"humidity": "$_tmpTargetHumidity"});
_resetStateTimer(entity); _resetStateTimer(entity);
}); });
} }
@ -120,7 +130,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpHVACMode = value; _tmpHVACMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"})); ConnectionManager().callService(entity.domain, "set_hvac_mode", entity.entityId, {"hvac_mode": "$_tmpHVACMode"});
_resetStateTimer(entity); _resetStateTimer(entity);
}); });
} }
@ -129,7 +139,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpSwingMode = value; _tmpSwingMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"})); ConnectionManager().callService(entity.domain, "set_swing_mode", entity.entityId, {"swing_mode": "$_tmpSwingMode"});
_resetStateTimer(entity); _resetStateTimer(entity);
}); });
} }
@ -138,7 +148,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpFanMode = value; _tmpFanMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"})); ConnectionManager().callService(entity.domain, "set_fan_mode", entity.entityId, {"fan_mode": "$_tmpFanMode"});
_resetStateTimer(entity); _resetStateTimer(entity);
}); });
} }
@ -147,7 +157,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpPresetMode = value; _tmpPresetMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"})); ConnectionManager().callService(entity.domain, "set_preset_mode", entity.entityId, {"preset_mode": "$_tmpPresetMode"});
_resetStateTimer(entity); _resetStateTimer(entity);
}); });
} }
@ -165,7 +175,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() { setState(() {
_tmpAuxHeat = value; _tmpAuxHeat = value;
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"})); ConnectionManager().callService(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"});
_resetStateTimer(entity); _resetStateTimer(entity);
}); });
} }

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../../../main.dart';
class ModeSelectorWidget extends StatelessWidget { class ModeSelectorWidget extends StatelessWidget {

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../../../main.dart';
class ModeSwitchWidget extends StatelessWidget { class ModeSwitchWidget extends StatelessWidget {

View File

@ -18,7 +18,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
setState(() { setState(() {
_tmpPosition = position.roundToDouble(); _tmpPosition = position.roundToDouble();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()})); ConnectionManager().callService(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;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()})); ConnectionManager().callService(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()});
}); });
} }
@ -135,18 +135,18 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
class CoverTiltControlsWidget extends StatelessWidget { class CoverTiltControlsWidget extends StatelessWidget {
void _open(CoverEntity entity) { void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "open_cover_tilt", entity.entityId, null)); entity.domain, "open_cover_tilt", entity.entityId, null);
} }
void _close(CoverEntity entity) { void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "close_cover_tilt", entity.entityId, null)); entity.domain, "close_cover_tilt", entity.entityId, null);
} }
void _stop(CoverEntity entity) { void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "stop_cover_tilt", entity.entityId, null)); entity.domain, "stop_cover_tilt", entity.entityId, null);
} }
@override @override

View File

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

View File

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

View File

@ -34,32 +34,37 @@ class DefaultEntityContainer extends StatelessWidget {
], ],
); );
} }
return InkWell( Widget result = Row(
onLongPress: () { mainAxisSize: MainAxisSize.max,
if (entityModel.handleTap) { children: <Widget>[
entityModel.entityWrapper.handleHold(); EntityIcon(),
}
},
onTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleTap();
}
},
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
EntityIcon(),
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 3, flex: 3,
child: EntityName( child: EntityName(
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0), padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
),
), ),
state ),
], state
), ],
); );
if (entityModel.handleTap) {
return InkWell(
onLongPress: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleHold();
}
},
onTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleTap();
}
},
child: result,
);
} else {
return result;
}
} }
} }

View File

@ -211,31 +211,6 @@ class Entity {
); );
} }
Widget buildEntityPageWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
),
LastUpdatedWidget(),
Divider(),
_buildAdditionalControlsForPage(context),
Divider(),
buildHistoryWidget(),
EntityAttributesList()
]),
handleTap: false,
);
}
Widget buildHistoryWidget() {
return EntityHistoryWidget(
config: historyConfig,
);
}
Widget buildBadgeWidget(BuildContext context) { Widget buildBadgeWidget(BuildContext context) {
return EntityModel( return EntityModel(
entityWrapper: EntityWrapper(entity: this), entityWrapper: EntityWrapper(entity: this),

View File

@ -14,9 +14,12 @@ class EntityColor {
"auto": Colors.amber, "auto": Colors.amber,
EntityState.active: Colors.amber, EntityState.active: Colors.amber,
EntityState.playing: Colors.amber, EntityState.playing: Colors.amber,
EntityState.paused: Colors.amber,
"above_horizon": Colors.amber, "above_horizon": Colors.amber,
EntityState.home: Colors.amber, EntityState.home: Colors.amber,
EntityState.open: Colors.amber, EntityState.open: Colors.amber,
EntityState.cleaning: Colors.amber,
EntityState.returning: Colors.amber,
EntityState.off: defaultStateColor, EntityState.off: defaultStateColor,
EntityState.closed: defaultStateColor, EntityState.closed: defaultStateColor,
"below_horizon": defaultStateColor, "below_horizon": defaultStateColor,

View File

@ -0,0 +1,70 @@
part of '../main.dart';
class EntityPageLayout extends StatelessWidget {
final bool showClose;
final Entity entity;
EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key);
@override
Widget build(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: entity),
child: ListView(
padding: EdgeInsets.all(0),
children: <Widget>[
showClose ?
Container(
color: Colors.blue[300],
height: 40,
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: 36.0,
onPressed: () {
eventBus.fire(ShowEntityPageEvent());
},
)
],
),
) :
Container(height: 0, width: 0,),
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),
),
LastUpdatedWidget(),
Divider(),
entity._buildAdditionalControlsForPage(context),
Divider(),
SpoilerCard(
title: "State history",
body: EntityHistoryWidget(),
),
SpoilerCard(
title: "Attributes",
body: EntityAttributesList(),
),
]
),
handleTap: false,
);
}
}

View File

@ -4,6 +4,7 @@ class EntityWrapper {
String displayName; String displayName;
String icon; String icon;
String unitOfMeasurement;
String entityPicture; String entityPicture;
EntityUIAction uiAction; EntityUIAction uiAction;
Entity entity; Entity entity;
@ -24,23 +25,22 @@ class EntityWrapper {
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: {
eventBus.fire( ConnectionManager().callService("homeassistant", "toggle", entity.entityId, null);
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break; break;
} }
case EntityUIAction.callService: { case EntityUIAction.callService: {
if (uiAction.tapService != null) { if (uiAction.tapService != null) {
eventBus.fire( ConnectionManager().callService(uiAction.tapService.split(".")[0],
ServiceCallEvent(uiAction.tapService.split(".")[0],
uiAction.tapService.split(".")[1], null, uiAction.tapService.split(".")[1], null,
uiAction.tapServiceData)); uiAction.tapServiceData);
} }
break; break;
} }
@ -51,7 +51,7 @@ class EntityWrapper {
case EntityUIAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entity)); new ShowEntityPageEvent(entity: entity));
break; break;
} }
@ -74,24 +74,22 @@ class EntityWrapper {
void handleHold() { void handleHold() {
switch (uiAction.holdAction) { switch (uiAction.holdAction) {
case EntityUIAction.toggle: { case EntityUIAction.toggle: {
eventBus.fire( ConnectionManager().callService("homeassistant", "toggle", entity.entityId, null);
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break; break;
} }
case EntityUIAction.callService: { case EntityUIAction.callService: {
if (uiAction.holdService != null) { if (uiAction.holdService != null) {
eventBus.fire( ConnectionManager().callService(uiAction.holdService.split(".")[0],
ServiceCallEvent(uiAction.holdService.split(".")[0],
uiAction.holdService.split(".")[1], null, uiAction.holdService.split(".")[1], null,
uiAction.holdServiceData)); uiAction.holdServiceData);
} }
break; break;
} }
case EntityUIAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entity)); new ShowEntityPageEvent(entity: entity));
break; break;
} }

View File

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

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../main.dart';
class FlatServiceButton extends StatelessWidget { class FlatServiceButton extends StatelessWidget {
@ -18,7 +18,7 @@ class FlatServiceButton extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
void _setNewState() { void _setNewState() {
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null)); ConnectionManager().callService(serviceDomain, serviceName, entityId, null);
} }
@override @override

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../../../main.dart';
class LightColorPicker extends StatefulWidget { class LightColorPicker extends StatefulWidget {
@ -27,7 +27,6 @@ class LightColorPickerState extends State<LightColorPicker> {
List<Widget> colorRows = []; List<Widget> colorRows = [];
Border border; Border border;
bool isSomethingSelected = false; bool isSomethingSelected = false;
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) { for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
List<Widget> rowChildren = []; List<Widget> rowChildren = [];
//Logger.d("$saturation"); //Logger.d("$saturation");

View File

@ -28,9 +28,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpBrightness = value.round(); _tmpBrightness = value.round();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, entity.domain, "turn_on", entity.entityId,
{"brightness": _tmpBrightness})); {"brightness": _tmpBrightness});
}); });
} }
@ -38,9 +38,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpWhiteValue = value.round(); _tmpWhiteValue = value.round();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, entity.domain, "turn_on", entity.entityId,
{"white_value": _tmpWhiteValue})); {"white_value": _tmpWhiteValue});
}); });
} }
@ -49,9 +49,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() { setState(() {
_tmpColorTemp = value.round(); _tmpColorTemp = value.round();
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, entity.domain, "turn_on", entity.entityId,
{"color_temp": _tmpColorTemp})); {"color_temp": _tmpColorTemp});
}); });
} }
@ -60,9 +60,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpColor = color; _tmpColor = color;
_changedHere = true; _changedHere = true;
Logger.d( "HS Color: [${color.hue}, ${color.saturation}]"); Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, entity.domain, "turn_on", entity.entityId,
{"hs_color": [color.hue, color.saturation*100]})); {"hs_color": [color.hue, color.saturation*100]});
}); });
} }
@ -71,9 +71,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpEffect = value; _tmpEffect = value;
_changedHere = true; _changedHere = true;
if (_tmpEffect != null) { if (_tmpEffect != null) {
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, entity.domain, "turn_on", entity.entityId,
{"effect": "$value"})); {"effect": "$value"});
} }
}); });
} }
@ -100,7 +100,19 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
} }
Widget _buildBrightnessControl(LightEntity entity) { Widget _buildBrightnessControl(LightEntity entity) {
if ((entity.supportBrightness) && (_tmpBrightness != null)) { if (entity.supportBrightness) {
double val;
if (_tmpBrightness != null) {
if (_tmpBrightness > 255) {
val = 255;
} else if (_tmpBrightness < 1) {
val = 1;
} else {
val = _tmpBrightness.toDouble();
}
} else {
val = 1;
}
return UniversalSlider( return UniversalSlider(
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
@ -111,7 +123,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
min: 1.0, min: 1.0,
max: 255.0, max: 255.0,
onChangeEnd: (value) => _setBrightness(entity, value), onChangeEnd: (value) => _setBrightness(entity, value),
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(), value: val,
leading: Icon(Icons.brightness_5), leading: Icon(Icons.brightness_5),
title: "Brightness", title: "Brightness",
); );
@ -143,10 +155,22 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildColorTempControl(LightEntity entity) { Widget _buildColorTempControl(LightEntity entity) {
if (entity.supportColorTemp) { if (entity.supportColorTemp) {
double val;
if (_tmpColorTemp != null) {
if (_tmpColorTemp > entity.maxMireds) {
val = entity.maxMireds;
} else if (_tmpColorTemp < entity.minMireds) {
val = entity.minMireds;
} else {
val = _tmpColorTemp.toDouble();
}
} else {
val = entity.minMireds;
}
return UniversalSlider( return UniversalSlider(
title: "Color temperature", title: "Color temperature",
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),), leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
value: _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(), value: val,
onChangeEnd: (value) => _setColorTemp(entity, value), onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds, max: entity.maxMireds,
min: entity.minMireds, min: entity.minMireds,
@ -203,10 +227,14 @@ 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)) {
List<String> list = List.from(entity.effectList);
if (_tmpEffect!= null && !list.contains(_tmpEffect)) {
list.insert(0, _tmpEffect);
}
return ModeSelectorWidget( return ModeSelectorWidget(
onChange: (effect) => _setEffect(entity, effect), onChange: (effect) => _setEffect(entity, effect),
caption: "Effect", caption: "Effect",
options: entity.effectList, options: list,
value: _tmpEffect value: _tmpEffect
); );
} else { } else {

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) {
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null)); ConnectionManager().callService("lock", "lock", entity.entityId, null);
} }
void _unlock(Entity entity) { void _unlock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null)); ConnectionManager().callService("lock", "unlock", entity.entityId, null);
} }
@override @override

View File

@ -74,10 +74,37 @@ class MediaPlayerEntity extends Entity {
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list"); List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
List<String> get sourceList => getStringListAttributeValue("source_list"); List<String> get sourceList => getStringListAttributeValue("source_list");
DateTime get positionLastUpdated => DateTime.tryParse("${attributes["media_position_updated_at"]}")?.toLocal();
int get durationSeconds => _getIntAttributeValue("media_duration");
int get positionSeconds => _getIntAttributeValue("media_position");
@override @override
Widget _buildAdditionalControlsForPage(BuildContext context) { Widget _buildAdditionalControlsForPage(BuildContext context) {
return MediaPlayerControls(); return MediaPlayerControls();
} }
bool canCalculateActualPosition() {
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds >= 0;
}
double getActualPosition() {
double result = 0;
if (canCalculateActualPosition()) {
Duration durationD;
Duration positionD;
durationD = Duration(seconds: durationSeconds);
positionD = Duration(
seconds: positionSeconds);
result = positionD.inSeconds.toDouble();
int differenceInSeconds = DateTime
.now()
.difference(positionLastUpdated)
.inSeconds;
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
}
return result;
}
} }

View File

@ -0,0 +1,46 @@
part of '../../../main.dart';
class MediaPlayerProgressBar extends StatefulWidget {
@override
_MediaPlayerProgressBarState createState() => _MediaPlayerProgressBarState();
}
class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
Timer _timer;
@override
initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
setState(() {
});
});
}
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress;
int currentPosition;
if (entity.canCalculateActualPosition()) {
currentPosition = entity.getActualPosition().toInt();
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
} else {
progress = 0;
}
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}

View File

@ -0,0 +1,137 @@
part of '../../../main.dart';
class MediaPlayerSeekBar extends StatefulWidget {
@override
_MediaPlayerSeekBarState createState() => _MediaPlayerSeekBarState();
}
class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
Timer _timer;
bool _seekStarted = false;
bool _changedHere = false;
double _currentPosition = 0;
int _savedPosition = 0;
final TextStyle _seekTextStyle = TextStyle(
fontSize: 20,
color: Colors.blue,
fontWeight: FontWeight.bold
);
@override
initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
if (!_seekStarted && !_changedHere) {
setState(() {});
}
});
}
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
if (entity.canCalculateActualPosition() && entity.state != EntityState.idle) {
if (HomeAssistant().sendToPlayerId == entity.entityId && HomeAssistant().savedPlayerPosition != null) {
_savedPosition = HomeAssistant().savedPlayerPosition;
HomeAssistant().savedPlayerPosition = null;
HomeAssistant().sendToPlayerId = null;
}
if (entity.state == EntityState.playing && !_seekStarted &&
!_changedHere) {
_currentPosition = entity.getActualPosition();
} else if (entity.state == EntityState.paused) {
_currentPosition = entity.positionSeconds.toDouble();
} else if (_changedHere) {
_changedHere = false;
}
List<Widget> buttons = [];
if (_savedPosition > 0) {
buttons.add(
RaisedButton(
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
color: Colors.orange,
focusColor: Colors.white,
onPressed: () {
ConnectionManager().callService(
"media_player",
"media_seek",
"${entity.entityId}",
{"seek_position": _savedPosition}
);
setState(() {
_savedPosition = 0;
});
},
)
);
}
return Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 20, Sizes.rightWidgetPadding, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text("00:00"),
Expanded(
child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle),
),
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
],
),
Container(height: 10,),
Slider(
min: 0,
activeColor: Colors.amber,
inactiveColor: Colors.black26,
max: entity.durationSeconds.toDouble(),
value: _currentPosition,
onChangeStart: (val) {
_seekStarted = true;
},
onChanged: (val) {
setState(() {
_currentPosition = val;
});
},
onChangeEnd: (val) {
_seekStarted = false;
Timer(Duration(milliseconds: 500), () {
if (!_seekStarted) {
ConnectionManager().callService(
"media_player",
"media_seek",
"${entity.entityId}",
{"seek_position": val}
);
setState(() {
_changedHere = true;
_currentPosition = val;
});
}
});
},
),
ButtonBar(
children: buttons,
)
],
),
);
} else {
return Container(width: 0, height: 0,);
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}

View File

@ -26,7 +26,7 @@ class MediaPlayerWidget extends StatelessWidget {
bottom: 0.0, bottom: 0.0,
left: 0.0, left: 0.0,
right: 0.0, right: 0.0,
child: MediaPlayerProgressWidget() child: MediaPlayerProgressBar()
) )
], ],
), ),
@ -121,23 +121,23 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) { if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
if (entity.state == EntityState.off) { if (entity.state == EntityState.off) {
Logger.d("${entity.entityId} turn_on"); Logger.d("${entity.entityId} turn_on");
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId, entity.domain, "turn_on", entity.entityId,
null)); null);
} else { } else {
Logger.d("${entity.entityId} turn_off"); Logger.d("${entity.entityId} turn_off");
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "turn_off", entity.entityId, entity.domain, "turn_off", entity.entityId,
null)); null);
} }
} }
} }
void _callAction(MediaPlayerEntity entity, String action) { void _callAction(MediaPlayerEntity entity, String action) {
Logger.d("${entity.entityId} $action"); Logger.d("${entity.entityId} $action");
eventBus.fire(new ServiceCallEvent( ConnectionManager().callService(
entity.domain, "$action", entity.entityId, entity.domain, "$action", entity.entityId,
null)); null);
} }
@override @override
@ -229,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(entity)) 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) {
@ -264,27 +264,27 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
setState(() { setState(() {
_changedHere = true; _changedHere = true;
_newVolumeLevel = value; _newVolumeLevel = value;
eventBus.fire(ServiceCallEvent("media_player", "volume_set", entityId, {"volume_level": value})); ConnectionManager().callService("media_player", "volume_set", entityId, {"volume_level": value});
}); });
} }
void _setVolumeMute(bool isMuted, String entityId) { void _setVolumeMute(bool isMuted, String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_mute", entityId, {"is_volume_muted": isMuted})); ConnectionManager().callService("media_player", "volume_mute", entityId, {"is_volume_muted": isMuted});
} }
void _setVolumeUp(String entityId) { void _setVolumeUp(String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_up", entityId, null)); ConnectionManager().callService("media_player", "volume_up", entityId, null);
} }
void _setVolumeDown(String entityId) { void _setVolumeDown(String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_down", entityId, null)); ConnectionManager().callService("media_player", "volume_down", entityId, null);
} }
void _setSoundMode(String value, String entityId) { void _setSoundMode(String value, String entityId) {
setState(() { setState(() {
_newSoundMode = value; _newSoundMode = value;
_changedHere = true; _changedHere = true;
eventBus.fire(ServiceCallEvent("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"})); ConnectionManager().callService("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"});
}); });
} }
@ -292,7 +292,7 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
setState(() { setState(() {
_newSource = source; _newSource = source;
_changedHere = true; _changedHere = true;
eventBus.fire(ServiceCallEvent("media_player", "select_source", entityId, {"source": "$source"})); ConnectionManager().callService("media_player", "select_source", entityId, {"source": "$source"});
}); });
} }
@ -305,6 +305,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
) )
]; ];
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) { if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
if (entity.supportSeek) {
children.add(MediaPlayerSeekBar());
} else {
children.add(MediaPlayerProgressBar());
}
Widget muteWidget; Widget muteWidget;
Widget volumeStepWidget; Widget volumeStepWidget;
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) { if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
@ -398,69 +403,43 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
) )
); );
} }
if (entity.state == EntityState.playing || entity.state == EntityState.paused) {
children.add(
ButtonBar(
children: <Widget>[
RaisedButton(
child: Text("Duplicate to"),
color: Colors.blue,
textColor: Colors.white,
onPressed: () => _duplicateTo(entity),
),
RaisedButton(
child: Text("Switch to"),
color: Colors.blue,
textColor: Colors.white,
onPressed: () => _switchTo(entity),
)
],
)
);
}
} }
return Column( return Column(
children: children, children: children,
); );
} }
} void _duplicateTo(entity) {
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
class MediaPlayerProgressWidget extends StatefulWidget { Navigator.of(context).pushNamed("/play-media", arguments: {
@override "url": entity.attributes["media_content_id"],
_MediaPlayerProgressWidgetState createState() => _MediaPlayerProgressWidgetState(); "type": entity.attributes["media_content_type"]
} });
class _MediaPlayerProgressWidgetState extends State<MediaPlayerProgressWidget> {
Timer _timer;
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress;
try {
DateTime lastUpdated = DateTime.parse(
entity.attributes["media_position_updated_at"]).toLocal();
Duration duration = Duration(seconds: entity._getIntAttributeValue("media_duration") ?? 1);
Duration position = Duration(seconds: entity._getIntAttributeValue("media_position") ?? 0);
int currentPosition = position.inSeconds;
if (entity.state == EntityState.playing) {
_timer?.cancel();
_timer = Timer(Duration(seconds: 1), () {
setState(() {
});
});
int differenceInSeconds = DateTime
.now()
.difference(lastUpdated)
.inSeconds;
currentPosition = currentPosition + differenceInSeconds;
} else {
_timer?.cancel();
}
progress = currentPosition / duration.inSeconds;
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
);
} catch (e) {
_timer?.cancel();
progress = 0.0;
}
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
);
} }
@override void _switchTo(entity) {
void dispose() { HomeAssistant().sendFromPlayerId = entity.entityId;
_timer?.cancel(); _duplicateTo(entity);
super.dispose();
} }
} }

View File

@ -11,8 +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) {
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId, ConnectionManager().callService(domain, "select_option", entityId,
{"option": "$newValue"})); {"option": "$newValue"});
} }
@override @override

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../main.dart';
class SimpleEntityState extends StatelessWidget { class SimpleEntityState extends StatelessWidget {
@ -7,8 +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 double fontSize;
final bool bold;
const SimpleEntityState({Key key, this.maxLines: 10, 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); 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) {
@ -21,18 +23,22 @@ class SimpleEntityState extends StatelessWidget {
state = customValue; state = customValue;
} }
TextStyle textStyle = TextStyle( TextStyle textStyle = TextStyle(
fontSize: Sizes.stateFontSize, fontSize: this.fontSize,
fontWeight: FontWeight.normal
); );
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) { if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
textStyle = textStyle.apply(color: Colors.blue); textStyle = textStyle.apply(color: Colors.blue);
} }
if (this.bold) {
textStyle = textStyle.apply(fontWeightDelta: 100);
}
while (state.contains(" ")){ while (state.contains(" ")){
state = state.replaceAll(" ", " "); state = state.replaceAll(" ", " ");
} }
Widget result = Padding( Widget result = Padding(
padding: padding, padding: padding,
child: Text( child: Text(
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}", "$state ${entityModel.entityWrapper.unitOfMeasurement}",
textAlign: textAlign, textAlign: textAlign,
maxLines: maxLines, maxLines: maxLines,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@ -18,8 +18,8 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
_newValue = newValue; _newValue = newValue;
_changedHere = true; _changedHere = true;
}); });
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId, ConnectionManager().callService(domain, "set_value", entityId,
{"value": "${newValue.toString()}"})); {"value": "${newValue.toString()}"});
} }
@override @override

View File

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

View File

@ -26,8 +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)) {
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId, ConnectionManager().callService(domain, "set_value", entityId,
{"value": "$newValue"})); {"value": "$newValue"});
} else { } else {
setState(() { setState(() {
_tmpValue = _entityState; _tmpValue = _entityState;

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../main.dart';
class UniversalSlider extends StatelessWidget { class UniversalSlider extends StatelessWidget {

View File

@ -0,0 +1,98 @@
part of '../../main.dart';
class VacuumEntity extends Entity {
static const SUPPORT_TURN_ON = 1;
static const SUPPORT_TURN_OFF = 2;
static const SUPPORT_PAUSE = 4;
static const SUPPORT_STOP = 8;
static const SUPPORT_RETURN_HOME = 16;
static const SUPPORT_FAN_SPEED = 32;
static const SUPPORT_BATTERY = 64;
static const SUPPORT_STATUS = 128;
static const SUPPORT_SEND_COMMAND = 256;
static const SUPPORT_LOCATE = 512;
static const SUPPORT_CLEAN_SPOT = 1024;
static const SUPPORT_MAP = 2048;
static const SUPPORT_STATE = 4096;
static const SUPPORT_START = 8192;
VacuumEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportTurnOn => ((supportedFeatures &
VacuumEntity.SUPPORT_TURN_ON) ==
VacuumEntity.SUPPORT_TURN_ON);
bool get supportTurnOff => ((supportedFeatures &
VacuumEntity.SUPPORT_TURN_OFF) ==
VacuumEntity.SUPPORT_TURN_OFF);
bool get supportPause => ((supportedFeatures &
VacuumEntity.SUPPORT_PAUSE) ==
VacuumEntity.SUPPORT_PAUSE);
bool get supportStop => ((supportedFeatures &
VacuumEntity.SUPPORT_STOP) ==
VacuumEntity.SUPPORT_STOP);
bool get supportReturnHome => ((supportedFeatures &
VacuumEntity.SUPPORT_RETURN_HOME) ==
VacuumEntity.SUPPORT_RETURN_HOME);
bool get supportFanSpeed => ((supportedFeatures &
VacuumEntity.SUPPORT_FAN_SPEED) ==
VacuumEntity.SUPPORT_FAN_SPEED);
bool get supportBattery => ((supportedFeatures &
VacuumEntity.SUPPORT_BATTERY) ==
VacuumEntity.SUPPORT_BATTERY);
bool get supportStatus => ((supportedFeatures &
VacuumEntity.SUPPORT_STATUS) ==
VacuumEntity.SUPPORT_STATUS);
bool get supportSendCommand => ((supportedFeatures &
VacuumEntity.SUPPORT_SEND_COMMAND) ==
VacuumEntity.SUPPORT_SEND_COMMAND);
bool get supportLocate => ((supportedFeatures &
VacuumEntity.SUPPORT_LOCATE) ==
VacuumEntity.SUPPORT_LOCATE);
bool get supportCleanSpot => ((supportedFeatures &
VacuumEntity.SUPPORT_CLEAN_SPOT) ==
VacuumEntity.SUPPORT_CLEAN_SPOT);
bool get supportMap => ((supportedFeatures &
VacuumEntity.SUPPORT_MAP) ==
VacuumEntity.SUPPORT_MAP);
bool get supportState => ((supportedFeatures &
VacuumEntity.SUPPORT_STATE) ==
VacuumEntity.SUPPORT_STATE);
bool get supportStart => ((supportedFeatures &
VacuumEntity.SUPPORT_START) ==
VacuumEntity.SUPPORT_START);
List<String> get fanSpeedList => getStringListAttributeValue("fan_speed_list");
String get fanSpeed => getAttribute("fan_speed");
String get status => getAttribute("status");
int get batteryLevel => _getIntAttributeValue("battery_level");
String get batteryIcon => getAttribute("battery_icon");
double get cleanedArea => _getDoubleAttributeValue("cleaned_area");
@override
Widget _buildStatePart(BuildContext context) {
if (supportTurnOn || supportTurnOff) {
return SwitchStateWidget(
domainForService: "vacuum",
);
} else {
return SimpleEntityState();
}
}
@override
Widget _buildStatePartForPage(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(
entity: this
),
child: VacuumStateButton(),
handleTap: false,
);
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return VacuumControls();
}
}

View File

@ -0,0 +1,239 @@
part of '../../../main.dart';
class VacuumControls extends StatelessWidget {
@override
Widget build(BuildContext context) {
VacuumEntity entity = EntityModel.of(context).entityWrapper.entity;
return Padding(
padding: EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildStatusAndBattery(entity),
_buildCommands(entity),
_buildFanSpeed(entity),
_buildAdditionalInfo(entity)
],
),
);
}
Widget _buildStatusAndBattery(VacuumEntity entity) {
List<Widget> result = [];
if (entity.supportStatus) {
result.addAll(
<Widget>[
Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),),
Container(width: 6,),
Expanded(
//flex: 1,
child: Text(
"${entity.status}",
maxLines: 1,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: Sizes.stateFontSize,
fontWeight: FontWeight.bold
),
),
),
]
);
}
if (entity.supportBattery && entity.batteryLevel != null) {
String iconName = entity.batteryIcon ?? "mdi:battery";
int batteryLevel = entity.batteryLevel ?? 100;
result.addAll(<Widget>[
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
Container(width: 6,),
Text("$batteryLevel %", style: TextStyle(fontSize: Sizes.stateFontSize))
]
);
}
if (result.isEmpty) {
return Container(width: 0, height: 0);
}
return Padding(
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
);
}
Widget _buildCommands(VacuumEntity entity) {
List<Widget> commandButtons = [];
double iconSize = 32;
if (entity.supportStart) {
commandButtons.add(
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:play")),
iconSize: iconSize,
onPressed: () => ConnectionManager().callService(
"vacuum",
entity.entityId,
"start",
null
),
)
);
}
if (entity.supportPause && !entity.supportStart) {
commandButtons.add(
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:play-pause")),
iconSize: iconSize,
onPressed: () => ConnectionManager().callService(
"vacuum",
entity.entityId,
"start_pause",
null
),
)
);
} else if (entity.supportPause) {
commandButtons.add(
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:pause")),
iconSize: iconSize,
onPressed: () => ConnectionManager().callService(
"vacuum",
entity.entityId,
"pause",
null
),
)
);
}
if (entity.supportStop) {
commandButtons.add(
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:stop")),
iconSize: iconSize,
onPressed: () => ConnectionManager().callService(
"vacuum",
entity.entityId,
"stop",
null
),
)
);
}
if (entity.supportCleanSpot) {
commandButtons.add(
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:broom")),
iconSize: iconSize,
onPressed: () => ConnectionManager().callService(
"vacuum",
entity.entityId,
"clean_spot",
null
),
)
);
}
if (entity.supportLocate) {
commandButtons.add(
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:map-marker")),
iconSize: iconSize,
onPressed: () => ConnectionManager().callService(
"vacuum",
entity.entityId,
"locate",
null
),
)
);
}
if (entity.supportReturnHome) {
commandButtons.add(
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-map-marker")),
iconSize: iconSize,
onPressed: () => ConnectionManager().callService(
"vacuum",
entity.entityId,
"return_to_base",
null
),
)
);
}
if (commandButtons.isEmpty) {
return Container(width: 0, height: 0,);
}
return Padding(
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Vacuum cleaner commands:", style: TextStyle(fontSize: Sizes.stateFontSize)),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.max,
children: commandButtons.map((button) => Expanded(
child: button,
)).toList(),
)
],
),
);
}
Widget _buildFanSpeed(VacuumEntity entity) {
if (entity.supportFanSpeed) {
return Padding(
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
child: ModeSelectorWidget(
caption: "Fan speed",
options: entity.fanSpeedList,
value: entity.fanSpeed,
onChange: (val) => ConnectionManager().callService(
"vacuum",
entity.entityId,
"set_fan_speed",
{"fan_speed": val}
)
),
);
} else {
return Container(width: 0, height: 0,);
}
}
Widget _buildAdditionalInfo(VacuumEntity entity) {
List<Widget> rows = [];
if (entity.cleanedArea != null) {
rows.add(
Text("Cleaned area: ${entity.cleanedArea}")
);
}
if (rows.isEmpty) {
return Container(width: 0, height: 0,);
}
return Padding(
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: rows,
),
);
}
}

View File

@ -0,0 +1,40 @@
part of '../../../main.dart';
class VacuumStateButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget result;
VacuumEntity entity = EntityModel.of(context).entityWrapper.entity;
if (entity.supportTurnOn && entity.supportTurnOff) {
result = FlatServiceButton(
serviceDomain: "vacuum",
serviceName: entity.state == EntityState.on ? "turn_off" : "turn_on",
entityId: entity.entityId,
text: entity.state == EntityState.on ? "TURN OFF" : "TURN ON"
);
} else if (entity.supportStart && (entity.state == EntityState.docked || entity.state == EntityState.idle)) {
result = FlatServiceButton(
serviceDomain: "vacuum",
serviceName: "start",
entityId: entity.entityId,
text: "START CLEANING"
);
} else if (entity.supportReturnHome && entity.state == EntityState.cleaning) {
result = FlatServiceButton(
serviceDomain: "vacuum",
serviceName: "return_to_base",
entityId: entity.entityId,
text: "RETURN TO DOCK"
);
} else {
result = Text(entity.state.toUpperCase(), style: TextStyle(
fontSize: 16,
color: Colors.grey
));
}
return Padding(
padding: EdgeInsets.only(right: 15),
child: result,
);
}
}

View File

@ -101,6 +101,9 @@ class EntityCollection {
case "timer": { case "timer": {
return TimerEntity(rawEntityData, homeAssistantWebHost); return TimerEntity(rawEntityData, homeAssistantWebHost);
} }
case "vacuum": {
return VacuumEntity(rawEntityData, homeAssistantWebHost);
}
default: { default: {
return Entity(rawEntityData, homeAssistantWebHost); return Entity(rawEntityData, homeAssistantWebHost);
} }
@ -149,6 +152,13 @@ class EntityCollection {
return _allEntities[entityId] != null; return _allEntities[entityId] != null;
} }
List<Entity> getByDomains({List<String> domains, List<String> stateFiler}) {
return _allEntities.values.where((entity) {
return domains.contains(entity.domain) &&
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
}).toList();
}
List<Entity> filterEntitiesForDefaultView() { List<Entity> filterEntitiesForDefaultView() {
List<Entity> result = []; List<Entity> result = [];
List<Entity> groups = []; List<Entity> groups = [];

View File

@ -1,14 +0,0 @@
part of '../main.dart';
class EntityPageContainer extends StatelessWidget {
EntityPageContainer({Key key, @required this.children}) : super(key: key);
final List<Widget> children;
@override
Widget build(BuildContext context) {
return ListView(
children: children,
);
}
}

View File

@ -11,8 +11,13 @@ class HomeAssistant {
EntityCollection entities; EntityCollection entities;
HomeAssistantUI ui; HomeAssistantUI ui;
Map _instanceConfig = {}; Map _instanceConfig = {};
Map services;
String _userName; String _userName;
bool childMode;
HSVColor savedColor; HSVColor savedColor;
int savedPlayerPosition;
String sendToPlayerId;
String sendFromPlayerId;
String fcmToken; String fcmToken;
@ -64,7 +69,7 @@ class HomeAssistant {
)); ));
Future.wait(futures).then((_) { Future.wait(futures).then((_) {
if (isMobileAppEnabled) { if (isMobileAppEnabled) {
_createUI(); if (!childMode) _createUI();
_fetchCompleter.complete(); _fetchCompleter.complete();
MobileAppIntegrationManager.checkAppRegistration(); MobileAppIntegrationManager.checkAppRegistration();
} else { } else {
@ -109,14 +114,21 @@ class HomeAssistant {
Future _getUserInfo() async { Future _getUserInfo() async {
_userName = null; _userName = null;
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) { await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) {
Logger.w("Can't get user info: ${e}"); _userName = data["name"];
childMode = _userName.startsWith("[child]");
}).catchError((e) {
Logger.w("Can't get user info: $e");
}); });
} }
Future _getServices() async { Future _getServices() async {
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) { await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
Logger.w("Can't get services: ${e}"); Logger.d("Got ${data.length} services");
Logger.d("Media extractor: ${data["media_extractor"]}");
services = data;
}).catchError((e) {
Logger.w("Can't get services: $e");
}); });
} }
@ -162,14 +174,23 @@ class HomeAssistant {
count: viewCounter, count: viewCounter,
id: "${rawView['id']}", id: "${rawView['id']}",
name: rawView['title'], name: rawView['title'],
iconName: rawView['icon'] iconName: rawView['icon'],
panel: rawView['panel'] ?? false,
); );
if (rawView['badges'] != null && rawView['badges'] is List) { if (rawView['badges'] != null && rawView['badges'] is List) {
rawView['badges'].forEach((entity) { rawView['badges'].forEach((entity) {
if (entities.isExist(entity)) { if (entity is String) {
Entity e = entities.get(entity); if (entities.isExist(entity)) {
view.badges.add(e); Entity e = entities.get(entity);
view.badges.add(e);
}
} else {
String eId = '${entity['entity']}';
if (entities.isExist(eId)) {
Entity e = entities.get(eId);
view.badges.add(e);
}
} }
}); });
} }
@ -191,7 +212,7 @@ class HomeAssistant {
HACard card = HACard( HACard card = HACard(
id: "card", id: "card",
name: rawCardInfo["title"] ?? rawCardInfo["name"], name: rawCardInfo["title"] ?? rawCardInfo["name"],
type: rawCardInfo['type'] ?? CardType.entities, type: rawCardInfo['type'] ?? CardType.ENTITIES,
columnsCount: rawCardInfo['columns'] ?? 4, columnsCount: rawCardInfo['columns'] ?? 4,
showName: rawCardInfo['show_name'] ?? true, showName: rawCardInfo['show_name'] ?? true,
showState: rawCardInfo['show_state'] ?? true, showState: rawCardInfo['show_state'] ?? true,
@ -199,12 +220,17 @@ class HomeAssistant {
stateFilter: rawCardInfo['state_filter'] ?? [], stateFilter: rawCardInfo['state_filter'] ?? [],
states: rawCardInfo['states'], states: rawCardInfo['states'],
conditions: rawCard['conditions'] ?? [], conditions: rawCard['conditions'] ?? [],
content: rawCardInfo['content'] content: rawCardInfo['content'],
min: rawCardInfo['min'] ?? 0,
max: rawCardInfo['max'] ?? 100,
unit: rawCardInfo['unit'],
severity: rawCardInfo['severity']
); );
if (rawCardInfo["cards"] != null) { if (rawCardInfo["cards"] != null) {
card.childCards = _createLovelaceCards(rawCardInfo["cards"]); card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
} }
rawCardInfo["entities"]?.forEach((rawEntity) { var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
rawEntities?.forEach((rawEntity) {
if (rawEntity is String) { if (rawEntity is String) {
if (entities.isExist(rawEntity)) { if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity))); card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
@ -267,8 +293,9 @@ class HomeAssistant {
} }
} }
}); });
if (rawCardInfo["entity"] != null) { var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
var en = rawCardInfo["entity"]; if (rawSingleEntity != null) {
var en = rawSingleEntity;
if (en is String) { if (en is String) {
if (entities.isExist(en)) { if (entities.isExist(en)) {
Entity e = entities.get(en); Entity e = entities.get(en);

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -16,11 +17,21 @@ import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:device_info/device_info.dart'; import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/circular_slider/single_circular_slider.dart';
import 'package:share/receive_share_state.dart';
import 'package:share/share.dart';
import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart';
import 'package:uni_links/uni_links.dart';
import 'package:workmanager/workmanager.dart' as workManager;
import 'package:geolocator/geolocator.dart';
import 'package:battery/battery.dart';
import 'utils/logger.dart';
part 'const.dart'; part 'const.dart';
part 'utils/launcher.dart'; part 'utils/launcher.dart';
@ -45,35 +56,34 @@ part 'entities/fan/fan_entity.class.dart';
part 'entities/automation/automation_entity.class.dart'; part 'entities/automation/automation_entity.class.dart';
part 'entities/camera/camera_entity.class.dart'; part 'entities/camera/camera_entity.class.dart';
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart'; part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
part 'entity_widgets/common/badge.dart'; part 'entities/badge.widget.dart';
part 'entity_widgets/model_widgets.dart'; part 'entities/entity_model.widget.dart';
part 'entity_widgets/default_entity_container.dart'; part 'entities/default_entity_container.widget.dart';
part 'entity_widgets/missed_entity.dart'; part 'entities/missed_entity.widget.dart';
part 'entity_widgets/glance_entity_container.dart'; part 'cards/widgets/glance_card_entity_container.dart';
part 'entity_widgets/button_entity_container.dart'; part 'cards/widgets/entity_button_card_body.widget.dart';
part 'entity_widgets/common/entity_attributes_list.dart'; part 'pages/widgets/entity_attributes_list.dart';
part 'entity_widgets/entity_icon.dart'; part 'entities/entity_icon.widget.dart';
part 'entity_widgets/entity_name.dart'; part 'entities/entity_name.widget.dart';
part 'entity_widgets/common/last_updated.dart'; part 'pages/widgets/last_updated.dart';
part 'entity_widgets/common/mode_swicth.dart'; part 'entities/climate/widgets/mode_swicth.dart';
part 'entity_widgets/common/mode_selector.dart'; part 'entities/climate/widgets/mode_selector.dart';
part 'entity_widgets/common/universal_slider.dart'; part 'entities/universal_slider.widget.dart';
part 'entity_widgets/common/flat_service_button.dart'; part 'entities/flat_service_button.widget.dart';
part 'entity_widgets/common/light_color_picker.dart'; part 'entities/light/widgets/light_color_picker.dart';
part 'entity_widgets/common/camera_stream_view.dart'; part 'entities/camera/widgets/camera_stream_view.dart';
part 'entity_widgets/entity_colors.class.dart'; part 'entities/entity_colors.class.dart';
part 'entity_widgets/entity_page_container.dart'; part 'plugins/history_chart/entity_history.dart';
part 'entity_widgets/history_chart/entity_history.dart'; part 'plugins/history_chart/simple_state_history_chart.dart';
part 'entity_widgets/history_chart/simple_state_history_chart.dart'; part 'plugins/history_chart/numeric_state_history_chart.dart';
part 'entity_widgets/history_chart/numeric_state_history_chart.dart'; part 'plugins/history_chart/combined_history_chart.dart';
part 'entity_widgets/history_chart/combined_history_chart.dart'; part 'plugins/history_chart/history_control_widget.dart';
part 'entity_widgets/history_chart/history_control_widget.dart'; part 'plugins/history_chart/entity_history_moment.dart';
part 'entity_widgets/history_chart/entity_history_moment.dart';
part 'entities/switch/widget/switch_state.dart'; part 'entities/switch/widget/switch_state.dart';
part 'entities/slider/widgets/slider_controls.dart'; part 'entities/slider/widgets/slider_controls.dart';
part 'entities/text/widgets/text_input_state.dart'; part 'entities/text/widgets/text_input_state.dart';
part 'entities/select/widgets/select_state.dart'; part 'entities/select/widgets/select_state.dart';
part 'entity_widgets/common/simple_state.dart'; part 'entities/simple_state.widget.dart';
part 'entities/timer/widgets/timer_state.dart'; part 'entities/timer/widgets/timer_state.dart';
part 'entities/climate/widgets/climate_state.widget.dart'; part 'entities/climate/widgets/climate_state.widget.dart';
part 'entities/cover/widgets/cover_state.dart'; part 'entities/cover/widgets/cover_state.dart';
@ -86,6 +96,9 @@ part 'entities/light/widgets/light_controls.dart';
part 'entities/media_player/widgets/media_player_widgets.dart'; part 'entities/media_player/widgets/media_player_widgets.dart';
part 'entities/fan/widgets/fan_controls.dart'; part 'entities/fan/widgets/fan_controls.dart';
part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.dart'; part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.dart';
part 'entities/vacuum/vacuum_entity.class.dart';
part 'entities/vacuum/widgets/vacuum_controls.dart';
part 'entities/vacuum/widgets/vacuum_state_button.dart';
part 'pages/settings.page.dart'; part 'pages/settings.page.dart';
part 'pages/purchase.page.dart'; part 'pages/purchase.page.dart';
part 'pages/widgets/product_purchase.widget.dart'; part 'pages/widgets/product_purchase.widget.dart';
@ -93,10 +106,11 @@ part 'pages/widgets/page_loading_indicator.dart';
part 'pages/widgets/page_loading_error.dart'; part 'pages/widgets/page_loading_error.dart';
part 'pages/panel.page.dart'; part 'pages/panel.page.dart';
part 'pages/main.page.dart'; part 'pages/main.page.dart';
part 'pages/integration_settings.page.dart';
part 'home_assistant.class.dart'; part 'home_assistant.class.dart';
part 'pages/log.page.dart'; part 'pages/log.page.dart';
part 'pages/entity.page.dart'; part 'pages/entity.page.dart';
part 'mdi.class.dart'; part 'utils/mdi.class.dart';
part 'entity_collection.class.dart'; part 'entity_collection.class.dart';
part 'managers/auth_manager.class.dart'; part 'managers/auth_manager.class.dart';
part 'managers/location_manager.class.dart'; part 'managers/location_manager.class.dart';
@ -104,26 +118,32 @@ part 'managers/mobile_app_integration_manager.class.dart';
part 'managers/connection_manager.class.dart'; part 'managers/connection_manager.class.dart';
part 'managers/device_info_manager.class.dart'; part 'managers/device_info_manager.class.dart';
part 'managers/startup_user_messages_manager.class.dart'; part 'managers/startup_user_messages_manager.class.dart';
part 'ui_class/ui.dart'; part 'ui.dart';
part 'ui_class/view.class.dart'; part 'view.class.dart';
part 'ui_class/card.class.dart'; part 'cards/card.class.dart';
part 'ui_class/sizes_class.dart'; part 'panels/panel_class.dart';
part 'ui_class/panel_class.dart'; part 'viewWidget.widget.dart';
part 'ui_widgets/view.dart'; part 'cards/card_widget.dart';
part 'ui_widgets/card_widget.dart'; part 'cards/widgets/card_header.widget.dart';
part 'ui_widgets/card_header_widget.dart';
part 'panels/config_panel_widget.dart'; part 'panels/config_panel_widget.dart';
part 'panels/widgets/link_to_web_config.dart'; part 'panels/widgets/link_to_web_config.dart';
part 'utils/logger.dart';
part 'types/ha_error.dart'; part 'types/ha_error.dart';
part 'types/event_bus_events.dart'; part 'types/event_bus_events.dart';
part 'cards/widgets/gauge_card_body.dart';
part 'cards/widgets/light_card_body.dart';
part 'pages/play_media.page.dart';
part 'entities/entity_page_layout.widget.dart';
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
part 'pages/whats_new.page.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client"; const String appName = "HA Client";
const appVersion = "0.6.5"; const appVersionNumber = "0.7.2";
const appVersionAdd = "";
const appVersion = "$appVersionNumber-$appVersionAdd";
void main() async { void main() async {
FlutterError.onError = (errorDetails) { FlutterError.onError = (errorDetails) {
@ -134,10 +154,11 @@ void main() async {
}; };
runZoned(() { runZoned(() {
//AndroidAlarmManager.initialize().then((_) { workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
runApp(new HAClientApp()); runApp(new HAClientApp());
// print("Running MAIN isolate ${Isolate.current.hashCode}");
//});
}, onError: (error, stack) { }, onError: (error, stack) {
Logger.e("$error"); Logger.e("$error");
@ -162,36 +183,14 @@ class HAClientApp extends StatelessWidget {
routes: { routes: {
"/": (context) => MainPage(title: 'HA Client'), "/": (context) => MainPage(title: 'HA Client'),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/integration-settings": (context) => IntegrationSettingsPage(title: "Integration settings"),
"/putchase": (context) => PurchasePage(title: "Support app development"), "/putchase": (context) => PurchasePage(title: "Support app development"),
"/log-view": (context) => LogViewPage(title: "Log"), "/play-media": (context) => PlayMediaPage(
"/login": (context) => WebviewScaffold( mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
url: "${ConnectionManager().oauthUrl}", mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
),
title: new Text("Login with HA"),
actions: <Widget>[
FlatButton(
child: Text("Manual", style: TextStyle(color: Colors.white)),
onPressed: () {
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
},
)
],
),
), ),
"/webview": (context) => WebviewScaffold( "/log-view": (context) => LogViewPage(title: "Log"),
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}", "/whats-new": (context) => WhatsNewPage()
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop()
),
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
),
)
}, },
); );
} }

View File

@ -9,24 +9,37 @@ class AuthManager {
} }
AuthManager._internal(); AuthManager._internal();
StreamSubscription deepLinksSubscription;
Future getTempToken({String oauthUrl}) { Future start({String oauthUrl}) {
Completer completer = Completer(); Completer completer = Completer();
final flutterWebviewPlugin = new FlutterWebviewPlugin(); deepLinksSubscription?.cancel();
flutterWebviewPlugin.onUrlChanged.listen((String url) { deepLinksSubscription = getUriLinksStream().listen((Uri uri) {
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) { Logger.d("[LINKED AUTH] We got something private");
String authCode = url.split("=")[1]; _getTempToken(oauthUrl, uri.queryParameters["code"])
Logger.d("We have auth code. Getting temporary access token..."); .then((tempToken) => completer.complete(tempToken))
ConnectionManager().sendHTTPPost( .catchError((_){
completer.completeError(HAError("Auth error"));
});
}, onError: (err) {
Logger.e("[LINKED AUTH] Error handling linked auth: $e");
completer.completeError(HAError("Auth error"));
});
Logger.d("Launching OAuth");
eventBus.fire(StartAuthEvent(oauthUrl, true));
return completer.future;
}
Future _getTempToken(String oauthUrl,String authCode) {
Completer completer = Completer();
ConnectionManager().sendHTTPPost(
endPoint: "/auth/token", endPoint: "/auth/token",
contentType: "application/x-www-form-urlencoded", contentType: "application/x-www-form-urlencoded",
includeAuthHeader: false, includeAuthHeader: false,
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}" data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems')}"
).then((response) { ).then((response) {
Logger.d("Got temp token"); Logger.d("Got temp token");
String tempToken = json.decode(response)['access_token']; String tempToken = json.decode(response)['access_token'];
Logger.d("Closing webview...");
//flutterWebviewPlugin.close();
eventBus.fire(StartAuthEvent(oauthUrl, false)); eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.complete(tempToken); completer.complete(tempToken);
}).catchError((e) { }).catchError((e) {
@ -35,10 +48,6 @@ class AuthManager {
eventBus.fire(StartAuthEvent(oauthUrl, false)); eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.completeError(HAError("Error getting temp token")); completer.completeError(HAError("Error getting temp token"));
}); });
}
});
Logger.d("Launching OAuth");
eventBus.fire(StartAuthEvent(oauthUrl, true));
return completer.future; return completer.future;
} }

View File

@ -59,9 +59,9 @@ class ConnectionManager {
_token = await storage.read(key: "hacl_llt"); _token = await storage.read(key: "hacl_llt");
Logger.e("Long-lived token read successful"); Logger.e("Long-lived token read successful");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent( oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri 'http://ha-client.homemade.systems')}&redirect_uri=${Uri
.encodeComponent( .encodeComponent(
'http://ha-client.homemade.systems/service/auth_callback.html')}"; 'haclient://auth')}";
settingsLoaded = true; settingsLoaded = true;
} catch (e) { } catch (e) {
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()])); completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
@ -79,7 +79,7 @@ class ConnectionManager {
if (!stopInit) { if (!stopInit) {
if (_token == null) { if (_token == null) {
AuthManager().getTempToken( AuthManager().start(
oauthUrl: oauthUrl oauthUrl: oauthUrl
).then((token) { ).then((token) {
Logger.d("Token from AuthManager recived"); Logger.d("Token from AuthManager recived");
@ -100,7 +100,9 @@ class ConnectionManager {
if (forceReconnect || !isConnected) { if (forceReconnect || !isConnected) {
_connect().timeout(connectTimeout, onTimeout: () { _connect().timeout(connectTimeout, onTimeout: () {
_disconnect().then((_) { _disconnect().then((_) {
completer?.completeError(HAError("Connection timeout")); if (completer != null && !completer.isCompleted) {
completer.completeError(HAError("Connection timeout"));
}
}); });
}).then((_) { }).then((_) {
completer?.complete(); completer?.complete();
@ -161,8 +163,6 @@ class ConnectionManager {
} }
} }
Future _disconnect() { Future _disconnect() {
Completer completer = Completer(); Completer completer = Completer();
if (!isConnected) { if (!isConnected) {
@ -186,10 +186,10 @@ class ConnectionManager {
_handleMessage(data) { _handleMessage(data) {
if (data["type"] == "result") { if (data["type"] == "result") {
if (data["id"] != null && data["success"]) { if (data["id"] != null && data["success"]) {
Logger.d("[Received] <== Request id ${data['id']} was successful"); //Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]); _messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) { } else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}"); //Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}"); _messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
} }
_messageResolver.remove("${data["id"]}"); _messageResolver.remove("${data["id"]}");
@ -348,7 +348,10 @@ class ConnectionManager {
_currentMessageId += 1; _currentMessageId += 1;
} }
Future callService({String domain, String service, String entityId, Map additionalServiceData}) { Future callService(String domain, String service, String entityId, Map additionalServiceData) {
eventBus.fire(NotifyServiceCallEvent(domain, service, entityId));
Logger.d("Service call: $domain.$service, $entityId, $additionalServiceData");
Completer completer = Completer();
Map serviceData = {}; Map serviceData = {};
if (entityId != null) { if (entityId != null) {
serviceData["entity_id"] = entityId; serviceData["entity_id"] = entityId;
@ -357,9 +360,17 @@ class ConnectionManager {
serviceData.addAll(additionalServiceData); serviceData.addAll(additionalServiceData);
} }
if (serviceData.isNotEmpty) if (serviceData.isNotEmpty)
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData}); sendHTTPPost(
endPoint: "/api/services/$domain/$service",
data: json.encode(serviceData)
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else else
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service}); sendHTTPPost(
endPoint: "/api/services/$domain/$service"
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
return completer.future;
} }
Future<List> getHistory(String entityId) async { Future<List> getHistory(String entityId) async {
@ -398,10 +409,11 @@ class ConnectionManager {
headers: headers, headers: headers,
body: data body: data
).then((response) { ).then((response) {
Logger.d("[Received] <== HTTP ${response.statusCode}");
if (response.statusCode >= 200 && response.statusCode < 300 ) { if (response.statusCode >= 200 && response.statusCode < 300 ) {
Logger.d("[Received] <== HTTP ${response.statusCode}");
completer.complete(response.body); completer.complete(response.body);
} else { } else {
Logger.d("[Received] <== HTTP ${response.statusCode}: ${response.body}");
completer.completeError({"code": response.statusCode, "message": "${response.body}"}); completer.completeError({"code": response.statusCode, "message": "${response.body}"});
} }
}).catchError((e) { }).catchError((e) {

View File

@ -2,4 +2,201 @@ part of '../main.dart';
class LocationManager { class LocationManager {
static final LocationManager _instance = LocationManager
._internal();
factory LocationManager() {
return _instance;
}
LocationManager._internal() {
init();
}
final int defaultUpdateIntervalMinutes = 20;
final String backgroundTaskId = "haclocationtask4352";
final String backgroundTaskTag = "haclocation";
Duration _updateInterval;
bool _isRunning;
void init() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
_updateInterval = Duration(minutes: prefs.getInt("location-interval") ??
defaultUpdateIntervalMinutes);
_isRunning = prefs.getBool("location-enabled") ?? false;
if (_isRunning) {
await _startLocationService();
}
}
setSettings(bool enabled, int interval) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if (interval != _updateInterval.inMinutes) {
prefs.setInt("location-interval", interval);
_updateInterval = Duration(minutes: interval);
if (_isRunning) {
Logger.d("Stopping location tracking...");
_isRunning = false;
await _stopLocationService();
}
}
if (enabled && !_isRunning) {
Logger.d("Starting location tracking");
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("location-enabled", enabled);
_isRunning = true;
await _startLocationService();
} else if (!enabled && _isRunning) {
Logger.d("Stopping location tracking...");
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("location-enabled", enabled);
_isRunning = false;
await _stopLocationService();
}
}
_startLocationService() async {
Logger.d("Scheduling location update for every ${_updateInterval
.inMinutes} minutes...");
String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) {
await workManager.Workmanager.registerPeriodicTask(
backgroundTaskId,
"haClientLocationTracking",
tag: backgroundTaskTag,
inputData: {
"webhookId": webhookId,
"httpWebHost": httpWebHost
},
frequency: _updateInterval,
existingWorkPolicy: workManager.ExistingWorkPolicy.keep,
backoffPolicy: workManager.BackoffPolicy.linear,
backoffPolicyDelay: _updateInterval,
constraints: workManager.Constraints(
networkType: workManager.NetworkType.connected
)
);
}
}
_stopLocationService() async {
Logger.d("Canceling previous schedule if any...");
await workManager.Workmanager.cancelByTag(backgroundTaskTag);
}
updateDeviceLocation() async {
Logger.d("[Test location] Started");
Logger.d("[Test location] Forcing Android location manager...");
Geolocator geolocator = Geolocator()..forceAndroidLocationManager = true;
var battery = Battery();
int batteryLevel = 100;
String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) {
String url = "$httpWebHost/api/webhook/$webhookId";
Map<String, String> headers = {};
headers["Content-Type"] = "application/json";
Map data = {
"type": "update_location",
"data": {
"gps": [],
"gps_accuracy": 0,
"battery": batteryLevel
}
};
Logger.d("[Test location] Getting battery level...");
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){
Logger.d("[Test location] Getting device location...");
geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways).then((location) {
Logger.d("[Test location] Got location: ${location.latitude} ${location.longitude} with accuracy of ${location.accuracy}");
if (location != null) {
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
Logger.d("[Test location] Sending data home...");
http.post(
url,
headers: headers,
body: json.encode(data)
);
}
}).catchError((e) {
Logger.d("[Test location] Error getting current location: ${e.toString()}. Trying last known...");
geolocator.getLastKnownPosition(desiredAccuracy: LocationAccuracy.medium).then((location){
Logger.d("[Test location] Got last known location: ${location.latitude} ${location.longitude} with accuracy of ${location.accuracy}");
if (location != null) {
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
Logger.d("[Test location] Sending data home...");
http.post(
url,
headers: headers,
body: json.encode(data)
);
}
});
});
});
}
}
}
void updateDeviceLocationIsolate() {
workManager.Workmanager.executeTask((backgroundTask, data) {
//print("[Background $backgroundTask] Started");
Geolocator geolocator = Geolocator()..forceAndroidLocationManager = true;
var battery = Battery();
int batteryLevel = 100;
String webhookId = data["webhookId"];
String httpWebHost = data["httpWebHost"];
if (webhookId != null && webhookId.isNotEmpty) {
//print("[Background $backgroundTask] hour=$battery");
String url = "$httpWebHost/api/webhook/$webhookId";
Map<String, String> headers = {};
headers["Content-Type"] = "application/json";
Map data = {
"type": "update_location",
"data": {
"gps": [],
"gps_accuracy": 0,
"battery": batteryLevel
}
};
//print("[Background $backgroundTask] Getting battery level...");
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){
//print("[Background $backgroundTask] Getting device location...");
geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways).then((location) {
//print("[Background $backgroundTask] Got location: ${location.latitude} ${location.longitude}");
if (location != null) {
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
//print("[Background $backgroundTask] Sending data home...");
http.post(
url,
headers: headers,
body: json.encode(data)
);
}
}).catchError((e) {
//print("[Background $backgroundTask] Error getting current location: ${e.toString()}. Trying last known...");
geolocator.getLastKnownPosition(desiredAccuracy: LocationAccuracy.medium).then((location){
//print("[Background $backgroundTask] Got last known location: ${location.latitude} ${location.longitude}");
if (location != null) {
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
//print("[Background $backgroundTask] Sending data home...");
http.post(
url,
headers: headers,
body: json.encode(data)
);
}
});
});
});
}
return Future.value(true);
});
} }

View File

@ -4,18 +4,20 @@ class MobileAppIntegrationManager {
static final _appRegistrationData = { static final _appRegistrationData = {
"app_version": "$appVersion", "app_version": "$appVersion",
"device_name": "${HomeAssistant().userName}'s ${DeviceInfoManager().model}", "device_name": "",
"manufacturer": DeviceInfoManager().manufacturer, "manufacturer": DeviceInfoManager().manufacturer,
"model": DeviceInfoManager().model, "model": DeviceInfoManager().model,
"os_version": DeviceInfoManager().osVersion, "os_version": DeviceInfoManager().osVersion,
"app_data": { "app_data": {
"push_token": "${HomeAssistant().fcmToken}", "push_token": "",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification" "push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
} }
}; };
static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) { static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
Completer completer = Completer(); Completer completer = Completer();
_appRegistrationData["device_name"] = "${HomeAssistant().userName}'s ${DeviceInfoManager().model}";
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
if (ConnectionManager().webhookId == null || forceRegister) { if (ConnectionManager().webhookId == null || forceRegister) {
Logger.d("Mobile app was not registered yet or need to be reseted. Registering..."); Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
var registrationData = Map.from(_appRegistrationData); var registrationData = Map.from(_appRegistrationData);
@ -35,6 +37,7 @@ class MobileAppIntegrationManager {
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
prefs.setString("app-webhook-id", responseObject["webhook_id"]); prefs.setString("app-webhook-id", responseObject["webhook_id"]);
ConnectionManager().webhookId = responseObject["webhook_id"]; ConnectionManager().webhookId = responseObject["webhook_id"];
completer.complete(); completer.complete();
eventBus.fire(ShowPopupDialogEvent( eventBus.fire(ShowPopupDialogEvent(
title: "Mobile app Integration was created", title: "Mobile app Integration was created",
@ -42,7 +45,7 @@ class MobileAppIntegrationManager {
positiveText: "Restart now", positiveText: "Restart now",
negativeText: "Later", negativeText: "Later",
onPositive: () { onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null); ConnectionManager().callService("homeassistant","restart", null, null);
}, },
)); ));
}); });

View File

@ -12,13 +12,18 @@ class StartupUserMessagesManager {
StartupUserMessagesManager._internal() {} StartupUserMessagesManager._internal() {}
bool _supportAppDevelopmentMessageShown; bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3"; static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-706";
void checkMessagesToShow() async { void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload(); await prefs.reload();
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false; _supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
if (!_supportAppDevelopmentMessageShown) { _whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false;
if (!_whatsNewMessageShown) {
_showWhatsNewMessage();
} else if (!_supportAppDevelopmentMessageShown) {
_showSupportAppDevelopmentMessage(); _showSupportAppDevelopmentMessage();
} }
} }
@ -43,4 +48,11 @@ class StartupUserMessagesManager {
)); ));
} }
void _showWhatsNewMessage() {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_whatsNewMessageKey, true);
eventBus.fire(ShowPageEvent(path: "/whats-new"));
});
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -10,49 +10,63 @@ class EntityViewPage extends StatefulWidget {
} }
class _EntityViewPageState extends State<EntityViewPage> { class _EntityViewPageState extends State<EntityViewPage> {
String _title;
StreamSubscription _refreshDataSubscription; StreamSubscription _refreshDataSubscription;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
Entity entity;
Entity forwardToMainPage;
bool _popScheduled = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId == widget.entityId) { if (event.entityId == widget.entityId) {
Logger.d("State change event handled by entity page: ${event.entityId}"); entity = HomeAssistant().entities.get(widget.entityId);
Logger.d("[Entity page] State change event handled: ${event.entityId}");
setState(() {}); setState(() {});
} }
}); });
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) { _refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
entity = HomeAssistant().entities.get(widget.entityId);
setState(() {}); setState(() {});
}); });
_prepareData(); entity = HomeAssistant().entities.get(widget.entityId);
} }
void _prepareData() async {
_title = HomeAssistant().entities.get(widget.entityId).displayName;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget body;
if (MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
if (!_popScheduled) {
_popScheduled = true;
_popAfterBuild();
}
body = PageLoadingIndicator();
} else {
body = EntityPageLayout(entity: entity);
}
return new Scaffold( return new Scaffold(
appBar: new AppBar( appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context); Navigator.pop(context);
}), }),
// Here we take the value from the MyHomePage object that was created by title: new Text("${entity.displayName}"),
// the App.build method, and use it to set our appbar title.
title: new Text(_title),
), ),
body: HomeAssistant().entities.get(widget.entityId).buildEntityPageWidget(context), body: body,
); );
} }
_popAfterBuild() async {
forwardToMainPage = entity;
await Future.delayed(Duration(milliseconds: 300));
Navigator.of(context).pop();
}
@override @override
void dispose(){ void dispose(){
if (_stateSubscription != null) _stateSubscription.cancel(); if (_stateSubscription != null) _stateSubscription.cancel();
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel(); if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
eventBus.fire(ShowEntityPageEvent(entity: forwardToMainPage));
super.dispose(); super.dispose();
} }
} }

View File

@ -0,0 +1,197 @@
part of '../main.dart';
class IntegrationSettingsPage extends StatefulWidget {
IntegrationSettingsPage({Key key, this.title}) : super(key: key);
final String title;
@override
_IntegrationSettingsPageState createState() => new _IntegrationSettingsPageState();
}
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
bool _locationTrackingEnabled = false;
bool _wait = false;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
SharedPreferences.getInstance().then((prefs) {
setState(() {
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
});
});
}
void incLocationInterval() {
if (_locationInterval < 720) {
setState(() {
_locationInterval = _locationInterval + 1;
});
}
}
void decLocationInterval() {
if (_locationInterval > 15) {
setState(() {
_locationInterval = _locationInterval - 1;
});
}
}
restart() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to restart Home Assistant?",
body: "This will restart your Home Assistant server.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService("homeassistant", "restart", null, null);
},
));
}
stop() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to STOP Home Assistant?",
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService("homeassistant","stop", null, null);
},
));
}
updateRegistration() {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
}
resetRegistration() {
eventBus.fire(ShowPopupDialogEvent(
title: "Waaaait",
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
positiveText: "Done it already",
negativeText: "Ok, I will",
onPositive: () {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
},
));
}
_switchLocationTrackingState(bool state) async {
if (state) {
await LocationManager().updateDeviceLocation();
}
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
setState(() {
_wait = false;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text(widget.title),
),
body: ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
Container(height: Sizes.rowPadding,),
InkWell(
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#location-tracking"),
child: Text(
"Please read documentation!",
style: TextStyle(
color: Colors.blue,
fontSize: 16,
decoration: TextDecoration.underline
)
),
),
Container(height: Sizes.rowPadding,),
Row(
children: <Widget>[
Text("Enable device location tracking"),
Switch(
value: _locationTrackingEnabled,
onChanged: _wait ? null : (value) {
setState(() {
_locationTrackingEnabled = value;
_wait = true;
});
_switchLocationTrackingState(value);
},
),
],
),
Container(height: Sizes.rowPadding,),
Text("Location update interval in minutes:"),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
//Expanded(child: Container(),),
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("-", style: TextStyle(fontSize: Sizes.largeFontSize)),
onPressed: () => decLocationInterval(),
),
Text("$_locationInterval", style: TextStyle(fontSize: Sizes.largeFontSize)),
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("+", style: TextStyle(fontSize: Sizes.largeFontSize)),
onPressed: () => incLocationInterval(),
),
],
),
Divider(),
Text("Integration status", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
Container(height: Sizes.rowPadding,),
Text("${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}"),
Container(height: 6.0,),
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
//Divider(),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
RaisedButton(
color: Colors.blue,
onPressed: () => updateRegistration(),
child: Text("Check integration", style: TextStyle(color: Colors.white))
),
Container(width: 10.0,),
RaisedButton(
color: Colors.redAccent,
onPressed: () => resetRegistration(),
child: Text("Reset integration", style: TextStyle(color: Colors.white))
)
],
),
]
),
);
}
@override
void dispose() {
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
super.dispose();
}
}

View File

@ -9,7 +9,7 @@ class MainPage extends StatefulWidget {
_MainPageState createState() => new _MainPageState(); _MainPageState createState() => new _MainPageState();
} }
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin { class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
StreamSubscription<List<PurchaseDetails>> _subscription; StreamSubscription<List<PurchaseDetails>> _subscription;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
@ -25,6 +25,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
int _previousViewCount; int _previousViewCount;
bool _showLoginButton = false; bool _showLoginButton = false;
bool _preventAppRefresh = false; bool _preventAppRefresh = false;
String _savedSharedText;
Entity _entityToShow;
@override @override
void initState() { void initState() {
@ -34,6 +36,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_handlePurchaseUpdates(purchases); _handlePurchaseUpdates(purchases);
}); });
super.initState(); super.initState();
enableShareReceiving();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure( _firebaseMessaging.configure(
@ -72,8 +75,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}); });
_fullLoad(); _fullLoad();
}
@override void receiveShare(Share shared) {
if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) {
_savedSharedText = shared.text;
}
} }
Future onSelectNotification(String payload) async { Future onSelectNotification(String payload) async {
@ -102,6 +109,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_subscribe().then((_) { _subscribe().then((_) {
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){ ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
_fetchData(); _fetchData();
LocationManager();
StartupUserMessagesManager().checkMessagesToShow(); StartupUserMessagesManager().checkMessagesToShow();
}, onError: (e) { }, onError: (e) {
_setErrorState(e); _setErrorState(e);
@ -121,6 +129,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
_fetchData() async { _fetchData() async {
if (_savedSharedText != null && !HomeAssistant().isNoEntities) {
Logger.d("Got shared text: $_savedSharedText");
Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText});
_savedSharedText = null;
}
await HomeAssistant().fetchData().then((_) { await HomeAssistant().fetchData().then((_) {
_hideBottomBar(); _hideBottomBar();
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0; int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
@ -129,6 +142,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_viewsTabController = TabController(vsync: this, length: currentViewCount); _viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount; _previousViewCount = currentViewCount;
} }
if (_entityToShow != null) {
_entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
}
}).catchError((e) { }).catchError((e) {
if (e is HAError) { if (e is HAError) {
_setErrorState(e); _setErrorState(e);
@ -205,16 +221,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
if (_serviceCallSubscription == null) { if (_serviceCallSubscription == null) {
_serviceCallSubscription = _serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) { eventBus.on<NotifyServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId, _notifyServiceCalled(event.domain, event.service, event.entityId);
event.additionalParams);
}); });
} }
if (_showEntityPageSubscription == null) { if (_showEntityPageSubscription == null) {
_showEntityPageSubscription = _showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) { eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity.entityId); _showEntityPage(event.entity?.entityId);
}); });
} }
@ -240,7 +255,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_showOAuth(); _showOAuth();
} else { } else {
_preventAppRefresh = false; _preventAppRefresh = false;
Navigator.of(context).pop();
} }
}); });
} }
@ -254,7 +268,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
void _showOAuth() { void _showOAuth() {
_preventAppRefresh = true; _preventAppRefresh = true;
Navigator.of(context).pushNamed('/login'); Launcher.launchURLInCustomTab(
url: ConnectionManager().oauthUrl
);
} }
_setErrorState(HAError e) { _setErrorState(HAError e) {
@ -304,22 +320,28 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
); );
} }
//TODO remove this shit void _notifyServiceCalled(String domain, String service, String entityId) {
void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar( _showInfoBottomBar(
message: "Calling $domain.$service", message: "Calling $domain.$service",
duration: Duration(seconds: 3) duration: Duration(seconds: 4)
); );
ConnectionManager().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
} }
void _showEntityPage(String entityId) { void _showEntityPage(String entityId) {
Navigator.push( setState(() {
context, _entityToShow = HomeAssistant().entities.get(entityId);
MaterialPageRoute( if (_entityToShow != null) {
builder: (context) => EntityViewPage(entityId: entityId), _mainScrollController?.jumpTo(0);
) }
); });
/*if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EntityViewPage(entityId: entityId),
)
);
}*/
} }
void _showPage(String path, bool goBackFirst) { void _showPage(String path, bool goBackFirst) {
@ -349,25 +371,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
menuItems.add( menuItems.add(
UserAccountsDrawerHeader( UserAccountsDrawerHeader(
accountName: Text(HomeAssistant().userName), accountName: Text(HomeAssistant().userName),
accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"), accountEmail: Text(HomeAssistant().locationName ?? ""),
onDetailsPressed: () {
final flutterWebViewPlugin = new FlutterWebviewPlugin();
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
if (viewState.type == WebViewState.startLoad) {
Logger.d("[WebView] Injecting external auth JS");
rootBundle.loadString('assets/js/externalAuth.js').then((js){
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
});
}
});
Navigator.of(context).pushNamed(
"/webview",
arguments: {
"url": "${ConnectionManager().httpWebHost}/profile?external_auth=1",
"title": "Profile"
}
);
},
currentAccountPicture: CircleAvatar( currentAccountPicture: CircleAvatar(
child: Text( child: Text(
HomeAssistant().userAvatarText, HomeAssistant().userAvatarText,
@ -389,7 +393,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
children: <Widget>[ children: <Widget>[
Text("${panel.title}"), Text("${panel.title}"),
Container(width: 4.0,), Container(width: 4.0,),
panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,) panel.isWebView ? Text("WEB", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
], ],
), ),
onTap: () { onTap: () {
@ -404,12 +408,20 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
menuItems.addAll([ menuItems.addAll([
Divider(), Divider(),
ListTile( ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")), leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:server-network")),
title: Text("Connection settings"), title: Text("Connection settings"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pushNamed('/connection-settings'); Navigator.of(context).pushNamed('/connection-settings');
}, },
),
ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:cellphone-settings-variant")),
title: Text("Integration settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/integration-settings');
},
) )
]); ]);
menuItems.addAll([ menuItems.addAll([
@ -622,87 +634,186 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final ScrollController _mainScrollController = ScrollController();
Widget _buildScaffoldBody(bool empty) { Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> popupMenuItems = []; List<PopupMenuItem<String>> serviceMenuItems = [];
List<PopupMenuItem<String>> mediaMenuItems = [];
popupMenuItems.add(PopupMenuItem<String>( serviceMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"), child: new Text("Reload"),
value: "reload", value: "reload",
)); ));
List<Widget> emptyBody = [
Text("."),
];
if (ConnectionManager().isAuthenticated) { if (ConnectionManager().isAuthenticated) {
_showLoginButton = false; _showLoginButton = false;
popupMenuItems.add( serviceMenuItems.add(
PopupMenuItem<String>( PopupMenuItem<String>(
child: new Text("Logout"), child: new Text("Logout"),
value: "logout", value: "logout",
)); ));
} }
if (_showLoginButton) { Widget mediaMenuIcon;
emptyBody = [ int playersCount = 0;
FlatButton( if (!empty && !HomeAssistant().entities.isEmpty) {
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)), List<Entity> activePlayers = HomeAssistant().entities.getByDomains(domains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
color: Colors.blue, playersCount = activePlayers.length;
onPressed: () => _fullLoad(), mediaMenuItems.addAll(
) activePlayers.map((entity) => PopupMenuItem<String>(
]; child: Text(
"${entity.displayName}",
style: TextStyle(
color: EntityColor.stateColor(entity.state)
),
),
value: "${entity.entityId}",
)).toList()
);
} }
return NestedScrollView( mediaMenuItems.addAll([
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { PopupMenuItem<String>(
return <Widget>[ child: new Text("Play media..."),
SliverAppBar( value: "play_media",
floating: true,
pinned: true,
primary: true,
title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,),
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
context: context,
items: popupMenuItems
).then((String val) {
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
_quickLoad();
});
}
});
}
)
],
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
},
),
bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
isScrollable: true,
),
),
];
},
body: empty ?
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: emptyBody
),
) )
: ]);
HomeAssistant().buildViews(context, _viewsTabController), if (playersCount > 0) {
mediaMenuIcon = Stack(
overflow: Overflow.visible,
children: <Widget>[
Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:television"), color: Colors.white,),
Positioned(
bottom: -4,
right: -4,
child: Container(
height: 16,
width: 16,
decoration: new BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
child: Center(
child: Text("$playersCount", style: TextStyle(fontSize: 12)),
),
),
)
],
);
} else {
mediaMenuIcon = Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:television"), color: Colors.white,);
}
Widget mainScrollBody;
if (empty) {
if (_showLoginButton) {
mainScrollBody = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlatButton(
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
color: Colors.blue,
onPressed: () => _fullLoad(),
)
]
)
);
} else {
mainScrollBody = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("...")
]
),
);
}
} else {
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
mainScrollBody = Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
child: HomeAssistant().buildViews(context, _viewsTabController),
),
Container(
width: Sizes.mainPageScreenSeparatorWidth,
color: Colors.blue,
),
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth),
child: EntityPageLayout(entity: _entityToShow, showClose: true,),
)
],
);
} else if (_entityToShow != null) {
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
} else {
mainScrollBody = HomeAssistant().buildViews(context, _viewsTabController);
}
}
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
floating: true,
pinned: true,
primary: true,
title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[
IconButton(
icon: mediaMenuIcon,
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100.0, 50, 0.0),
context: context,
items: mediaMenuItems
).then((String val) {
if (val == "play_media") {
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
} else {
_showEntityPage(val);
}
});
}
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,),
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100, 0.0, 0.0),
context: context,
items: serviceMenuItems
).then((String val) {
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
_quickLoad();
});
}
});
}
)
],
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
},
),
bottom: (empty || _entityToShow != null) ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
isScrollable: true,
),
),
];
},
body: mainScrollBody,
controller: _mainScrollController,
); );
} }
@ -768,20 +879,28 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
body: _buildScaffoldBody(true) body: _buildScaffoldBody(true)
); );
} else { } else {
return Scaffold( return WillPopScope(
key: _scaffoldKey, child: Scaffold(
drawer: _buildAppDrawer(), key: _scaffoldKey,
primary: false, drawer: _buildAppDrawer(),
bottomNavigationBar: bottomBar, primary: false,
body: _buildScaffoldBody(false), bottomNavigationBar: bottomBar,
body: _buildScaffoldBody(false)
),
onWillPop: () {
if (_entityToShow != null) {
eventBus.fire(ShowEntityPageEvent());
return Future.value(false);
} else {
return Future.value(true);
}
},
); );
} }
} }
@override @override
void dispose() { void dispose() {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.dispose();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_viewsTabController?.dispose(); _viewsTabController?.dispose();
_stateSubscription?.cancel(); _stateSubscription?.cancel();

View File

@ -0,0 +1,250 @@
part of '../main.dart';
class PlayMediaPage extends StatefulWidget {
final String mediaUrl;
final String mediaType;
PlayMediaPage({Key key, this.mediaUrl, this.mediaType}) : super(key: key);
@override
_PlayMediaPageState createState() => new _PlayMediaPageState();
}
class _PlayMediaPageState extends State<PlayMediaPage> {
bool _loaded = false;
String _error = "";
String _validationMessage = "";
List<Entity> _players;
String _mediaUrl;
String _contentType;
bool _useMediaExtractor = false;
bool _isMediaExtractorExist = false;
StreamSubscription _stateSubscription;
StreamSubscription _refreshDataSubscription;
List<String> _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"];
@override
void initState() {
super.initState();
_mediaUrl = widget.mediaUrl;
if (widget.mediaType.isNotEmpty) {
if (!_contentTypes.contains(widget.mediaType)) {
_contentTypes.insert(0, widget.mediaType);
_contentType = _contentTypes[0];
} else {
_contentType = widget.mediaType;
}
} else {
_contentType = _contentTypes[0];
}
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId.contains("media_player")) {
Logger.d("State change event handled by play media page: ${event.entityId}");
setState(() {});
}
});
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
_loadMediaEntities();
});
_loadMediaEntities();
}
_loadMediaEntities() async {
if (HomeAssistant().isNoEntities) {
setState(() {
_loaded = false;
});
} else {
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
//_useMediaExtractor = _isMediaExtractorExist;
_players = HomeAssistant().entities.getByDomains(domains: ["media_player"]);
setState(() {
if (_players.isNotEmpty) {
_loaded = true;
} else {
_loaded = false;
_error = "Looks like you don't have any media player";
}
});
}
}
void _playMedia(Entity entity) {
if (_mediaUrl == null || _mediaUrl.isEmpty) {
setState(() {
_validationMessage = "Media url must be specified";
});
} else if (entity.state == EntityState.unavailable || entity.state == EntityState.unknown) {
setState(() {
_validationMessage = "${entity.displayName} is not available";
});
} else {
String serviceDomain;
if (_useMediaExtractor) {
serviceDomain = "media_extractor";
} else {
serviceDomain = "media_player";
}
Navigator.pop(context);
ConnectionManager().callService(
serviceDomain,
"play_media",
entity.entityId,
{
"media_content_id": _mediaUrl,
"media_content_type": _contentType
}
);
HomeAssistant().sendToPlayerId = entity.entityId;
if (HomeAssistant().sendFromPlayerId != null && HomeAssistant().sendFromPlayerId != HomeAssistant().sendToPlayerId) {
ConnectionManager().callService(
HomeAssistant().sendFromPlayerId.split(".")[0],
"turn_off",
HomeAssistant().sendFromPlayerId,
null
);
HomeAssistant().sendFromPlayerId = null;
}
eventBus.fire(ShowEntityPageEvent(entity: entity));
}
}
@override
Widget build(BuildContext context) {
Widget body;
if (!_loaded) {
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
} else {
List<Widget> children = [];
children.add(CardHeader(name: "Media:"));
children.add(
TextField(
maxLines: 5,
minLines: 1,
decoration: InputDecoration(
labelText: "Media url"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _mediaUrl)),
onChanged: (value) {
_mediaUrl = value;
}
),
);
if (_validationMessage.isNotEmpty) {
children.add(Text(
"$_validationMessage",
style: TextStyle(color: Colors.red)
));
}
children.addAll(<Widget>[
Container(height: Sizes.rowPadding,),
DropdownButton<String>(
value: _contentType,
isExpanded: true,
items: _contentTypes.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (value) {
setState(() {
_contentType = value;
});
},
)
]
);
if (_isMediaExtractorExist) {
children.addAll(<Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Text("Use media extractor"),
),
Switch(
value: _useMediaExtractor,
onChanged: (value) => setState((){_useMediaExtractor = value;}),
),
],
),
Container(
height: Sizes.rowPadding,
)
]
);
} else {
children.addAll(<Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Text("You can use media extractor here"),
),
GestureDetector(
onTap: () {
Launcher.launchURLInCustomTab(
context: context,
url: "https://www.home-assistant.io/components/media_extractor/"
);
},
child: Text(
"How?",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
),
],
),
Container(
height: Sizes.doubleRowPadding,
)
]
);
}
children.add(CardHeader(name: "Play on:"));
children.addAll(
_players.map((player) => InkWell(
child: EntityModel(
entityWrapper: EntityWrapper(entity: player),
handleTap: false,
child: Padding(
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
child: DefaultEntityContainer(state: player._buildStatePart(context)),
)
),
onTap: () => _playMedia(player),
))
);
body = ListView(
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
scrollDirection: Axis.vertical,
children: children
);
}
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text("Play media"),
),
body: body,
);
}
@override
void dispose(){
HomeAssistant().sendFromPlayerId = null;
_stateSubscription?.cancel();
_refreshDataSubscription?.cancel();
super.dispose();
}
}

View File

@ -0,0 +1,69 @@
part of '../main.dart';
class WhatsNewPage extends StatefulWidget {
WhatsNewPage({Key key}) : super(key: key);
@override
_WhatsNewPageState createState() => new _WhatsNewPageState();
}
class _WhatsNewPageState extends State<WhatsNewPage> {
String data = "";
String error = "";
@override
void initState() {
super.initState();
_loadData();
}
_loadData() async {
setState(() {
data = "";
error = "";
});
http.Response response;
response = await http.get("http://ha-client.homemade.systems/service/whats_new_0.7.0.md");
if (response.statusCode == 200) {
setState(() {
data = response.body;
});
} else {
setState(() {
error = "Can't load changelog";
});
}
}
@override
Widget build(BuildContext context) {
Widget body;
if (error.isNotEmpty) {
body = PageLoadingError(errorText: error,);
} else if (data.isEmpty) {
body = PageLoadingIndicator();
} else {
body = Markdown(
data: data,
);
}
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
actions: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => _loadData(),
)
],
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text("What's new"),
),
body: body
);
}
}

View File

@ -14,101 +14,13 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
super.initState(); super.initState();
} }
restart() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to restart Home Assistant?",
body: "This will restart your Home Assistant server.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
},
));
}
stop() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you wanr to STOP Home Assistant?",
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "stop", entityId: null);
},
));
}
updateRegistration() {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
}
resetRegistration() {
eventBus.fire(ShowPopupDialogEvent(
title: "Waaaait",
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
positiveText: "Done it already",
negativeText: "Ok, I will",
onPositive: () {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
},
));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return ListView(
children: [ children: [
Card( LinkToWebConfig(name: "Home Assistant configuration", url: ConnectionManager().httpWebHost+"/config"),
child: Padding(
padding: EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ListTile(
title: Text("Mobile app integration",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize))
),
Text("Registration", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
Container(height: Sizes.rowPadding,),
Text("${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}"),
Container(height: 6.0,),
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
//Divider(),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
RaisedButton(
color: Colors.blue,
onPressed: () => updateRegistration(),
child: Text("Check registration", style: TextStyle(color: Colors.white))
),
Container(width: 10.0,),
RaisedButton(
color: Colors.redAccent,
onPressed: () => resetRegistration(),
child: Text("Reset registration", style: TextStyle(color: Colors.white))
)
],
),
],
),
),
),
LinkToWebConfig(name: "Home Assistant Cloud", url: ConnectionManager().httpWebHost+"/config/cloud/account"),
Container(height: 8.0,),
LinkToWebConfig(name: "Integrations", url: ConnectionManager().httpWebHost+"/config/integrations/dashboard"),
LinkToWebConfig(name: "Users", url: ConnectionManager().httpWebHost+"/config/users/picker"),
Container(height: 8.0,),
LinkToWebConfig(name: "General", url: ConnectionManager().httpWebHost+"/config/core"),
LinkToWebConfig(name: "Server Control", url: ConnectionManager().httpWebHost+"/config/server_control"),
LinkToWebConfig(name: "Persons", url: ConnectionManager().httpWebHost+"/config/person"),
LinkToWebConfig(name: "Entity Registry", url: ConnectionManager().httpWebHost+"/config/entity_registry"),
LinkToWebConfig(name: "Area Registry", url: ConnectionManager().httpWebHost+"/config/area_registry"),
LinkToWebConfig(name: "Automation", url: ConnectionManager().httpWebHost+"/config/automation"),
LinkToWebConfig(name: "Script", url: ConnectionManager().httpWebHost+"/config/script"),
LinkToWebConfig(name: "Customization", url: ConnectionManager().httpWebHost+"/config/customize"),
], ],
); );
} }

View File

@ -23,7 +23,6 @@ class Panel {
if (icon == null || !icon.startsWith("mdi:")) { if (icon == null || !icon.startsWith("mdi:")) {
icon = Panel.iconsByComponent[type]; icon = Panel.iconsByComponent[type];
} }
Logger.d("New panel '$title'. type=$type, icon=$icon, urlPath=$urlPath");
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools'); isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
isWebView = (type != 'config'); isWebView = (type != 'config');
} }
@ -36,7 +35,7 @@ class Panel {
) )
); );
} else { } else {
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}"); Launcher.launchURLInCustomTab(url: "${ConnectionManager().httpWebHost}/$urlPath");
} }
} }

View File

@ -17,9 +17,9 @@ class LinkToWebConfig extends StatelessWidget {
textAlign: TextAlign.left, textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)), style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
subtitle: Text("Tap to opne web version"), subtitle: Text("Tap to open web version"),
onTap: () { onTap: () {
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name); Launcher.launchURLInCustomTab(url: this.url);
}, },
) )
], ],

View File

@ -0,0 +1,77 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'utils.dart';
class BasePainter extends CustomPainter {
Color baseColor;
Color selectionColor;
int primarySectors;
int secondarySectors;
double sliderStrokeWidth;
Offset center;
double radius;
BasePainter({
@required this.baseColor,
@required this.selectionColor,
@required this.primarySectors,
@required this.secondarySectors,
@required this.sliderStrokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
Paint base = _getPaint(color: baseColor);
center = Offset(size.width / 2, size.height / 2);
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
// we need this in the parent to calculate if the user clicks on the circumference
assert(radius > 0);
canvas.drawCircle(center, radius, base);
if (primarySectors > 0) {
_paintSectors(primarySectors, 8.0, selectionColor, canvas);
}
if (secondarySectors > 0) {
_paintSectors(secondarySectors, 6.0, baseColor, canvas);
}
}
void _paintSectors(
int sectors, double radiusPadding, Color color, Canvas canvas) {
Paint section = _getPaint(color: color, width: 2.0);
var endSectors =
getSectionsCoordinatesInCircle(center, radius + radiusPadding, sectors);
var initSectors =
getSectionsCoordinatesInCircle(center, radius - radiusPadding, sectors);
_paintLines(canvas, initSectors, endSectors, section);
}
void _paintLines(
Canvas canvas, List<Offset> inits, List<Offset> ends, Paint section) {
assert(inits.length == ends.length && inits.length > 0);
for (var i = 0; i < inits.length; i++) {
canvas.drawLine(inits[i], ends[i], section);
}
}
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
Paint()
..color = color
..strokeCap = StrokeCap.round
..style = style ?? PaintingStyle.stroke
..strokeWidth = width ?? sliderStrokeWidth;
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

View File

@ -0,0 +1,366 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'base_painter.dart';
import 'slider_painter.dart';
import 'utils.dart';
enum CircularSliderMode { singleHandler, doubleHandler }
enum SlidingState { none, endIsBiggerThanStart, endIsSmallerThanStart }
typedef SelectionChanged<T> = void Function(T a, T b, T c);
class CircularSliderPaint extends StatefulWidget {
final CircularSliderMode mode;
final int init;
final int end;
final int divisions;
final int primarySectors;
final int secondarySectors;
final SelectionChanged<int> onSelectionChange;
final SelectionChanged<int> onSelectionEnd;
final Color baseColor;
final Color selectionColor;
final Color handlerColor;
final double handlerOutterRadius;
final Widget child;
final bool showRoundedCapInSelection;
final bool showHandlerOutter;
final double sliderStrokeWidth;
final bool shouldCountLaps;
CircularSliderPaint({
@required this.mode,
@required this.divisions,
@required this.init,
@required this.end,
this.child,
@required this.primarySectors,
@required this.secondarySectors,
@required this.onSelectionChange,
@required this.onSelectionEnd,
@required this.baseColor,
@required this.selectionColor,
@required this.handlerColor,
@required this.handlerOutterRadius,
@required this.showRoundedCapInSelection,
@required this.showHandlerOutter,
@required this.sliderStrokeWidth,
@required this.shouldCountLaps,
});
@override
_CircularSliderState createState() => _CircularSliderState();
}
class _CircularSliderState extends State<CircularSliderPaint> {
bool _isInitHandlerSelected = false;
bool _isEndHandlerSelected = false;
SliderPainter _painter;
/// start angle in radians where we need to locate the init handler
double _startAngle;
/// end angle in radians where we need to locate the end handler
double _endAngle;
/// the absolute angle in radians representing the selection
double _sweepAngle;
/// in case we have a double slider and we want to move the whole selection by clicking in the slider
/// this will capture the position in the selection relative to the initial handler
/// that way we will be able to keep the selection constant when moving
int _differenceFromInitPoint;
/// will store the number of full laps (2pi radians) as part of the selection
int _laps = 0;
/// will be used to calculate in the next movement if we need to increase or decrease _laps
SlidingState _slidingState = SlidingState.none;
bool get isDoubleHandler => widget.mode == CircularSliderMode.doubleHandler;
bool get isSingleHandler => widget.mode == CircularSliderMode.singleHandler;
bool get isBothHandlersSelected =>
_isEndHandlerSelected && _isInitHandlerSelected;
bool get isNoHandlersSelected =>
!_isEndHandlerSelected && !_isInitHandlerSelected;
@override
void initState() {
super.initState();
_calculatePaintData();
}
// we need to update this widget both with gesture detector but
// also when the parent widget rebuilds itself
@override
void didUpdateWidget(CircularSliderPaint oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.init != widget.init || oldWidget.end != widget.end) {
_calculatePaintData();
}
}
@override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
CustomPanGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomPanGestureRecognizer>(
() => CustomPanGestureRecognizer(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
),
(CustomPanGestureRecognizer instance) {},
),
},
child: CustomPaint(
painter: BasePainter(
baseColor: widget.baseColor,
selectionColor: widget.selectionColor,
primarySectors: widget.primarySectors,
secondarySectors: widget.secondarySectors,
sliderStrokeWidth: widget.sliderStrokeWidth,
),
foregroundPainter: _painter,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: widget.child,
),
),
);
}
void _calculatePaintData() {
var initPercent = isDoubleHandler
? valueToPercentage(widget.init, widget.divisions)
: 0.0;
var endPercent = valueToPercentage(widget.end, widget.divisions);
var sweep = getSweepAngle(initPercent, endPercent);
var previousStartAngle = _startAngle;
var previousEndAngle = _endAngle;
_startAngle = isDoubleHandler ? percentageToRadians(initPercent) : 0.0;
_endAngle = percentageToRadians(endPercent);
_sweepAngle = percentageToRadians(sweep.abs());
// update full laps if need be
if (widget.shouldCountLaps) {
var newSlidingState = _calculateSlidingState(_startAngle, _endAngle);
if (isSingleHandler) {
_laps = _calculateLapsForsSingleHandler(
_endAngle, previousEndAngle, _slidingState, _laps);
_slidingState = newSlidingState;
} else {
// is double handler
if (newSlidingState != _slidingState) {
_laps = _calculateLapsForDoubleHandler(
_startAngle,
_endAngle,
previousStartAngle,
previousEndAngle,
_slidingState,
newSlidingState,
_laps);
_slidingState = newSlidingState;
}
}
}
_painter = SliderPainter(
mode: widget.mode,
startAngle: _startAngle,
endAngle: _endAngle,
sweepAngle: _sweepAngle,
selectionColor: widget.selectionColor,
handlerColor: widget.handlerColor,
handlerOutterRadius: widget.handlerOutterRadius,
showRoundedCapInSelection: widget.showRoundedCapInSelection,
showHandlerOutter: widget.showHandlerOutter,
sliderStrokeWidth: widget.sliderStrokeWidth,
);
}
int _calculateLapsForsSingleHandler(
double end, double prevEnd, SlidingState slidingState, int laps) {
if (slidingState != SlidingState.none) {
if (radiansWasModuloed(end, prevEnd)) {
var lapIncrement = end < prevEnd ? 1 : -1;
var newLaps = laps + lapIncrement;
return newLaps < 0 ? 0 : newLaps;
}
}
return laps;
}
int _calculateLapsForDoubleHandler(
double start,
double end,
double prevStart,
double prevEnd,
SlidingState slidingState,
SlidingState newSlidingState,
int laps) {
if (slidingState != SlidingState.none) {
if (!radiansWasModuloed(start, prevStart) &&
!radiansWasModuloed(end, prevEnd)) {
var lapIncrement =
newSlidingState == SlidingState.endIsBiggerThanStart ? 1 : -1;
var newLaps = laps + lapIncrement;
return newLaps < 0 ? 0 : newLaps;
}
}
return laps;
}
SlidingState _calculateSlidingState(double start, double end) {
return end > start
? SlidingState.endIsBiggerThanStart
: SlidingState.endIsSmallerThanStart;
}
void _onPanUpdate(Offset details) {
if (!_isInitHandlerSelected && !_isEndHandlerSelected) {
return;
}
if (_painter.center == null) {
return;
}
_handlePan(details, false);
}
void _onPanEnd(Offset details) {
_handlePan(details, true);
_isInitHandlerSelected = false;
_isEndHandlerSelected = false;
}
void _handlePan(Offset details, bool isPanEnd) {
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details);
var angle = coordinatesToRadians(_painter.center, position);
var percentage = radiansToPercentage(angle);
var newValue = percentageToValue(percentage, widget.divisions);
if (isBothHandlersSelected) {
var newValueInit =
(newValue - _differenceFromInitPoint) % widget.divisions;
if (newValueInit != widget.init) {
var newValueEnd =
(widget.end + (newValueInit - widget.init)) % widget.divisions;
widget.onSelectionChange(newValueInit, newValueEnd, _laps);
if (isPanEnd) {
widget.onSelectionEnd(newValueInit, newValueEnd, _laps);
}
}
return;
}
// isDoubleHandler but one handler was selected
if (_isInitHandlerSelected) {
widget.onSelectionChange(newValue, widget.end, _laps);
if (isPanEnd) {
widget.onSelectionEnd(newValue, widget.end, _laps);
}
} else {
widget.onSelectionChange(widget.init, newValue, _laps);
if (isPanEnd) {
widget.onSelectionEnd(widget.init, newValue, _laps);
}
}
}
bool _onPanDown(Offset details) {
if (_painter == null) {
return false;
}
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details);
if (position == null) {
return false;
}
if (isSingleHandler) {
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
_isEndHandlerSelected = true;
_onPanUpdate(details);
}
} else {
_isInitHandlerSelected = isPointInsideCircle(
position, _painter.initHandler, widget.handlerOutterRadius);
if (!_isInitHandlerSelected) {
_isEndHandlerSelected = isPointInsideCircle(
position, _painter.endHandler, widget.handlerOutterRadius);
}
if (isNoHandlersSelected) {
// we check if the user pressed in the selection in a double handler slider
// that means the user wants to move the selection as a whole
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
var angle = coordinatesToRadians(_painter.center, position);
if (isAngleInsideRadiansSelection(angle, _startAngle, _sweepAngle)) {
_isEndHandlerSelected = true;
_isInitHandlerSelected = true;
var positionPercentage = radiansToPercentage(angle);
// no need to account for negative values, that will be sorted out in the onPanUpdate
_differenceFromInitPoint =
percentageToValue(positionPercentage, widget.divisions) -
widget.init;
}
}
}
}
return _isInitHandlerSelected || _isEndHandlerSelected;
}
}
class CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
final Function onPanDown;
final Function onPanUpdate;
final Function onPanEnd;
CustomPanGestureRecognizer({
@required this.onPanDown,
@required this.onPanUpdate,
@required this.onPanEnd,
});
@override
void addPointer(PointerEvent event) {
if (onPanDown(event.position)) {
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);
} else {
stopTrackingPointer(event.pointer);
}
}
@override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
onPanUpdate(event.position);
}
if (event is PointerUpEvent) {
onPanEnd(event.position);
stopTrackingPointer(event.pointer);
}
}
@override
String get debugDescription => 'customPan';
@override
void didStopTrackingLastPointer(int pointer) {}
}

View File

@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart';
/// Returns a widget which displays a circle to be used as a slider.
///
/// Required arguments are init and end to set the initial selection.
/// onSelectionChange is a callback function which returns new values as the user
/// changes the interval.
/// The rest of the params are used to change the look and feel.
///
/// DoubleCircularSlider(5, 10, onSelectionChange: () => {});
class DoubleCircularSlider extends StatefulWidget {
/// the selection will be values between 0..divisions; max value is 300
final int divisions;
/// the initial value in the selection
final int init;
/// the end value in the selection
final int end;
/// the number of primary sectors to be painted
/// will be painted using selectionColor
final int primarySectors;
/// the number of secondary sectors to be painted
/// will be painted using baseColor
final int secondarySectors;
/// an optional widget that would be mounted inside the circle
final Widget child;
/// height of the canvas, default at 220
final double height;
/// width of the canvas, default at 220
final double width;
/// color of the base circle and sections
final Color baseColor;
/// color of the selection
final Color selectionColor;
/// color of the handlers
final Color handlerColor;
/// callback function when init and end change
/// (int init, int end) => void
final SelectionChanged<int> onSelectionChange;
/// callback function when init and end finish
/// (int init, int end) => void
final SelectionChanged<int> onSelectionEnd;
/// outter radius for the handlers
final double handlerOutterRadius;
/// if true an extra handler ring will be displayed in the handler
final bool showHandlerOutter;
/// stroke width for the slider, defaults at 12.0
final double sliderStrokeWidth;
/// if true, the onSelectionChange will also return the number of laps in the slider
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
final bool shouldCountLaps;
DoubleCircularSlider(
this.divisions,
this.init,
this.end, {
this.height,
this.width,
this.child,
this.primarySectors,
this.secondarySectors,
this.baseColor,
this.selectionColor,
this.handlerColor,
this.onSelectionChange,
this.onSelectionEnd,
this.handlerOutterRadius,
this.showHandlerOutter,
this.sliderStrokeWidth,
this.shouldCountLaps,
}) : assert(init >= 0 && init <= divisions,
'init has to be > 0 and < divisions value'),
assert(end >= 0 && end <= divisions,
'end has to be > 0 and < divisions value'),
assert(divisions >= 0 && divisions <= 300,
'divisions has to be > 0 and <= 300');
@override
_DoubleCircularSliderState createState() => _DoubleCircularSliderState();
}
class _DoubleCircularSliderState extends State<DoubleCircularSlider> {
int _init;
int _end;
@override
void initState() {
super.initState();
_init = widget.init;
_end = widget.end;
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.height ?? 220,
width: widget.width ?? 220,
child: CircularSliderPaint(
mode: CircularSliderMode.doubleHandler,
init: _init,
end: _end,
divisions: widget.divisions,
primarySectors: widget.primarySectors ?? 0,
secondarySectors: widget.secondarySectors ?? 0,
child: widget.child,
onSelectionChange: (newInit, newEnd, laps) {
if (widget.onSelectionChange != null) {
widget.onSelectionChange(newInit, newEnd, laps);
}
setState(() {
_init = newInit;
_end = newEnd;
});
},
onSelectionEnd: (newInit, newEnd, laps) {
if (widget.onSelectionEnd != null) {
widget.onSelectionEnd(newInit, newEnd, laps);
}
},
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
selectionColor:
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
handlerColor: widget.handlerColor ?? Colors.white,
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
showRoundedCapInSelection: false,
showHandlerOutter: widget.showHandlerOutter ?? true,
shouldCountLaps: widget.shouldCountLaps ?? false,
));
}
}

View File

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart';
import '../../utils/logger.dart';
/// Returns a widget which displays a circle to be used as a slider.
///
/// Required arguments are position and divisions to set the initial selection.
/// onSelectionChange is a callback function which returns new values as the user
/// changes the interval.
/// The rest of the params are used to change the look and feel.
///
/// SingleCircularSlider(5, 10, onSelectionChange: () => {});
class SingleCircularSlider extends StatefulWidget {
/// the selection will be values between 0..divisions; max value is 300
final int divisions;
/// the initial value in the selection
int position;
/// the number of primary sectors to be painted
/// will be painted using selectionColor
final int primarySectors;
/// the number of secondary sectors to be painted
/// will be painted using baseColor
final int secondarySectors;
/// an optional widget that would be mounted inside the circle
final Widget child;
/// height of the canvas, default at 220
final double height;
/// width of the canvas, default at 220
final double width;
/// color of the base circle and sections
final Color baseColor;
/// color of the selection
final Color selectionColor;
/// color of the handlers
final Color handlerColor;
/// callback function when init and end change
/// (int init, int end) => void
final SelectionChanged<int> onSelectionChange;
/// callback function when init and end finish
/// (int init, int end) => void
final SelectionChanged<int> onSelectionEnd;
/// outter radius for the handlers
final double handlerOutterRadius;
/// if true will paint a rounded cap in the selection slider start
final bool showRoundedCapInSelection;
/// if true an extra handler ring will be displayed in the handler
final bool showHandlerOutter;
/// stroke width for the slider, defaults at 12.0
final double sliderStrokeWidth;
/// if true, the onSelectionChange will also return the number of laps in the slider
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
final bool shouldCountLaps;
SingleCircularSlider(
this.divisions,
this.position, {
this.height,
this.width,
this.child,
this.primarySectors,
this.secondarySectors,
this.baseColor,
this.selectionColor,
this.handlerColor,
this.onSelectionChange,
this.onSelectionEnd,
this.handlerOutterRadius,
this.showRoundedCapInSelection,
this.showHandlerOutter,
this.sliderStrokeWidth,
this.shouldCountLaps,
}) : assert(position >= 0 && position <= divisions,
'init has to be > 0 and < divisions value'),
assert(divisions >= 0 && divisions <= 300,
'divisions has to be > 0 and <= 300');
@override
_SingleCircularSliderState createState() => _SingleCircularSliderState();
}
class _SingleCircularSliderState extends State<SingleCircularSlider> {
int _end;
@override
void initState() {
super.initState();
_end = widget.position;
Logger.d('Init: _end=$_end');
}
@override
Widget build(BuildContext context) {
Logger.d('Build: _end=$_end');
return Container(
height: widget.height ?? 220,
width: widget.width ?? 220,
child: CircularSliderPaint(
mode: CircularSliderMode.singleHandler,
init: 0,
end: _end,
divisions: widget.divisions,
primarySectors: widget.primarySectors ?? 0,
secondarySectors: widget.secondarySectors ?? 0,
child: widget.child,
onSelectionChange: (newInit, newEnd, laps) {
if (widget.onSelectionChange != null) {
widget.onSelectionChange(newInit, newEnd, laps);
}
setState(() {
_end = newEnd;
});
},
onSelectionEnd: (newInit, newEnd, laps) {
if (widget.onSelectionEnd != null) {
widget.onSelectionEnd(newInit, newEnd, laps);
}
},
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
selectionColor:
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
handlerColor: widget.handlerColor ?? Colors.white,
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
showRoundedCapInSelection: widget.showRoundedCapInSelection ?? false,
showHandlerOutter: widget.showHandlerOutter ?? true,
shouldCountLaps: widget.shouldCountLaps ?? false,
));
}
}

View File

@ -0,0 +1,77 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart' show CircularSliderMode;
import 'utils.dart';
class SliderPainter extends CustomPainter {
CircularSliderMode mode;
double startAngle;
double endAngle;
double sweepAngle;
Color selectionColor;
Color handlerColor;
double handlerOutterRadius;
bool showRoundedCapInSelection;
bool showHandlerOutter;
double sliderStrokeWidth;
Offset initHandler;
Offset endHandler;
Offset center;
double radius;
SliderPainter({
@required this.mode,
@required this.startAngle,
@required this.endAngle,
@required this.sweepAngle,
@required this.selectionColor,
@required this.handlerColor,
@required this.handlerOutterRadius,
@required this.showRoundedCapInSelection,
@required this.showHandlerOutter,
@required this.sliderStrokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
Paint progress = _getPaint(color: selectionColor);
center = Offset(size.width / 2, size.height / 2);
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
-pi / 2 + startAngle, sweepAngle, false, progress);
Paint handler = _getPaint(color: handlerColor, style: PaintingStyle.fill);
Paint handlerOutter = _getPaint(color: handlerColor, width: 2.0);
// draw handlers
if (mode == CircularSliderMode.doubleHandler) {
initHandler = radiansToCoordinates(center, -pi / 2 + startAngle, radius);
canvas.drawCircle(initHandler, 8.0, handler);
canvas.drawCircle(initHandler, handlerOutterRadius, handlerOutter);
}
endHandler = radiansToCoordinates(center, -pi / 2 + endAngle, radius);
canvas.drawCircle(endHandler, 8.0, handler);
if (showHandlerOutter) {
canvas.drawCircle(endHandler, handlerOutterRadius, handlerOutter);
}
}
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
Paint()
..color = color
..strokeCap =
showRoundedCapInSelection ? StrokeCap.round : StrokeCap.butt
..style = style ?? PaintingStyle.stroke
..strokeWidth = width ?? sliderStrokeWidth;
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

View File

@ -0,0 +1,75 @@
import 'dart:math';
import 'dart:ui';
double percentageToRadians(double percentage) => ((2 * pi * percentage) / 100);
double radiansToPercentage(double radians) {
var normalized = radians < 0 ? -radians : 2 * pi - radians;
var percentage = ((100 * normalized) / (2 * pi));
// TODO we have an inconsistency of pi/2 in terms of percentage and radians
return (percentage + 25) % 100;
}
double coordinatesToRadians(Offset center, Offset coords) {
var a = coords.dx - center.dx;
var b = center.dy - coords.dy;
return atan2(b, a);
}
Offset radiansToCoordinates(Offset center, double radians, double radius) {
var dx = center.dx + radius * cos(radians);
var dy = center.dy + radius * sin(radians);
return Offset(dx, dy);
}
double valueToPercentage(int time, int intervals) => (time / intervals) * 100;
int percentageToValue(double percentage, int intervals) =>
((percentage * intervals) / 100).round();
bool isPointInsideCircle(Offset point, Offset center, double rradius) {
var radius = rradius * 1.2;
return point.dx < (center.dx + radius) &&
point.dx > (center.dx - radius) &&
point.dy < (center.dy + radius) &&
point.dy > (center.dy - radius);
}
bool isPointAlongCircle(Offset point, Offset center, double radius) {
// distance is root(sqr(x2 - x1) + sqr(y2 - y1))
// i.e., (7,8) and (3,2) -> 7.21
var d1 = pow(point.dx - center.dx, 2);
var d2 = pow(point.dy - center.dy, 2);
var distance = sqrt(d1 + d2);
return (distance - radius).abs() < 10;
}
double getSweepAngle(double init, double end) {
if (end > init) {
return end - init;
}
return (100 - init + end).abs();
}
List<Offset> getSectionsCoordinatesInCircle(
Offset center, double radius, int sections) {
var intervalAngle = (pi * 2) / sections;
return List<int>.generate(sections, (int index) => index).map((i) {
var radians = (pi / 2) + (intervalAngle * i);
return radiansToCoordinates(center, radians, radius);
}).toList();
}
bool isAngleInsideRadiansSelection(double angle, double start, double sweep) {
var normalized = angle > pi / 2 ? 5 * pi / 2 - angle : pi / 2 - angle;
var end = (start + sweep) % (2 * pi);
return end > start
? normalized > start && normalized < end
: normalized > start || normalized < end;
}
// this is not 100% accurate but it works
// we just want to see if a value changed drastically its value
bool radiansWasModuloed(double current, double previous) {
return (previous - current).abs() > (3 * pi / 2);
}

View File

@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:math' as math;
class DynamicMultiColumnLayout extends MultiChildRenderObjectWidget {
final int minColumnWidth;
DynamicMultiColumnLayout({
Key key,
this.minColumnWidth: 350,
List<Widget> children = const <Widget>[],
}) : super(key: key, children: children);
@override
RenderCustomLayoutBox createRenderObject(BuildContext context) {
return RenderCustomLayoutBox(minColumnWidth: this.minColumnWidth);
}
}
class RenderCustomLayoutBox extends RenderBox
with ContainerRenderObjectMixin<RenderBox, CustomLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, CustomLayoutParentData> {
final int minColumnWidth;
RenderCustomLayoutBox({
this.minColumnWidth,
List<RenderBox> children,
}) {
addAll(children);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! CustomLayoutParentData) {
child.parentData = CustomLayoutParentData();
}
}
double _getIntrinsicHeight(double childSize(RenderBox child)) {
double inflexibleSpace = 0.0;
RenderBox child = firstChild;
while (child != null) {
inflexibleSpace += childSize(child);
final FlexParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
return inflexibleSpace;
}
double _getIntrinsicWidth(double childSize(RenderBox child)) {
double maxSpace = 0.0;
RenderBox child = firstChild;
while (child != null) {
maxSpace = math.max(maxSpace, childSize(child));
final FlexParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
return maxSpace;
}
@override
double computeMinIntrinsicWidth(double height) {
return _getIntrinsicWidth((RenderBox child) => child.getMinIntrinsicWidth(height));
}
@override
double computeMaxIntrinsicWidth(double height) {
return _getIntrinsicWidth((RenderBox child) => child.getMaxIntrinsicWidth(height));
}
@override
double computeMinIntrinsicHeight(double width) {
return _getIntrinsicHeight((RenderBox child) => child.getMinIntrinsicHeight(width));
}
@override
double computeMaxIntrinsicHeight(double width) {
return _getIntrinsicHeight((RenderBox child) => child.getMaxIntrinsicHeight(width));
}
@override
void performLayout() {
int columnsCount;
List<double> columnXPositions = [];
List<double> columnYPositions = [];
if (constraints.maxWidth < this.minColumnWidth) {
columnsCount = 1;
} else {
columnsCount = (constraints.maxWidth ~/ this.minColumnWidth);
}
if (childCount == 0 || columnsCount == 0) {
size = constraints.biggest;
assert(size.isFinite);
return;
}
double columnWidth = constraints.maxWidth / columnsCount;
double startY = 0;
for (int i =0; i < columnsCount; i++) {
columnXPositions.add(i*columnWidth);
columnYPositions.add(startY);
}
RenderBox child = firstChild;
while (child != null) {
final CustomLayoutParentData childParentData = child.parentData;
int columnToAdd = 0;
double minYPosition = columnYPositions[0];
for (int i=0; i<columnsCount; i++) {
if (columnYPositions[i] < minYPosition) {
minYPosition = columnYPositions[i];
columnToAdd = i;
}
}
child.layout(BoxConstraints.tightFor(width: columnWidth), parentUsesSize: true);
childParentData.offset = Offset(columnXPositions[columnToAdd], columnYPositions[columnToAdd]);
final Size newSize = child.size;
columnYPositions[columnToAdd] = minYPosition + newSize.height;
child = childParentData.nextSibling;
}
double width = constraints.maxWidth;
double height = 0;
for (int i=0; i<columnsCount; i++) {
if (columnYPositions[i] > height) {
height = columnYPositions[i];
}
}
size = Size(width, height);
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(HitTestResult result, { Offset position }) {
return defaultHitTestChildren(result, position: position);
}
}
class CustomLayoutParentData extends ContainerBoxParentData<RenderBox> {
}

View File

@ -17,9 +17,7 @@ class EntityHistoryConfig {
class EntityHistoryWidget extends StatefulWidget { class EntityHistoryWidget extends StatefulWidget {
final EntityHistoryConfig config; const EntityHistoryWidget({Key key}) : super(key: key);
const EntityHistoryWidget({Key key, @required this.config}) : super(key: key);
@override @override
_EntityHistoryWidgetState createState() { _EntityHistoryWidgetState createState() {
@ -33,6 +31,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
bool _needToUpdateHistory; bool _needToUpdateHistory;
DateTime _historyLastUpdated; DateTime _historyLastUpdated;
bool _disposed = false; bool _disposed = false;
Entity entity;
@override @override
void initState() { void initState() {
@ -75,10 +74,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
} else { } else {
_loadHistory(entity.entityId); _loadHistory(entity.entityId);
} }
return _buildChart(); return _buildChart(entity.historyConfig);
} }
Widget _buildChart() { Widget _buildChart(EntityHistoryConfig config) {
List<Widget> children = []; List<Widget> children = [];
if (_history == null) { if (_history == null) {
children.add( children.add(
@ -90,7 +89,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
); );
} else { } else {
children.add( children.add(
_selectChartWidget() _selectChartWidget(config)
); );
} }
children.add(Divider()); children.add(Divider());
@ -102,8 +101,8 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
); );
} }
Widget _selectChartWidget() { Widget _selectChartWidget(EntityHistoryConfig config) {
switch (widget.config.chartType) { switch (config.chartType) {
case EntityHistoryWidgetType.simple: { case EntityHistoryWidgetType.simple: {
return SimpleStateHistoryChartWidget( return SimpleStateHistoryChartWidget(
@ -114,14 +113,14 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
case EntityHistoryWidgetType.numericState: { case EntityHistoryWidgetType.numericState: {
return NumericStateHistoryChartWidget( return NumericStateHistoryChartWidget(
rawHistory: _history, rawHistory: _history,
config: widget.config, config: config,
); );
} }
case EntityHistoryWidgetType.numericAttributes: { case EntityHistoryWidgetType.numericAttributes: {
return CombinedHistoryChartWidget( return CombinedHistoryChartWidget(
rawHistory: _history, rawHistory: _history,
config: widget.config, config: config,
); );
} }

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
class SpoilerCard extends StatefulWidget {
final String title;
final Widget body;
final bool isExpanded;
SpoilerCard({Key key, @required this.title, @required this.body, this.isExpanded: false}) : super(key: key);
@override
_SpoilerCardState createState() => _SpoilerCardState();
}
class _SpoilerCardState extends State<SpoilerCard> {
bool _expanded;
@override
void initState() {
super.initState();
_expanded = widget.isExpanded;
}
@override
Widget build(BuildContext context) {
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text("${widget.title}"),
trailing: Icon(
_expanded ? Icons.arrow_drop_up : Icons.arrow_drop_down,
size: 20,
),
onTap: () => setState((){_expanded = !_expanded;}),
),
_expanded ? widget.body : Container(height: 0,)
],
),
);
}
}

View File

@ -33,13 +33,12 @@ class StartAuthEvent {
StartAuthEvent(this.oauthUrl, this.showButton); StartAuthEvent(this.oauthUrl, this.showButton);
} }
class ServiceCallEvent { class NotifyServiceCallEvent {
String domain; String domain;
String service; String service;
String entityId; String entityId;
Map<String, dynamic> additionalParams;
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams); NotifyServiceCallEvent(this.domain, this.service, this.entityId);
} }
class ShowPopupDialogEvent { class ShowPopupDialogEvent {
@ -63,9 +62,9 @@ class ShowPopupMessageEvent {
} }
class ShowEntityPageEvent { class ShowEntityPageEvent {
Entity entity; final Entity entity;
ShowEntityPageEvent(this.entity); ShowEntityPageEvent({this.entity});
} }
class ShowPageEvent { class ShowPageEvent {

View File

@ -1,4 +1,4 @@
part of '../main.dart'; part of 'main.dart';
class HomeAssistantUI { class HomeAssistantUI {
List<HAView> views; List<HAView> views;

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