Compare commits

..

1 Commits

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

View File

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

3
.gitignore vendored
View File

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

View File

@ -4,5 +4,9 @@ ENV ANDROID_HOME=/workspace/android-sdk \
FLUTTER_ROOT=/workspace/flutter \
FLUTTER_HOME=/workspace/flutter
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
&& sdk install java 8.0.242.j9-adpt"
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

View File

@ -3,12 +3,12 @@ image:
tasks:
- before: |
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
mkdir -p /home/gitpod/.android
touch /home/gitpod/.android/repositories.cfg
init: |
echo "Installing Flutter SDK..."
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.12.13+hotfix.7-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
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"
@ -18,6 +18,7 @@ tasks:
flutter doctor --android-licenses
flutter pub get
command: |
flutter pub upgrade
echo "Ready to go!"
flutter doctor
vscode:

View File

@ -1,14 +1,13 @@
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
# HA Client
## Native Android client for Home Assistant
### With notifications and Lovelace UI support
Visit [ha-client.app](http://ha-client.app/) for more info.
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
Discuss it on [Discord](https://discord.gg/nd6FZQ) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
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)

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,4 @@
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>

View File

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

View File

@ -3,4 +3,3 @@ org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true

View File

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

View File

@ -1,61 +0,0 @@
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<style>
body {
padding: 0;
margin: 0;
widows: 100%;
height: 100%;
}
video {
width: 100%;
}
</style>
<script>
var messageChannel = '{{message_channel}}';
</script>
</head>
<body>
<video id="screen" width="100%" controls></video>
<script>
if (Hls.isSupported()) {
var video = document.getElementById('screen');
var hls = new Hls();
hls.on(Hls.Events.ERROR, function (event, data) {
if (data.fatal) {
switch(data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
// try to recover network error
console.log("fatal network error encountered, try to recover");
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.log("fatal media error encountered, try to recover");
hls.recoverMediaError();
break;
default:
// cannot recover
hls.destroy();
break;
}
}
});
// bind them together
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
console.log("video and hls.js are now bound together !");
hls.loadSource("{{stream_url}}");
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
console.log("manifest loaded, found " + data.levels.length + " quality level");
video.play();
video.onloadedmetadata = function() {
window[messageChannel].postMessage(document.body.clientWidth / video.offsetHeight);
};
});
});
}
</script>
</body>
</html>

View File

@ -1,28 +0,0 @@
<html>
<head>
<style>
body {
padding: 0;
margin: 0;
widows: 100%;
height: 100%;
}
img {
width: 100%;
}
</style>
<script>
var messageChannel = '{{message_channel}}';
window.onload = function() {
var img = document.getElementById('screen');
if (img) {
window[messageChannel].postMessage(document.body.clientWidth / img.offsetHeight);
}
};
</script>
</head>
<body>
<img id="screen" src="{{stream_url}}">
</body>
</html>

View File

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

View File

@ -10,7 +10,6 @@ class HACard {
bool showName;
bool showState;
bool showEmpty;
bool showHeaderToggle;
int columnsCount;
List stateFilter;
List states;
@ -27,7 +26,6 @@ class HACard {
this.linkedEntityWrapper,
this.columnsCount: 4,
this.showName: true,
this.showHeaderToggle: true,
this.showState: true,
this.stateFilter: const [],
this.showEmpty: true,
@ -47,70 +45,13 @@ class HACard {
List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) {
if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) {
return false;
}
List currentStateFilter;
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
currentStateFilter = entityWrapper.stateFilter;
} else {
currentStateFilter = stateFilter;
if (stateFilter.isNotEmpty) {
return stateFilter.contains(entityWrapper.entity.state);
}
bool showByFilter = currentStateFilter.isEmpty;
for (var allowedState in currentStateFilter) {
if (allowedState is String && allowedState == entityWrapper.entity.state) {
showByFilter = true;
break;
} else if (allowedState is Map) {
try {
var tmpVal = allowedState['attribute'] != null ? entityWrapper.entity.getAttribute(allowedState['attribute']) : entityWrapper.entity.state;
var valToCompareWith = allowedState['value'];
var valToCompare;
if (valToCompareWith is! String && tmpVal is String) {
valToCompare = double.tryParse(tmpVal);
} else {
valToCompare = tmpVal;
}
if (valToCompare != null) {
bool result;
switch (allowedState['operator']) {
case '<=': { result = valToCompare <= valToCompareWith;}
break;
case '<': { result = valToCompare < valToCompareWith;}
break;
case '>=': { result = valToCompare >= valToCompareWith;}
break;
case '>': { result = valToCompare > valToCompareWith;}
break;
case '!=': { result = valToCompare != valToCompareWith;}
break;
case 'regex': {
RegExp regExp = RegExp(valToCompareWith.toString());
result = regExp.hasMatch(valToCompare.toString());
}
break;
default: {
result = valToCompare == valToCompareWith;
}
}
if (result) {
showByFilter = true;
break;
}
}
} catch (e) {
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState');
Logger.e('$e');
}
}
}
return showByFilter;
return true;
}).toList();
}

View File

@ -132,33 +132,7 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
Widget headerSwitch;
if (card.showHeaderToggle) {
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
}).map((EntityWrapper en) {
return en.entity.entityId;
}).toList();
headerSwitch = Switch(
value: headerToggleVal,
onChanged: (val) {
if (entitiesToToggle.isNotEmpty) {
ConnectionManager().callService(
domain: "homeassistant",
service: val ? "turn_on" : "turn_off",
entityId: entitiesToToggle
);
}
},
);
}
body.add(
CardHeader(
name: card.name,
trailing: headerSwitch
)
);
body.add(CardHeader(name: card.name));
entitiesToShow.forEach((EntityWrapper entity) {
body.add(
Padding(
@ -198,6 +172,9 @@ class CardWidget extends StatelessWidget {
body.add(CardHeader(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle(
color: Colors.grey
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
@ -303,23 +280,21 @@ class CardWidget extends StatelessWidget {
}
Widget _buildEntityButtonCard(BuildContext context) {
card.linkedEntityWrapper.overrideName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName.toUpperCase();
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: EntityButtonCardBody(
showName: card.showName,
),
child: EntityButtonCardBody(),
handleTap: true
)
);
}
Widget _buildGaugeCard(BuildContext context) {
card.linkedEntityWrapper.overrideName = card.name ??
card.linkedEntityWrapper.displayName = card.name ??
card.linkedEntityWrapper.displayName;
card.linkedEntityWrapper.unitOfMeasurementOverride = card.unit ??
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
card.linkedEntityWrapper.unitOfMeasurement;
return Card(
child: EntityModel(
@ -335,7 +310,7 @@ class CardWidget extends StatelessWidget {
}
Widget _buildLightCard(BuildContext context) {
card.linkedEntityWrapper.overrideName = card.name ??
card.linkedEntityWrapper.displayName = card.name ??
card.linkedEntityWrapper.displayName;
return Card(
child: EntityModel(
@ -352,11 +327,7 @@ class CardWidget extends StatelessWidget {
Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = [];
body.add(
CardHeader(
name: card.name ?? ""
)
);
body.add(CardHeader(name: card.name ?? ""));
List<Widget> result = [];
if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[

View File

@ -18,7 +18,7 @@ class CardHeader extends StatelessWidget {
title: Text("$name",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headline),
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
);
} else {
result = new Container(width: 0.0, height: 0.0);

View File

@ -2,10 +2,8 @@ part of '../../main.dart';
class EntityButtonCardBody extends StatelessWidget {
final bool showName;
EntityButtonCardBody({
Key key, this.showName: true,
Key key,
}) : super(key: key);
@override
@ -21,7 +19,6 @@ class EntityButtonCardBody extends StatelessWidget {
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: FractionallySizedBox(
widthFactor: 1,
child: Column(
@ -42,15 +39,13 @@ class EntityButtonCardBody extends StatelessWidget {
}
Widget _buildName() {
if (showName) {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
wordsWrap: true,
textAlign: TextAlign.center
textAlign: TextAlign.center,
fontSize: Sizes.nameFontSize,
);
}
return Container(width: 0, height: 0);
}
}

View File

@ -14,11 +14,10 @@ class GaugeCardBody extends StatefulWidget {
class _GaugeCardBodyState extends State<GaugeCardBody> {
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
List<charts.Series> seriesList;
List<charts.Series<GaugeSegment, String>> _createData(double value) {
double fixedValue;
double value = entityWrapper.entity.doubleState;
if (value > widget.max) {
fixedValue = widget.max.toDouble();
} else if (value < widget.min) {
@ -26,151 +25,129 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
} 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),
];
List<GaugeRange> ranges;
if (widget.severity != null && widget.severity["green"] is int && widget.severity["red"] is int && widget.severity["yellow"] is int) {
List<RangeContainer> rangesList = <RangeContainer>[
RangeContainer(widget.severity["green"], HAClientTheme().getGreenGaugeColor()),
RangeContainer(widget.severity["red"], HAClientTheme().getRedGaugeColor()),
RangeContainer(widget.severity["yellow"], HAClientTheme().getYellowGaugeColor())
];
rangesList.sort((current, next) {
if (current.startFrom > next.startFrom) {
return 1;
}
if (current.startFrom < next.startFrom) {
return -1;
}
return 0;
});
ranges = [
GaugeRange(
startValue: rangesList[0].startFrom.toDouble(),
endValue: rangesList[1].startFrom.toDouble(),
color: fixedValue < rangesList[1].startFrom ? rangesList[0].color : rangesList[0].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[1].startFrom.toDouble(),
endValue: rangesList[2].startFrom.toDouble(),
color: (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) ? rangesList[1].color : rangesList[1].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[2].startFrom.toDouble(),
endValue: widget.max.toDouble(),
color: fixedValue >= rangesList[2].startFrom ? rangesList[2].color : rangesList[2].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
)
];
}
if (ranges == null) {
ranges = <GaugeRange>[
GaugeRange(
startValue: widget.min.toDouble(),
endValue: widget.max.toDouble(),
color: Theme.of(context).primaryColorDark,
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
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(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: AspectRatio(
aspectRatio: 2,
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 fontSizeFactor;
if (constraints.maxWidth > 300.0) {
fontSizeFactor = 1.6;
} else if (constraints.maxWidth > 150.0) {
fontSizeFactor = 1;
} else if (constraints.maxWidth > 100.0) {
fontSizeFactor = 0.6;
} else {
fontSizeFactor = 0.4;
}
return SfRadialGauge(
axes: <RadialAxis>[
RadialAxis(
maximum: widget.max.toDouble(),
minimum: widget.min.toDouble(),
showLabels: false,
showTicks: false,
canScaleToFit: true,
ranges: ranges,
annotations: <GaugeAnnotation>[
GaugeAnnotation(
angle: -90,
positionFactor: 1.3,
//verticalAlignment: GaugeAlignment.far,
widget: EntityName(
textStyle: Theme.of(context).textTheme.body1.copyWith(
fontSize: Theme.of(context).textTheme.body1.fontSize * fontSizeFactor
),
),
),
GaugeAnnotation(
angle: 180,
positionFactor: 0,
verticalAlignment: GaugeAlignment.center,
widget: SimpleEntityState(
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,
textStyle: Theme.of(context).textTheme.title.copyWith(
fontSize: Theme.of(context).textTheme.title.fontSize * fontSizeFactor,
padding: EdgeInsets.all(0.0),
fontSize: fontSize,
//padding: EdgeInsets.only(top: Sizes.rowPadding),
),
);
}
),
),
)
],
axisLineStyle: AxisLineStyle(
thickness: 0.3,
thicknessUnit: GaugeSizeUnit.factor
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,
),
);
}
),
startAngle: 180,
endAngle: 0,
pointers: <GaugePointer>[
NeedlePointer(
value: fixedValue,
lengthUnit: GaugeSizeUnit.factor,
needleLength: 0.9,
needleColor: Theme.of(context).accentColor,
enableAnimation: true,
needleStartWidth: 1,
animationType: AnimationType.bounceOut,
needleEndWidth: 3,
knobStyle: KnobStyle(
sizeUnit: GaugeSizeUnit.factor,
color: Theme.of(context).buttonColor,
knobRadius: 0.1
)
)
]
)
],
);
},
),
),
);
}
}
class RangeContainer {
final int startFrom;
Color color;
class GaugeSegment {
final String segment;
final double value;
final charts.Color color;
RangeContainer(this.startFrom, this.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

@ -6,6 +6,7 @@ class GlanceCardEntityContainer extends StatelessWidget {
final bool showState;
final bool nameInTheBottom;
final double iconSize;
final double nameFontSize;
final bool wordsWrapInName;
GlanceCardEntityContainer({
@ -14,6 +15,7 @@ class GlanceCardEntityContainer extends StatelessWidget {
@required this.showState,
this.nameInTheBottom: false,
this.iconSize: Sizes.iconSize,
this.nameFontSize: Sizes.smallFontSize,
this.wordsWrapInName: false
}) : super(key: key);
@ -29,7 +31,7 @@ class GlanceCardEntityContainer extends StatelessWidget {
List<Widget> result = [];
if (!nameInTheBottom) {
if (showName) {
result.add(_buildName(context));
result.add(_buildName());
}
} else {
if (showState) {
@ -47,7 +49,7 @@ class GlanceCardEntityContainer extends StatelessWidget {
result.add(_buildState());
}
} else {
result.add(_buildName(context));
result.add(_buildName());
}
return Center(
@ -58,18 +60,17 @@ class GlanceCardEntityContainer extends StatelessWidget {
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
),
);
}
Widget _buildName(BuildContext context) {
Widget _buildName() {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: wordsWrapInName,
textAlign: TextAlign.center,
textStyle: Theme.of(context).textTheme.body1,
fontSize: nameFontSize,
);
}

View File

@ -34,5 +34,57 @@ class _LightCardBodyState extends State<LightCardBody> {
),
);
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: AspectRatio(
aspectRatio: 1.5,
child: Stack(
fit: StackFit.expand,
overflow: Overflow.clip,
children: [
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: 2*fontSize),
child: SimpleEntityState(
//textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
bold: true,
textAlign: TextAlign.center,
padding: EdgeInsets.all(0.0),
fontSize: fontSize,
//padding: EdgeInsets.only(top: Sizes.rowPadding),
),
);
}
),
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: fontSize),
child: EntityName(
fontSize: fontSize,
maxLines: 1,
padding: EdgeInsets.all(0.0),
textAlign: TextAlign.center,
textOverflow: TextOverflow.ellipsis,
),
);
}
),
)
]
)
),
);
}
}

View File

@ -51,10 +51,6 @@ class EntityUIAction {
String holdNavigationPath;
String holdService;
Map<String, dynamic> holdServiceData;
String doubleTapAction = EntityUIAction.none;
String doubleTapNavigationPath;
String doubleTapService;
Map<String, dynamic> doubleTapServiceData;
EntityUIAction({rawEntityData}) {
if (rawEntityData != null) {
@ -80,17 +76,6 @@ class EntityUIAction {
holdServiceData = rawEntityData["hold_action"]["service_data"];
}
}
if (rawEntityData["double_tap_action"] != null) {
if (rawEntityData["double_tap_action"] is String) {
doubleTapAction = rawEntityData["double_tap_action"];
} else {
doubleTapAction =
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
doubleTapService = rawEntityData["double_tap_action"]["service"];
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
}
}
}
}
@ -126,10 +111,10 @@ class Sizes {
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 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
setState(() {
_tmpPosition = position.roundToDouble();
_changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_cover_position", entityId: entity.entityId, data: {"position": _tmpPosition.round()});
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
});
}
@ -26,7 +26,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
setState(() {
_tmpTiltPosition = position.roundToDouble();
_changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_cover_tilt_position", entityId: entity.entityId, data: {"tilt_position": _tmpTiltPosition.round()});
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
});
}
@ -64,7 +64,9 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Position"),
child: Text("Position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
),
Slider(
value: _tmpPosition,
@ -116,7 +118,9 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Tilt position"),
child: Text("Tilt position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -131,18 +135,18 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
class CoverTiltControlsWidget extends StatelessWidget {
void _open(CoverEntity entity) {
ConnectionManager().callService(
domain: entity.domain, service: "open_cover_tilt", entityId: entity.entityId);
eventBus.fire(new ServiceCallEvent(
entity.domain, "open_cover_tilt", entity.entityId, null));
}
void _close(CoverEntity entity) {
ConnectionManager().callService(
domain: entity.domain, service: "close_cover_tilt", entityId: entity.entityId);
eventBus.fire(new ServiceCallEvent(
entity.domain, "close_cover_tilt", entity.entityId, null));
}
void _stop(CoverEntity entity) {
ConnectionManager().callService(
domain: entity.domain, service: "stop_cover_tilt", entityId: entity.entityId);
eventBus.fire(new ServiceCallEvent(
entity.domain, "stop_cover_tilt", entity.entityId, null));
}
@override

View File

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

View File

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

View File

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

View File

@ -15,19 +15,21 @@ class DefaultEntityContainer extends StatelessWidget {
return MissedEntityWidget();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
return Divider();
return Divider(
color: Colors.black45,
);
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Divider(),
Divider(
color: Colors.black45,
),
Text(
"${entityModel.entityWrapper.entity.displayName}",
style: HAClientTheme().getLinkTextStyle(context).copyWith(
decoration: TextDecoration.none
)
style: TextStyle(color: Colors.blue),
)
],
);
@ -59,11 +61,6 @@ class DefaultEntityContainer extends StatelessWidget {
entityModel.entityWrapper.handleTap();
}
},
onDoubleTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleDoubleTap();
}
},
child: result,
);
} else {

View File

@ -153,7 +153,7 @@ class Entity {
domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
@ -221,7 +221,7 @@ class Entity {
String getAttribute(String attributeName) {
if (attributes != null) {
return attributes["$attributeName"].toString();
return attributes["$attributeName"];
}
return null;
}

View File

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

View File

@ -67,7 +67,7 @@ class EntityIcon extends StatelessWidget {
padding: padding,
child: buildIcon(
entityWrapper,
color ?? HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context)
color ?? EntityColor.stateColor(entityWrapper.entity.state)
),
);
}

View File

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

View File

@ -16,8 +16,8 @@ class EntityPageLayout extends StatelessWidget {
children: <Widget>[
showClose ?
Container(
color: Theme.of(context).primaryColor,
height: 40,
color: Colors.blue[300],
height: 36,
child: Row(
children: <Widget>[
Expanded(
@ -25,15 +25,19 @@ class EntityPageLayout extends StatelessWidget {
padding: EdgeInsets.only(left: 8),
child: Text(
entity.displayName,
style: Theme.of(context).primaryTextTheme.headline
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 22
),
),
),
),
IconButton(
padding: EdgeInsets.all(0),
icon: Icon(Icons.close),
color: Theme.of(context).primaryTextTheme.headline.color,
iconSize: 36.0,
color: Colors.white,
iconSize: 30.0,
onPressed: () {
eventBus.fire(ShowEntityPageEvent());
},

View File

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

View File

@ -2,46 +2,47 @@ part of '../main.dart';
class EntityWrapper {
String overrideName;
final String overrideIcon;
String displayName;
String icon;
String unitOfMeasurement;
String entityPicture;
EntityUIAction uiAction;
Entity entity;
String unitOfMeasurementOverride;
final List stateFilter;
String get icon => overrideIcon ?? entity.icon;
String get entityPicture => entity.entityPicture;
String get displayName => overrideName ?? entity.displayName;
String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement;
EntityWrapper({
this.entity,
this.overrideIcon,
this.overrideName,
this.uiAction,
this.stateFilter
String icon,
String displayName,
this.uiAction
}) {
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
this.icon = icon ?? entity.icon;
if (icon == null) {
entityPicture = entity.entityPicture;
}
this.displayName = displayName ?? entity.displayName;
if (uiAction == null) {
uiAction = EntityUIAction();
}
unitOfMeasurement = entity.unitOfMeasurement;
}
}
void handleTap() {
switch (uiAction.tapAction) {
case EntityUIAction.toggle: {
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break;
}
case EntityUIAction.callService: {
if (uiAction.tapService != null) {
ConnectionManager().callService(
domain: uiAction.tapService.split(".")[0],
service: uiAction.tapService.split(".")[1],
data: uiAction.tapServiceData
);
eventBus.fire(
ServiceCallEvent(uiAction.tapService.split(".")[0],
uiAction.tapService.split(".")[1], null,
uiAction.tapServiceData));
}
break;
}
@ -57,7 +58,7 @@ class EntityWrapper {
}
case EntityUIAction.navigate: {
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
if (uiAction.tapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
@ -75,17 +76,17 @@ class EntityWrapper {
void handleHold() {
switch (uiAction.holdAction) {
case EntityUIAction.toggle: {
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break;
}
case EntityUIAction.callService: {
if (uiAction.holdService != null) {
ConnectionManager().callService(
domain: uiAction.holdService.split(".")[0],
service: uiAction.holdService.split(".")[1],
data: uiAction.holdServiceData
);
eventBus.fire(
ServiceCallEvent(uiAction.holdService.split(".")[0],
uiAction.holdService.split(".")[1], null,
uiAction.holdServiceData));
}
break;
}
@ -97,7 +98,7 @@ class EntityWrapper {
}
case EntityUIAction.navigate: {
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
if (uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
@ -112,44 +113,4 @@ class EntityWrapper {
}
}
void handleDoubleTap() {
switch (uiAction.doubleTapAction) {
case EntityUIAction.toggle: {
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
break;
}
case EntityUIAction.callService: {
if (uiAction.doubleTapService != null) {
ConnectionManager().callService(
domain: uiAction.doubleTapService.split(".")[0],
service: uiAction.doubleTapService.split(".")[1],
data: uiAction.doubleTapServiceData
);
}
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
Launcher.launchURL(uiAction.doubleTapService);
}
break;
}
default: {
break;
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -84,11 +84,12 @@ class MediaPlayerEntity extends Entity {
}
bool canCalculateActualPosition() {
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds > 0;
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds >= 0;
}
double getActualPosition() {
double result = 0;
if (canCalculateActualPosition()) {
Duration durationD;
Duration positionD;
durationD = Duration(seconds: durationSeconds);
@ -100,6 +101,8 @@ class MediaPlayerEntity extends Entity {
.difference(positionLastUpdated)
.inSeconds;
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
}
return result;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,10 @@ class UniversalSlider extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Sizes.rowPadding,),
Text("$title"),
Text(
"$title",
style: TextStyle(fontSize: Sizes.stateFontSize),
),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import 'dart:convert';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -22,19 +22,16 @@ import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/circular_slider/single_circular_slider.dart';
import 'package:share/receive_share_state.dart';
import 'package:share/share.dart';
import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart';
import 'package:uni_links/uni_links.dart';
import 'package:workmanager/workmanager.dart' as workManager;
import 'package:geolocator/geolocator.dart';
import 'package:battery/battery.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
import 'package:webview_flutter/webview_flutter.dart';
import 'package:syncfusion_flutter_core/core.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'utils/logger.dart';
import '.secrets.dart';
part 'const.dart';
part 'utils/launcher.dart';
@ -75,6 +72,7 @@ part 'entities/universal_slider.widget.dart';
part 'entities/flat_service_button.widget.dart';
part 'entities/light/widgets/light_color_picker.dart';
part 'entities/camera/widgets/camera_stream_view.dart';
part 'entities/entity_colors.class.dart';
part 'plugins/history_chart/entity_history.dart';
part 'plugins/history_chart/simple_state_history_chart.dart';
part 'plugins/history_chart/numeric_state_history_chart.dart';
@ -86,7 +84,6 @@ part 'entities/slider/widgets/slider_controls.dart';
part 'entities/text/widgets/text_input_state.dart';
part 'entities/select/widgets/select_state.dart';
part 'entities/simple_state.widget.dart';
part 'entities/entity_picture.widget.dart';
part 'entities/timer/widgets/timer_state.dart';
part 'entities/climate/widgets/climate_state.widget.dart';
part 'entities/cover/widgets/cover_state.dart';
@ -102,17 +99,14 @@ part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.d
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/connection_settings.part.dart';
part 'pages/settings.page.dart';
part 'pages/purchase.page.dart';
part 'pages/widgets/product_purchase.widget.dart';
part 'pages/widgets/page_loading_indicator.dart';
part 'pages/widgets/page_loading_error.dart';
part 'pages/panel.page.dart';
part 'pages/main/main.page.dart';
part 'pages/settings/integration_settings.part.dart';
part 'pages/settings/app_settings.page.dart';
part 'pages/settings/lookandfeel_settings.part.dart';
part 'pages/zha_page.dart';
part 'pages/main.page.dart';
part 'pages/integration_settings.page.dart';
part 'home_assistant.class.dart';
part 'pages/log.page.dart';
part 'pages/entity.page.dart';
@ -124,7 +118,6 @@ part 'managers/mobile_app_integration_manager.class.dart';
part 'managers/connection_manager.class.dart';
part 'managers/device_info_manager.class.dart';
part 'managers/startup_user_messages_manager.class.dart';
part 'managers/theme_manager.dart';
part 'ui.dart';
part 'view.class.dart';
part 'cards/card.class.dart';
@ -143,164 +136,62 @@ part 'entities/entity_page_layout.widget.dart';
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
part 'pages/whats_new.page.dart';
part 'pages/fullscreen.page.dart';
EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client";
const appVersionNumber = "0.8.3";
const appVersionNumber = "0.7.0";
const appVersionAdd = "";
const appVersion = "$appVersionNumber$appVersionAdd";
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console.
if (Logger.isInDebugMode) {
Logger.e('Caught error: $error');
Logger.p(stackTrace);
}
Crashlytics.instance.recordError(error, stackTrace);
}
const appVersion = "$appVersionNumber-$appVersionAdd";
void main() async {
Crashlytics.instance.enableInDevMode = false;
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
FlutterError.onError = (FlutterErrorDetails details) {
Logger.e("Caut Flutter runtime error: ${details.exception}");
FlutterError.onError = (errorDetails) {
Logger.e( "${errorDetails.exception}");
if (Logger.isInDebugMode) {
FlutterError.dumpErrorToConsole(details);
FlutterError.dumpErrorToConsole(errorDetails);
}
Crashlytics.instance.recordFlutterError(details);
};
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
runZoned(() {
runApp(new HAClientApp(
theme: theme,
));
}, onError: (error, stack) {
_reportError(error, stack);
});
}
class HAClientApp extends StatefulWidget {
final AppTheme theme;
const HAClientApp({Key key, this.theme: AppTheme.defaultTheme}) : super(key: key);
@override
_HAClientAppState createState() => new _HAClientAppState();
}
class _HAClientAppState extends State<HAClientApp> {
StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription;
StreamSubscription _themeChangeSubscription;
AppTheme _currentTheme = AppTheme.defaultTheme;
@override
void initState() {
InAppPurchaseConnection.enablePendingPurchases();
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
_purchaseUpdateSubscription = purchaseUpdates.listen((purchases) {
_handlePurchaseUpdates(purchases);
});
_currentTheme = widget.theme;
_themeChangeSubscription = eventBus.on<ChangeThemeEvent>().listen((event){
setState(() {
_currentTheme = event.theme;
});
});
workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
super.initState();
}
runApp(new HAClientApp());
void _handlePurchaseUpdates(purchase) {
if (purchase is List<PurchaseDetails>) {
if (purchase[0].status == PurchaseStatus.purchased) {
eventBus.fire(ShowPopupMessageEvent(
title: "Thanks a lot!",
body: "Thank you for supporting HA Client development!",
buttonText: "Ok"
));
} else {
Logger.d("Purchase change handler: ${purchase[0].status}");
}
} else {
Logger.e("Something wrong with purchase handling. Got: $purchase");
}
}, onError: (error, stack) {
Logger.e("$error");
Logger.e("$stack");
if (Logger.isInDebugMode) {
debugPrint("$stack");
}
});
}
class HAClientApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: appName,
theme: HAClientTheme().getThemeData(_currentTheme),
darkTheme: HAClientTheme().darkTheme,
debugShowCheckedModeBanner: false,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: "/",
routes: {
"/": (context) => MainPage(title: 'HA Client'),
"/app-settings": (context) => AppSettingsPage(),
"/connection-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.connectionSettings),
"/integration-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.integrationSettings),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/integration-settings": (context) => IntegrationSettingsPage(title: "Integration settings"),
"/putchase": (context) => PurchasePage(title: "Support app development"),
"/play-media": (context) => PlayMediaPage(
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
),
"/log-view": (context) => LogViewPage(title: "Log"),
"/webview": (context) => standaloneWebview.WebviewScaffold(
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop()
),
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
),
),
"/whats-new": (context) => WhatsNewPage(),
"/haclient_zha": (context) => ZhaPage(),
"/auth": (context) => new standaloneWebview.WebviewScaffold(
url: "${ConnectionManager().oauthUrl}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#authentication")
),
title: new Text("Login with HA"),
actions: <Widget>[
FlatButton(
child: Text("Manual", style: Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline
)),
onPressed: () {
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
},
)
],
),
)
"/whats-new": (context) => WhatsNewPage()
},
);
}
@override
void dispose() {
_purchaseUpdateSubscription.cancel();
_themeChangeSubscription.cancel();
super.dispose();
}
}

View File

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

View File

@ -19,6 +19,7 @@ class ConnectionManager {
String _tempToken;
String oauthUrl;
String webhookId;
bool useLovelace = true;
bool settingsLoaded = false;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
@ -27,7 +28,6 @@ class ConnectionManager {
bool isConnected = false;
var onStateChangeCallback;
var onLovelaceUpdatedCallback;
IOWebSocketChannel _socket;
@ -38,8 +38,9 @@ class ConnectionManager {
Completer completer = Completer();
bool stopInit = false;
if (loadSettings) {
Logger.d("Loading settings...");
Logger.e("Loading settings...");
SharedPreferences prefs = await SharedPreferences.getInstance();
useLovelace = prefs.getBool('use-lovelace') ?? true;
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
@ -58,9 +59,9 @@ class ConnectionManager {
_token = await storage.read(key: "hacl_llt");
Logger.e("Long-lived token read successful");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'https://ha-client.app')}&redirect_uri=${Uri
'http://ha-client.homemade.systems')}&redirect_uri=${Uri
.encodeComponent(
'https://ha-client.app/service/auth_callback.html')}";
'haclient://auth')}";
settingsLoaded = true;
} catch (e) {
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
@ -97,23 +98,16 @@ class ConnectionManager {
void _doConnect({Completer completer, bool forceReconnect}) {
if (forceReconnect || !isConnected) {
_disconnect().then((_){
_connect().timeout(connectTimeout).then((_) {
_connect().timeout(connectTimeout, onTimeout: () {
_disconnect().then((_) {
if (completer != null && !completer.isCompleted) {
completer.completeError(HAError("Connection timeout"));
}
});
}).then((_) {
completer?.complete();
}).catchError((e) {
_disconnect().then((_) {
if (e is TimeoutException) {
if (connecting != null && !connecting.isCompleted) {
connecting.completeError(HAError("Connection timeout"));
}
completer?.completeError(HAError("Connection timeout"));
} else if (e is HAError) {
completer?.completeError(e);
} else {
completer?.completeError(HAError("${e.toString()}"));
}
});
});
});
} else {
completer?.complete();
@ -130,7 +124,6 @@ class ConnectionManager {
connecting = Completer();
_disconnect().then((_) {
Logger.d("Socket connecting...");
try {
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
@ -147,21 +140,11 @@ class ConnectionManager {
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
Logger.d("[Connection] Subscribing to events");
sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "lovelace_updated"},
);
sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
).whenComplete((){
_messageResolver["auth"]?.complete();
_messageResolver.remove("auth");
if (_token != null) {
if (!connecting.isCompleted) connecting.complete();
}
});
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
@ -175,9 +158,6 @@ class ConnectionManager {
onDone: () => _handleSocketClose(connecting),
onError: (e) => _handleSocketError(e, connecting)
);
} catch(exeption) {
connecting.completeError(HAError("${exeption.toString()}"));
}
});
return connecting.future;
}
@ -209,19 +189,18 @@ class ConnectionManager {
//Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError("${data["error"]["code"]}");
//Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
}
_messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") {
if (data["event"] != null) {
if (data["event"]["event_type"] == "state_changed") {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
//Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]);
} else if (data["event"]["event_type"] == "lovelace_updated") {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: $data");
onLovelaceUpdatedCallback();
}
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $data");
}
} else {
Logger.d("[Received unhandled] <== ${data.toString()}");
@ -230,24 +209,38 @@ class ConnectionManager {
void _handleSocketClose(Completer connectionCompleter) {
Logger.d("Socket disconnected.");
_disconnect().then((_) {
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
}
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
}
}
void _handleSocketError(e, Completer connectionCompleter) {
Logger.e("Socket stream Error: $e");
_disconnect().then((_) {
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
}
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
}
}
Future _authenticate() {
@ -336,13 +329,13 @@ class ConnectionManager {
_messageResolver[callbackName] = _completer;
String rawMessage = json.encode(dataObject);
if (!isConnected) {
_connect().timeout(connectTimeout).then((_) {
_connect().timeout(connectTimeout, onTimeout: (){
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}).then((_) {
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
_socket.sink.add(rawMessage);
}).catchError((e) {
if (!_completer.isCompleted) {
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}
_completer.completeError(e);
});
} else {
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
@ -355,27 +348,25 @@ class ConnectionManager {
_currentMessageId += 1;
}
Future callService({@required String domain, @required String service, entityId, Map data}) {
eventBus.fire(NotifyServiceCallEvent(domain, service, entityId));
Logger.d("Service call: $domain.$service, $entityId, $data");
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
Completer completer = Completer();
Map serviceData = {};
if (entityId != null) {
serviceData["entity_id"] = entityId;
}
if (data != null && data.isNotEmpty) {
serviceData.addAll(data);
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
serviceData.addAll(additionalServiceData);
}
if (serviceData.isNotEmpty)
sendHTTPPost(
endPoint: "/api/services/$domain/$service",
data: json.encode(serviceData)
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
).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
sendHTTPPost(
endPoint: "/api/services/$domain/$service"
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
).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;
}
@ -416,12 +407,11 @@ class ConnectionManager {
headers: headers,
body: data
).then((response) {
if (response.statusCode >= 200 && response.statusCode < 300 ) {
Logger.d("[Received] <== HTTP ${response.statusCode}");
if (response.statusCode >= 200 && response.statusCode < 300 ) {
completer.complete(response.body);
} else {
Logger.d("[Received] <== HTTP ${response.statusCode}: ${response.body}");
completer.completeError(response);
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
}
}).catchError((e) {
completer.completeError(e);

View File

@ -14,7 +14,7 @@ class LocationManager {
}
final int defaultUpdateIntervalMinutes = 20;
final String backgroundTaskId = "haclocationtask0";
final String backgroundTaskId = "haclocationtask4352";
final String backgroundTaskTag = "haclocation";
Duration _updateInterval;
bool _isRunning;
@ -57,110 +57,76 @@ class LocationManager {
}
_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) {
Duration interval;
int delayFactor;
int taskCount;
Logger.d("Starting location update for every ${_updateInterval
.inMinutes} minutes...");
if (_updateInterval.inMinutes == 10) {
interval = Duration(minutes: 20);
taskCount = 2;
delayFactor = 10;
} else if (_updateInterval.inMinutes == 5) {
interval = Duration(minutes: 15);
taskCount = 3;
delayFactor = 5;
} else {
interval = _updateInterval;
taskCount = 1;
delayFactor = 0;
}
for (int i = 1; i <= taskCount; i++) {
int delay = i*delayFactor;
Logger.d("Scheduling location update task #$i for every ${interval.inMinutes} minutes in $delay minutes...");
await workManager.Workmanager.registerPeriodicTask(
"$backgroundTaskId$i",
"haClientLocationTracking-0$i",
backgroundTaskId,
"haClientLocationTracking",
tag: backgroundTaskTag,
inputData: {
"webhookId": webhookId,
"httpWebHost": httpWebHost
},
frequency: interval,
initialDelay: Duration(minutes: delay),
frequency: _updateInterval,
existingWorkPolicy: workManager.ExistingWorkPolicy.keep,
backoffPolicy: workManager.BackoffPolicy.linear,
backoffPolicyDelay: interval,
backoffPolicyDelay: _updateInterval,
constraints: workManager.Constraints(
networkType: workManager.NetworkType.connected,
),
networkType: workManager.NetworkType.connected
)
);
}
}
}
_stopLocationService() async {
Logger.d("Canceling previous schedule if any...");
await workManager.Workmanager.cancelAll();
await workManager.Workmanager.cancelByTag(backgroundTaskTag);
}
updateDeviceLocation() async {
Logger.d("[Foreground location] Started");
Geolocator geolocator = Geolocator();
var battery = Battery();
String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) {
Logger.d("[Foreground location] Getting battery level...");
int batteryLevel = await battery.batteryLevel;
Logger.d("[Foreground location] Getting device location...");
Position position = await geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
locationPermissionLevel: GeolocationPermission.locationAlways
);
if (position != null) {
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
String url = "$httpWebHost/api/webhook/$webhookId";
Map data = {
if (ConnectionManager().webhookId != null &&
ConnectionManager().webhookId.isNotEmpty) {
String url = "${ConnectionManager()
.httpWebHost}/api/webhook/${ConnectionManager().webhookId}";
Map<String, String> headers = {};
Logger.d("[Location] Getting device location...");
Position location = await Geolocator().getCurrentPosition(
desiredAccuracy: LocationAccuracy.medium);
Logger.d("[Location] Got location: ${location.latitude} ${location
.longitude}. Sending home...");
int battery = await Battery().batteryLevel;
var data = {
"type": "update_location",
"data": {
"gps": [position.latitude, position.longitude],
"gps_accuracy": position.accuracy,
"battery": batteryLevel ?? 100
"gps": [location.latitude, location.longitude],
"gps_accuracy": location.accuracy,
"battery": battery
}
};
Logger.d("[Foreground location] Sending data home...");
var response = await http.post(
headers["Content-Type"] = "application/json";
await http.post(
url,
headers: {"Content-Type": "application/json"},
headers: headers,
body: json.encode(data)
);
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
} else {
Logger.d("[Foreground location] No location. Aborting.");
}
Logger.d("[Location] ...done.");
}
}
}
void updateDeviceLocationIsolate() {
workManager.Workmanager.executeTask((backgroundTask, data) async {
workManager.Workmanager.executeTask((backgroundTask, data) {
//print("[Background $backgroundTask] Started");
Geolocator geolocator = Geolocator();
var battery = Battery();
int batteryLevel = 100;
String webhookId = data["webhookId"];
String httpWebHost = data["httpWebHost"];
//String logData = '==> ${DateTime.now()} [Background $backgroundTask]:';
//print("[Background $backgroundTask] Getting path for log file...");
//final logFileDirectory = await getExternalStorageDirectory();
//print("[Background $backgroundTask] Opening log file...");
//File logFile = File('${logFileDirectory.path}/ha-client-background-log.txt');
//print("[Background $backgroundTask] Log file path: ${logFile.path}");
if (webhookId != null && webhookId.isNotEmpty) {
//print("[Background $backgroundTask] hour=$battery");
String url = "$httpWebHost/api/webhook/$webhookId";
Map<String, String> headers = {};
headers["Content-Type"] = "application/json";
@ -169,69 +135,42 @@ void updateDeviceLocationIsolate() {
"data": {
"gps": [],
"gps_accuracy": 0,
"battery": 100
"battery": batteryLevel
}
};
//print("[Background $backgroundTask] Getting battery level...");
int batteryLevel;
try {
batteryLevel = await battery.batteryLevel;
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
} catch(e) {
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
batteryLevel = 0;
//logData += 'Battery: error, $e';
}
if (batteryLevel != null) {
data["data"]["battery"] = batteryLevel;
//logData += 'Battery: success, $batteryLevel';
}/* else {
logData += 'Battery: error, level is null';
}*/
Position location;
try {
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
if (location != null && location.latitude != null) {
//logData += ' || Location: success, ${location.latitude} ${location.longitude} (${location.timestamp})';
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){
//print("[Background $backgroundTask] Getting device location...");
Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.medium).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;
try {
http.Response response = await http.post(
//print("[Background $backgroundTask] Sending data home...");
http.post(
url,
headers: headers,
body: json.encode(data)
);
/*if (response.statusCode >= 200 && response.statusCode < 300) {
logData += ' || Post: success, ${response.statusCode}';
} else {
logData += ' || Post: error, ${response.statusCode}';
}*/
} catch(e) {
//logData += ' || Post: error, $e';
}
}/* else {
logData += ' || Location: error, location is null';
}*/
} catch (e) {
//print("[Background $backgroundTask] Location error: $e");
//logData += ' || Location: error, $e';
}).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)
);
}
}/* else {
logData += 'Not configured';
}*/
//print("[Background $backgroundTask] Writing log data...");
/*try {
var fileMode;
if (logFile.existsSync() && logFile.lengthSync() < 5000000) {
fileMode = FileMode.append;
} else {
fileMode = FileMode.write;
});
});
});
}
await logFile.writeAsString('$logData\n', mode: fileMode);
} catch (e) {
print("[Background $backgroundTask] Error writing log: $e");
}
print("[Background $backgroundTask] Finished.");*/
return true;
return Future.value(true);
});
}

View File

@ -45,7 +45,7 @@ class MobileAppIntegrationManager {
positiveText: "Restart now",
negativeText: "Later",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart");
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
},
));
});
@ -81,7 +81,7 @@ class MobileAppIntegrationManager {
}
completer.complete();
}).catchError((e) {
if (e is http.Response && e.statusCode == 410) {
if (e['code'] != null && e['code'] == 410) {
Logger.e("MobileApp integration was removed");
_askToRegisterApp();
} else {

View File

@ -9,12 +9,12 @@ class StartupUserMessagesManager {
return _instance;
}
StartupUserMessagesManager._internal();
StartupUserMessagesManager._internal() {}
bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-888";
static final _whatsNewMessageKey = "user-message-shown-whats-new-706";
void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance();

View File

@ -1,272 +0,0 @@
part of '../main.dart';
enum AppTheme {darkTheme, defaultTheme, haTheme}
class HAClientTheme {
static const TextTheme textTheme = TextTheme(
display1: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
display2: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
headline: TextStyle(fontSize: 24, fontWeight: FontWeight.normal),
title: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
subhead: TextStyle(fontSize: 16, fontWeight: FontWeight.normal),
body1: TextStyle(fontSize: 15, fontWeight: FontWeight.normal),
body2: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
subtitle: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
caption: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
overline: TextStyle(
fontSize: 10,
fontWeight: FontWeight.normal,
letterSpacing: 1,
),
button: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
);
static const offEntityStates = [
EntityState.off,
EntityState.closed,
"below_horizon",
"default",
EntityState.idle,
EntityState.alarm_disarmed,
];
static const onEntityStates = [
EntityState.on,
"auto",
EntityState.active,
EntityState.playing,
EntityState.paused,
"above_horizon",
EntityState.home,
EntityState.open,
EntityState.cleaning,
EntityState.returning,
"cool",
EntityState.alarm_arming,
EntityState.alarm_disarming,
EntityState.alarm_pending,
];
static const disabledEntityStates = [
EntityState.unavailable,
EntityState.unknown,
];
static const alarmEntityStates = [
EntityState.alarm_armed_away,
EntityState.alarm_armed_custom_bypass,
EntityState.alarm_armed_home,
EntityState.alarm_armed_night,
EntityState.alarm_triggered,
"heat",
];
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
};
static final HAClientTheme _instance = HAClientTheme
._internal();
factory HAClientTheme() {
return _instance;
}
HAClientTheme._internal();
ThemeData getThemeData(AppTheme theme) {
switch (theme) {
case AppTheme.darkTheme:
return darkTheme;
break;
case AppTheme.defaultTheme:
return defaultTheme;
break;
case AppTheme.haTheme:
return homeAssistantTheme;
break;
default:
return defaultTheme;
}
}
final ThemeData defaultTheme = ThemeData.from(
colorScheme: ColorScheme(
primary: Color.fromRGBO(112, 154, 193, 1),
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
secondary: Color.fromRGBO(253, 216, 53, 1),
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
background: Color.fromRGBO(250, 250, 250, 1),
surface: Colors.white,
error: Colors.red,
onPrimary: Colors.white,
onSecondary: Colors.black87,
onBackground: Colors.black87,
onSurface: Colors.black87,
onError: Colors.white,
brightness: Brightness.light
),
textTheme: ThemeData.light().textTheme.copyWith(
display1: textTheme.display1.copyWith(color: Colors.black54),
display2: textTheme.display2.copyWith(color: Colors.redAccent),
headline: textTheme.headline.copyWith(color: Colors.black87),
title: textTheme.title.copyWith(color: Colors.black87),
subhead: textTheme.subhead.copyWith(color: Colors.black54),
body1: textTheme.body1.copyWith(color: Colors.black87),
body2: textTheme.body2.copyWith(color: Colors.black87),
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
caption: textTheme.caption.copyWith(color: Colors.black45),
overline: textTheme.overline.copyWith(color: Colors.black26),
button: textTheme.button.copyWith(color: Colors.white),
)
);
final ThemeData homeAssistantTheme = ThemeData.from(
colorScheme: ColorScheme(
primary: Color.fromRGBO(2, 165, 238, 1),
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
secondary: Color.fromRGBO(253, 216, 53, 1),
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
background: Color.fromRGBO(250, 250, 250, 1),
surface: Colors.white,
error: Colors.red,
onPrimary: Colors.white,
onSecondary: Colors.black87,
onBackground: Colors.black87,
onSurface: Colors.black87,
onError: Colors.white,
brightness: Brightness.light
),
textTheme: ThemeData.light().textTheme.copyWith(
display1: textTheme.display1.copyWith(color: Colors.black54),
display2: textTheme.display2.copyWith(color: Colors.redAccent),
headline: textTheme.headline.copyWith(color: Colors.black87),
title: textTheme.title.copyWith(color: Colors.black87),
subhead: textTheme.subhead.copyWith(color: Colors.black54),
body1: textTheme.body1.copyWith(color: Colors.black87),
body2: textTheme.body2.copyWith(color: Colors.black87),
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
caption: textTheme.caption.copyWith(color: Colors.black45),
overline: textTheme.overline.copyWith(color: Colors.black26),
button: textTheme.button.copyWith(color: Colors.white),
)
);
final ThemeData darkTheme = ThemeData.from(
colorScheme: ColorScheme(
primary: Color.fromRGBO(112, 154, 193, 1),
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
secondary: Color.fromRGBO(253, 216, 53, 1),
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
background: Color.fromRGBO(47, 49, 54, 1),
surface: Color.fromRGBO(54, 57, 63, 1),
error: Color.fromRGBO(183, 109, 109, 1),
onPrimary: Colors.white,
onSecondary: Colors.black87,
onBackground: Color.fromRGBO(220, 221, 222, 1),
onSurface: Colors.white,
onError: Colors.white,
brightness: Brightness.dark
),
textTheme: textTheme
);
Color getOnStateColor(BuildContext context) {
return Theme.of(context).colorScheme.secondary;
}
Color getOffStateColor(BuildContext context) {
return Theme.of(context).colorScheme.primaryVariant;
}
Color getDisabledStateColor(BuildContext context) {
return Theme.of(context).disabledColor;
}
Color getAlertStateColor(BuildContext context) {
return Theme.of(context).colorScheme.error;
}
Color getColorByEntityState(String state, BuildContext context) {
if (onEntityStates.contains(state)) {
return getOnStateColor(context);
} else if (disabledEntityStates.contains(state)) {
return getDisabledStateColor(context);
} else if (alarmEntityStates.contains(state)) {
return getAlertStateColor(context);
} else {
return getOffStateColor(context);
}
}
Color getGreenGaugeColor() {
return Colors.green;
}
Color getYellowGaugeColor() {
return Colors.yellow;
}
Color getRedGaugeColor() {
return Colors.red;
}
TextStyle getLinkTextStyle(BuildContext context) {
ThemeData theme = Theme.of(context);
return theme.textTheme.body1.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline
);
}
TextStyle getActionTextStyle(BuildContext context) {
ThemeData theme = Theme.of(context);
return theme.textTheme.subhead.copyWith(
color: Colors.blue
);
}
Color getBadgeColor(String entityDomain) {
return badgeColors[entityDomain] ??
badgeColors["default"];
}
Color getOnBadgeTextColor() {
return Colors.white;
}
charts.Color chartHistoryStateColor(String state, int id, BuildContext context) {
Color c = getColorByEntityState(state, context);
if (c != null) {
return charts.Color(
r: c.red,
g: c.green,
b: c.blue,
a: c.alpha
);
} else {
double r = id.toDouble() % 10;
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
}
}
Color historyStateColor(String state, int id, BuildContext context) {
Color c = getColorByEntityState(state, context);
if (c != null) {
return c;
} else {
if (id > -1) {
double r = id.toDouble() % 10;
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
} else {
return getOnStateColor(context);
}
}
}
}

View File

@ -1,18 +0,0 @@
part of '../main.dart';
class FullScreenPage extends StatelessWidget {
final Widget child;
const FullScreenPage({Key key, this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Center(
child: this.child,
),
);
}
}

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 > 1) {
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(domain: "homeassistant", service: "restart", entityId: 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(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);
},
));
}
_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

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../main.dart';
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
@ -9,10 +9,10 @@ class MainPage extends StatefulWidget {
_MainPageState createState() => new _MainPageState();
}
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
StreamSubscription<List<PurchaseDetails>> _subscription;
StreamSubscription _stateSubscription;
StreamSubscription _lovelaceSubscription;
StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription;
@ -25,11 +25,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
int _previousViewCount;
bool _showLoginButton = false;
bool _preventAppRefresh = false;
Entity _entityToShow;
String _savedSharedText;
String _entityToShow;
@override
void initState() {
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
_subscription = purchaseUpdates.listen((purchases) {
_handlePurchaseUpdates(purchases);
});
super.initState();
enableShareReceiving();
WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure(
@ -70,6 +77,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_fullLoad();
}
@override void receiveShare(Share shared) {
if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) {
_savedSharedText = shared.text;
}
}
Future onSelectNotification(String payload) async {
if (payload != null) {
Logger.d('Notification clicked: ' + payload);
@ -91,42 +104,43 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
);
}
void _fullLoad() {
void _fullLoad() async {
_showInfoBottomBar(progress: true,);
_subscribe().then((_) {
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
SharedPreferences.getInstance().then((prefs) {
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
_fetchData(useCache: true);
_fetchData();
LocationManager();
StartupUserMessagesManager().checkMessagesToShow();
});
}, onError: (e) {
_setErrorState(e);
});
});
}
void _quickLoad({bool uiOnly: false}) {
void _quickLoad() {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
_fetchData(useCache: false, uiOnly: uiOnly);
_fetchData();
//StartupUserMessagesManager().checkMessagesToShow();
}, onError: (e) {
_setErrorState(e);
});
}
_fetchData({useCache: false, uiOnly: false}) async {
if (useCache && !uiOnly) {
HomeAssistant().fetchDataFromCache().then((_) {
setState((){});
});
_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(uiOnly).then((_) {
await HomeAssistant().fetchData().then((_) {
_hideBottomBar();
if (_entityToShow != null) {
_entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
}).catchError((e) {
if (e is HAError) {
@ -143,32 +157,40 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
Logger.d("$state");
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
_quickLoad();
} else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
HomeAssistant().saveCache();
}
}
void _handlePurchaseUpdates(purchase) {
if (purchase is List<PurchaseDetails>) {
if (purchase[0].status == PurchaseStatus.purchased) {
eventBus.fire(ShowPopupMessageEvent(
title: "Thanks a lot!",
body: "Thank you for supporting HA Client development!",
buttonText: "Ok"
));
} else {
Logger.d("Purchase change handler: ${purchase[0].status}");
}
} else {
Logger.e("Something wrong with purchase handling. Got: $purchase");
}
}
Future _subscribe() {
Completer completer = Completer();
if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
Logger.d("Need to rebuild UI");
Logger.d("New entity. Need to rebuild UI");
_quickLoad();
} else {
setState(() {});
}
});
}
if (_lovelaceSubscription == null) {
_lovelaceSubscription = eventBus.on<LovelaceChangedEvent>().listen((event) {
_quickLoad();
});
}
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_quickLoad(uiOnly: true);
_quickLoad();
});
}
if (_showPopupDialogSubscription == null) {
@ -196,8 +218,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
if (_serviceCallSubscription == null) {
_serviceCallSubscription =
eventBus.on<NotifyServiceCallEvent>().listen((event) {
_notifyServiceCalled(event.domain, event.service, event.entityId);
eventBus.on<ServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId,
event.additionalParams);
});
}
@ -230,7 +253,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_showOAuth();
} else {
_preventAppRefresh = false;
Navigator.of(context).pop();
}
});
}
@ -244,7 +266,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
void _showOAuth() {
_preventAppRefresh = true;
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
Launcher.launchURLInCustomTab(
url: ConnectionManager().oauthUrl
);
}
_setErrorState(HAError e) {
@ -294,28 +318,27 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
);
}
void _notifyServiceCalled(String domain, String service, entityId) {
//TODO remove this shit.... maybe
void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar(
message: "Calling $domain.$service",
duration: Duration(seconds: 4)
duration: Duration(seconds: 3)
);
ConnectionManager().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
}
void _showEntityPage(String entityId) {
setState(() {
_entityToShow = HomeAssistant().entities?.get(entityId);
if (_entityToShow != null) {
_mainScrollController?.jumpTo(0);
}
_entityToShow = entityId;
});
/*if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
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) {
@ -345,12 +368,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
menuItems.add(
UserAccountsDrawerHeader(
accountName: Text(HomeAssistant().userName),
accountEmail: Text(HomeAssistant().locationName ?? ""),
accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"),
onDetailsPressed: () {
Launcher.launchURLInCustomTab(
url: "${ConnectionManager().httpWebHost}/profile?external_auth=1"
);
},
currentAccountPicture: CircleAvatar(
backgroundColor: Theme.of(context).backgroundColor,
child: Text(
HomeAssistant().userAvatarText,
style: Theme.of(context).textTheme.display1
style: TextStyle(
fontSize: 32.0
),
),
),
)
@ -359,7 +388,21 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
HomeAssistant().panels.forEach((Panel panel) {
if (!panel.isHidden) {
menuItems.add(
panel.getMenuItemWidget(context)
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("${panel.title}"),
Container(width: 4.0,),
panel.isWebView ? Text("WEB", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
],
),
onTap: () {
Navigator.of(context).pop();
panel.handleOpen(context);
}
)
);
}
});
@ -367,11 +410,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
menuItems.addAll([
Divider(),
ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:cellphone-settings-variant")),
title: Text("App settings"),
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:server-network")),
title: Text("Connection settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/app-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');
},
)
]);
@ -408,36 +459,29 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
title: Text("Help"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.app/docs");
Launcher.launchURL("http://ha-client.homemade.systems/docs");
},
),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
title: Text("Contacts/Discussion"),
title: Text("Join Discord channel"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("https://discord.gg/nd6FZQ");
Launcher.launchURL("https://discord.gg/AUzEvwn");
},
),
new ListTile(
title: Text("What's new?"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/whats-new');
}
),
new AboutListTile(
aboutBoxChildren: <Widget>[
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.app/");
Launcher.launchURL("http://ha-client.homemade.systems/");
},
child: Text(
"ha-client.app",
style: Theme.of(context).textTheme.body1.copyWith(
"ha-client.homemade.systems",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
decoration: TextDecoration.underline
),
),
),
@ -447,13 +491,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/terms_and_conditions");
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
},
child: Text(
"Terms and Conditions",
style: Theme.of(context).textTheme.body1.copyWith(
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
decoration: TextDecoration.underline
),
),
),
@ -463,13 +507,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/privacy_policy");
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
},
child: Text(
"Privacy Policy",
style: Theme.of(context).textTheme.body1.copyWith(
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
decoration: TextDecoration.underline
),
),
)
@ -496,13 +540,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
bool _showBottomBar = false;
String _bottomBarText;
bool _bottomBarProgress;
bool _bottomBarErrorColor;
Color _bottomBarColor;
Timer _bottomBarTimer;
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
_bottomBarTimer?.cancel();
_bottomBarAction = Container(height: 0.0, width: 0.0,);
_bottomBarErrorColor = false;
_bottomBarColor = Colors.grey.shade50;
setState(() {
_bottomBarText = message;
_bottomBarProgress = progress;
@ -516,10 +560,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
void _showErrorBottomBar(HAError error) {
TextStyle textStyle = Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline
TextStyle textStyle = TextStyle(
color: Colors.blue,
fontSize: Sizes.nameFontSize
);
_bottomBarErrorColor = true;
_bottomBarColor = Colors.red.shade100;
List<Widget> actions = [];
error.actions.forEach((HAErrorAction action) {
switch (action.type) {
@ -591,19 +636,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final ScrollController _mainScrollController = ScrollController();
Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> serviceMenuItems = [];
List<PopupMenuItem<String>> mediaMenuItems = [];
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
serviceMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
@ -619,15 +656,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
Widget mediaMenuIcon;
int playersCount = 0;
if (!empty && !HomeAssistant().entities.isEmpty) {
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(domains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
playersCount = activePlayers.length;
mediaMenuItems.addAll(
activePlayers.map((entity) => PopupMenuItem<String>(
child: Text(
"${entity.displayName}",
style: Theme.of(context).textTheme.body1.copyWith(
color: HAClientTheme().getColorByEntityState(entity.state, context)
)
style: TextStyle(
color: EntityColor.stateColor(entity.state)
),
),
value: "${entity.entityId}",
)).toList()
@ -656,12 +693,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
shape: BoxShape.circle,
),
child: Center(
child: Text(
"$playersCount",
style: Theme.of(context).textTheme.caption.copyWith(
color: Colors.white
)
),
child: Text("$playersCount", style: TextStyle(fontSize: 12)),
),
),
)
@ -679,7 +711,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlatButton(
child: Text("Login with Home Assistant", style: Theme.of(context).textTheme.button),
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
color: Colors.blue,
onPressed: () => _fullLoad(),
)
@ -698,11 +730,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
} else {
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
Entity entity = HomeAssistant().entities.get(_entityToShow);
mainScrollBody = Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
child: HomeAssistant().ui.build(context, _viewsTabController),
child: HomeAssistant().buildViews(context, _viewsTabController),
),
Container(
width: Sizes.mainPageScreenSeparatorWidth,
@ -710,14 +743,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
),
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth),
child: EntityPageLayout(entity: _entityToShow, showClose: true,),
child: EntityPageLayout(entity: entity, showClose: true,),
)
],
);
} else if (_entityToShow != null) {
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
} else {
mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
_entityToShow = null;
mainScrollBody = HomeAssistant().buildViews(context, _viewsTabController);
}
}
@ -755,9 +787,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
context: context,
items: serviceMenuItems
).then((String val) {
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
@ -774,7 +804,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_scaffoldKey.currentState.openDrawer();
},
),
bottom: (empty || _entityToShow != null) ? null : TabBar(
bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
isScrollable: true,
@ -783,8 +813,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
];
},
body: mainScrollBody,
controller: _mainScrollController,
body: mainScrollBody
);
}
@ -814,16 +843,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
bottomBarChildren.add(
CollectionScaleTransition(
children: <Widget>[
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOnStateColor(context),),
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getDisabledStateColor(context),),
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOffStateColor(context),),
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
],
),
);
}
if (bottomBarChildren.isNotEmpty) {
bottomBar = Container(
color: _bottomBarErrorColor ? Theme.of(context).errorColor : Theme.of(context).primaryColorLight,
color: _bottomBarColor,
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
@ -840,6 +869,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
);
}
}
// This method is rerun every time setState is called.
if (HomeAssistant().isNoViews) {
return Scaffold(
key: _scaffoldKey,
@ -849,22 +879,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
body: _buildScaffoldBody(true)
);
} else {
return WillPopScope(
child: Scaffold(
return Scaffold(
key: _scaffoldKey,
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
body: _buildScaffoldBody(false)
),
onWillPop: () {
if (_entityToShow != null) {
eventBus.fire(ShowEntityPageEvent());
return Future.value(false);
} else {
return Future.value(true);
}
},
);
}
}
@ -872,11 +892,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
//flutterWebviewPlugin.dispose();
_viewsTabController?.dispose();
_stateSubscription?.cancel();
_lovelaceSubscription?.cancel();
_settingsSubscription?.cancel();
_serviceCallSubscription?.cancel();
_showPopupDialogSubscription?.cancel();
@ -884,6 +901,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_showEntityPageSubscription?.cancel();
_showErrorSubscription?.cancel();
_startAuthSubscription?.cancel();
_subscription?.cancel();
_showPageSubscription?.cancel();
_reloadUISubscription?.cancel();
//TODO disconnect

View File

@ -57,9 +57,9 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
_loaded = false;
});
} else {
_isMediaExtractorExist = HomeAssistant().isServiceExist("media_extractor");
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
//_useMediaExtractor = _isMediaExtractorExist;
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
_players = HomeAssistant().entities.getByDomains(domains: ["media_player"]);
setState(() {
if (_players.isNotEmpty) {
_loaded = true;
@ -90,20 +90,16 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
Navigator.pop(context);
ConnectionManager().callService(
domain: serviceDomain,
service: "play_media",
entityId: entity.entityId,
data: {
service: "play_media",
additionalServiceData: {
"media_content_id": _mediaUrl,
"media_content_type": _contentType
}
);
HomeAssistant().sendToPlayerId = entity.entityId;
if (HomeAssistant().sendFromPlayerId != null && HomeAssistant().sendFromPlayerId != HomeAssistant().sendToPlayerId) {
ConnectionManager().callService(
domain: HomeAssistant().sendFromPlayerId.split(".")[0],
service: "turn_off",
entityId: HomeAssistant().sendFromPlayerId
);
eventBus.fire(ServiceCallEvent(HomeAssistant().sendFromPlayerId.split(".")[0], "turn_off", HomeAssistant().sendFromPlayerId, null));
HomeAssistant().sendFromPlayerId = null;
}
eventBus.fire(ShowEntityPageEvent(entity: entity));
@ -135,9 +131,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
if (_validationMessage.isNotEmpty) {
children.add(Text(
"$_validationMessage",
style: Theme.of(context).textTheme.body1.copyWith(
color: Theme.of(context).errorColor
)
style: TextStyle(color: Colors.red)
));
}
children.addAll(<Widget>[
@ -195,7 +189,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
},
child: Text(
"How?",
style: Theme.of(context).textTheme.body1.copyWith(
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),

View File

@ -0,0 +1,228 @@
part of '../main.dart';
class ConnectionSettingsPage extends StatefulWidget {
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
final String title;
@override
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
}
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = "";
String _newHassioDomain = "";
String _hassioPort = "";
String _newHassioPort = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _longLivedToken = "";
String _newLongLivedToken = "";
bool _useLovelace = true;
bool _newUseLovelace = true;
String oauthUrl;
bool useOAuth = false;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
try {
useOAuth = prefs.getBool("oauth-used") ?? true;
} catch (e) {
useOAuth = true;
}
if (!useOAuth) {
try {
_longLivedToken = _newLongLivedToken =
await storage.read(key: "hacl_llt");
} catch (e) {
_longLivedToken = _newLongLivedToken = "";
await storage.delete(key: "hacl_llt");
}
}
setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
try {
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
} catch (e) {
_useLovelace = _newUseLovelace = true;
}
});
}
bool _checkConfigChanged() {
return (
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newUseLovelace != _useLovelace) ||
(_newLongLivedToken != _longLivedToken));
}
_saveSettings() async {
_newHassioDomain = _newHassioDomain.trim();
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
_newHassioDomain = _newHassioDomain.split("//")[1];
}
_newHassioDomain = _newHassioDomain.split("/")[0];
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
if (_newLongLivedToken.isNotEmpty) {
_newLongLivedToken = _newLongLivedToken.trim();
prefs.setBool("oauth-used", false);
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
} else if (!useOAuth) {
await storage.delete(key: "hacl_llt");
}
prefs.setString("hassio-domain", _newHassioDomain);
if (_newHassioPort == null || _newHassioPort.isEmpty) {
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
} else {
_newHassioPort = _newHassioPort.trim();
}
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setBool("use-lovelace", _newUseLovelace);
}
@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),
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: (){
if (_checkConfigChanged()) {
Logger.d("Settings changed. Saving...");
_saveSettings().then((r) {
Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true));
});
} else {
Logger.d("Settings was not changed");
Navigator.pop(context);
}
}
)
],
),
body: ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text(
"Connection settings",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
new Row(
children: [
Text("Use ssl (HTTPS)"),
Switch(
value: (_newSocketProtocol == "wss"),
onChanged: (value) {
setState(() {
_newSocketProtocol = value ? "wss" : "ws";
});
},
)
],
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
onChanged: (value) {
_newHassioDomain = value;
}
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant port (default is 8123)"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
onChanged: (value) {
_newHassioPort = value;
}
),
new Text(
"Try ports 80 and 443 if default is not working and you don't know why.",
style: TextStyle(color: Colors.grey),
),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Text(
"UI",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
),
new Row(
children: [
Text("Use Lovelace UI"),
Switch(
value: _newUseLovelace,
onChanged: (value) {
setState(() {
_newUseLovelace = value;
});
},
)
],
),
Text(
"Authentication settings",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
Container(height: 10.0,),
Text(
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
style: TextStyle(color: Colors.redAccent),
),
new TextField(
decoration: InputDecoration(
labelText: "Long-lived token"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
onChanged: (value) {
_newLongLivedToken = value;
}
),
],
),
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -1,104 +0,0 @@
part of '../../main.dart';
enum AppSettingsSection {menu, connectionSettings, integrationSettings, lookAndFeel}
class AppSettingsPage extends StatefulWidget {
final AppSettingsSection showSection;
AppSettingsPage({Key key, this.showSection: AppSettingsSection.menu}) : super(key: key);
@override
_AppSettingsPageState createState() => new _AppSettingsPageState();
}
class _AppSettingsPageState extends State<AppSettingsPage> {
var _currentSection;
@override
void initState() {
super.initState();
_currentSection = widget.showSection;
}
Widget _buildMenuItem(BuildContext context, IconData icon,String title, AppSettingsSection section) {
return ListTile(
title: Text(title, style: Theme.of(context).textTheme.subhead),
leading: Icon(icon),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
setState(() {
_currentSection = section;
});
},
);
}
Widget _buildMenu(BuildContext context) {
return ListView(
children: <Widget>[
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
],
);
}
@override
Widget build(BuildContext context) {
Widget section;
String title;
switch (_currentSection) {
case AppSettingsSection.menu: {
section = _buildMenu(context);
title = 'App settings';
break;
}
case AppSettingsSection.connectionSettings: {
section = ConnectionSettingsPage();
title = 'App settings - Connection';
break;
}
case AppSettingsSection.integrationSettings: {
section = IntegrationSettingsPage();
title = 'App settings - Integration';
break;
}
case AppSettingsSection.lookAndFeel: {
section = LookAndFeelSettingsPage();
title = 'App settings - Look&Feel';
break;
}
default:
title = ':(';
section = PageLoadingIndicator();
}
return WillPopScope(
child: Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
if (_currentSection == AppSettingsSection.menu) {
Navigator.pop(context);
} else {
setState(() {
_currentSection = AppSettingsSection.menu;
});
}
}),
title: Text(title),
),
body: section
),
onWillPop: () {
if (_currentSection == AppSettingsSection.menu) {
return Future.value(true);
} else {
setState(() {
_currentSection = AppSettingsSection.menu;
});
return Future.value(false);
}
},
);
}
}

View File

@ -1,192 +0,0 @@
part of '../../main.dart';
class ConnectionSettingsPage extends StatefulWidget {
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
final String title;
@override
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
}
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = "";
String _newHassioDomain = "";
String _hassioPort = "";
String _newHassioPort = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _longLivedToken = "";
String _newLongLivedToken = "";
String oauthUrl;
bool useOAuth = false;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
try {
useOAuth = prefs.getBool("oauth-used") ?? true;
} catch (e) {
useOAuth = true;
}
if (!useOAuth) {
try {
_longLivedToken = _newLongLivedToken =
await storage.read(key: "hacl_llt");
} catch (e) {
_longLivedToken = _newLongLivedToken = "";
await storage.delete(key: "hacl_llt");
}
}
setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
});
}
bool _checkConfigChanged() {
return (
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newLongLivedToken != _longLivedToken));
}
_saveSettings() async {
_newHassioDomain = _newHassioDomain.trim();
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
_newHassioDomain = _newHassioDomain.split("//")[1];
}
_newHassioDomain = _newHassioDomain.split("/")[0];
if (_newHassioDomain.contains(":")) {
List<String> domainAndPort = _newHassioDomain.split(":");
_newHassioDomain = domainAndPort[0];
_newHassioPort = domainAndPort[1];
}
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
if (_newLongLivedToken.isNotEmpty) {
_newLongLivedToken = _newLongLivedToken.trim();
prefs.setBool("oauth-used", false);
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
} else if (!useOAuth) {
await storage.delete(key: "hacl_llt");
}
prefs.setString("hassio-domain", _newHassioDomain);
if (_newHassioPort == null || _newHassioPort.isEmpty) {
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
} else {
_newHassioPort = _newHassioPort.trim();
}
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
}
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text(
"Connection settings",
style: Theme.of(context).textTheme.headline,
),
new Row(
children: [
Text("Use ssl (HTTPS)"),
Switch(
value: (_newSocketProtocol == "wss"),
onChanged: (value) {
setState(() {
_newSocketProtocol = value ? "wss" : "ws";
});
},
)
],
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
onChanged: (value) {
_newHassioDomain = value;
}
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant port (default is 8123)"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
onChanged: (value) {
_newHassioPort = value;
}
),
new Text(
"Try ports 80 and 443 if default is not working and you don't know why.",
style: Theme.of(context).textTheme.caption,
),
Text(
"Authentication settings",
style: Theme.of(context).textTheme.headline,
),
Container(height: 10.0,),
Text(
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
style: Theme.of(context).textTheme.body1.copyWith(
color: Colors.redAccent
),
),
new TextField(
decoration: InputDecoration(
labelText: "Long-lived token"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
onChanged: (value) {
_newLongLivedToken = value;
}
),
Container(
height: Sizes.rowPadding,
),
RaisedButton(
child: Text('Apply', style: Theme.of(context).textTheme.button),
color: Theme.of(context).primaryColorDark,
onPressed: () {
if (_checkConfigChanged()) {
Logger.d("Settings changed. Saving...");
_saveSettings().then((r) {
Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true));
});
} else {
Logger.d("Settings was not changed");
Navigator.pop(context);
}
},
)
],
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -1,194 +0,0 @@
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;
if (_locationInterval % 5 != 0) {
_locationInterval = 5 * (_locationInterval ~/ 5);
}
});
});
}
void incLocationInterval() {
if (_locationInterval < 720) {
setState(() {
_locationInterval = _locationInterval + 5;
});
}
}
void decLocationInterval() {
if (_locationInterval > 5) {
setState(() {
_locationInterval = _locationInterval - 5;
});
}
}
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");
},
));
}
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(domain: "homeassistant", service: "stop");
},
));
}
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 ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text("Location tracking", style: Theme.of(context).textTheme.title),
Container(height: Sizes.rowPadding,),
InkWell(
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
child: Text(
"Please read documentation!",
style: Theme.of(context).textTheme.subhead.copyWith(
color: Colors.blue,
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: Theme.of(context).textTheme.title),
onPressed: () => decLocationInterval(),
),
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("+", style: Theme.of(context).textTheme.title),
onPressed: () => incLocationInterval(),
),
],
),
Divider(),
Text("Integration status", style: Theme.of(context).textTheme.title),
Container(height: Sizes.rowPadding,),
Text(
"${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}",
style: Theme.of(context).textTheme.subtitle,
),
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: Theme.of(context).textTheme.button)
),
Container(width: 10.0,),
RaisedButton(
color: Colors.redAccent,
onPressed: () => resetRegistration(),
child: Text("Reset integration", style: Theme.of(context).textTheme.button)
)
],
),
]
);
}
@override
void dispose() {
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
super.dispose();
}
}

View File

@ -1,79 +0,0 @@
part of '../../main.dart';
class LookAndFeelSettingsPage extends StatefulWidget {
LookAndFeelSettingsPage({Key key, this.title}) : super(key: key);
final String title;
@override
_LookAndFeelSettingsPageState createState() => new _LookAndFeelSettingsPageState();
}
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
AppTheme _currentTheme;
bool _changed = false;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
SharedPreferences.getInstance().then((prefs) {
setState(() {
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme];
});
});
}
_saveSettings(AppTheme theme) {
SharedPreferences.getInstance().then((prefs) {
prefs.setInt('app-theme', theme.index);
setState(() {
_currentTheme = theme;
eventBus.fire(ChangeThemeEvent(_currentTheme));
});
});
}
Map appThemeName = {
AppTheme.defaultTheme: 'Default',
AppTheme.haTheme: 'Home Assistant theme',
AppTheme.darkTheme: 'Dark theme'
};
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text("Application theme:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton<AppTheme>(
value: _currentTheme,
iconSize: 30.0,
isExpanded: true,
style: Theme.of(context).textTheme.title,
//hint: Text("Select ${caption.toLowerCase()}"),
items: AppTheme.values.map((value) {
return new DropdownMenuItem<AppTheme>(
value: value,
child: Text('${appThemeName[value]}'),
);
}).toList(),
onChanged: (theme) => _saveSettings(theme),
)
]
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
error = "";
});
http.Response response;
response = await http.get("http://ha-client.app/service/whats_new_0.8.3.md");
response = await http.get("http://ha-client.homemade.systems/service/whats_new_$appVersionNumber.md");
if (response.statusCode == 200) {
setState(() {
data = response.body;

View File

@ -10,7 +10,8 @@ class LastUpdatedWidget extends StatelessWidget {
child: Text(
'${entityModel.entityWrapper.entity.lastUpdated}',
textAlign: TextAlign.left,
style: Theme.of(context).textTheme.caption
style: TextStyle(
fontSize: Sizes.smallFontSize, color: Colors.black26),
),
);
}

View File

@ -23,7 +23,7 @@ class PageLoadingError extends StatelessWidget {
size: 48.0
)
),
Text(this.errorText, style: Theme.of(context).textTheme.subtitle)
Text(this.errorText, style: TextStyle(color: Colors.black45))
],
)
],

View File

@ -14,7 +14,7 @@ class PageLoadingIndicator extends StatelessWidget {
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
child: CircularProgressIndicator()
),
Text("Loading...", style: Theme.of(context).textTheme.subtitle)
Text("Loading...", style: TextStyle(color: Colors.black45))
],
)
],

View File

@ -40,7 +40,10 @@ class ProductPurchase extends StatelessWidget {
children: <Widget>[
Text(
"${product.title}",
style: Theme.of(context).textTheme.body2,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0
),
),
Container(height: Sizes.rowPadding,),
Text(
@ -50,9 +53,7 @@ class ProductPurchase extends StatelessWidget {
softWrap: true,
),
Container(height: Sizes.rowPadding,),
Text("${product.price} $period", style: Theme.of(context).textTheme.body1.copyWith(
color: priceColor
)),
Text("${product.price} $period", style: TextStyle(color: priceColor)),
],
)
),
@ -60,7 +61,7 @@ class ProductPurchase extends StatelessWidget {
Expanded(
flex: 2,
child: RaisedButton(
child: Text(this.purchased ? buttonTextInactive : buttonText, style: Theme.of(context).textTheme.button),
child: Text(this.purchased ? buttonTextInactive : buttonText, style: TextStyle(color: Colors.white)),
color: Colors.blue,
onPressed: this.purchased ? null : () => this.onBuy(this.product),
),

View File

@ -1,90 +0,0 @@
part of '../main.dart';
class ZhaPage extends StatefulWidget {
ZhaPage({Key key}) : super(key: key);
@override
_ZhaPageState createState() => new _ZhaPageState();
}
class _ZhaPageState extends State<ZhaPage> {
List data = [];
String error = "";
@override
void initState() {
super.initState();
_loadData();
}
_loadData() async {
setState(() {
data.clear();
error = "";
});
ConnectionManager().sendSocketMessage(
type: 'zha_map/devices'
).then((response){
setState(() {
data = response['devices'];
});
}).catchError((e){
setState(() {
error = '$e';
});
});
}
@override
Widget build(BuildContext context) {
Widget body;
if (error.isNotEmpty) {
body = PageLoadingError(errorText: error,);
} else if (data.isEmpty) {
body = PageLoadingIndicator();
} else {
List<Widget> devicesListWindet = [];
data.forEach((device) {
devicesListWindet.add(
Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardHeader(
name: '${device['ieee']}',
subtitle: Text('${device['manufacturer']}'),
),
Text('${device['device_type']}'),
Text('model: ${device['model']}'),
Text('offline: ${device['offline']}'),
Text('neighbours: ${device['neighbours'].length}'),
Text('raw: $device'),
],
),
)
);
});
body = ListView(
children: devicesListWindet
);
}
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
actions: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => _loadData(),
)
],
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text('ZHA'),
),
body: body
);
}
}

View File

@ -11,7 +11,7 @@ class Panel {
};
final String id;
final String componentName;
final String type;
final String title;
final String urlPath;
final Map config;
@ -19,61 +19,34 @@ class Panel {
bool isHidden = true;
bool isWebView = false;
Panel({this.id, this.componentName, this.title, this.urlPath, this.icon, this.config}) {
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
if (icon == null || !icon.startsWith("mdi:")) {
icon = Panel.iconsByComponent[componentName];
icon = Panel.iconsByComponent[type];
}
isHidden = (componentName == 'kiosk' || componentName == 'states' || componentName == 'profile' || componentName == 'developer-tools');
isWebView = (componentName != 'config' && componentName != 'lovelace' && !componentName.startsWith('haclient'));
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
isWebView = (type != 'config');
}
void handleOpen(BuildContext context) {
if (componentName == "config") {
if (type == "config") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this),
)
);
} else if (componentName.startsWith('haclient')) {
Navigator.of(context).pushNamed(urlPath);
} else if (componentName == 'lovelace') {
HomeAssistant().lovelaceDashboardUrl = this.urlPath;
HomeAssistant().autoUi = false;
SharedPreferences.getInstance().then((prefs) {
prefs.setString('lovelace_dashboard_url', this.urlPath);
eventBus.fire(ReloadUIEvent());
});
} else {
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
Launcher.launchURLInCustomTab(url: "${ConnectionManager().httpWebHost}/$urlPath");
}
}
Widget getMenuItemWidget(BuildContext context) {
return ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(this.icon)),
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("${this.title}"),
Container(width: 4.0,),
isWebView ? Text("webview", style: Theme.of(context).textTheme.overline) : Container(width: 1.0,)
],
),
onTap: () {
Navigator.of(context).pop();
this.handleOpen(context);
}
);
}
Widget getWidget() {
switch (componentName) {
switch (type) {
case "config": {
return ConfigPanelWidget();
}
default: {
return Text("Unsupported panel component: $componentName");
return Text("Unsupported panel component: $type");
}
}
}

View File

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

View File

@ -156,8 +156,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
result.add(
new charts.Series<EntityHistoryMoment, DateTime>(
id: "value",
colorFn: (EntityHistoryMoment historyMoment, __) =>
HAClientTheme().chartHistoryStateColor("_", historyMoment.colorId, context),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor("_", historyMoment.colorId),
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
if (historyMoment.hiddenDot) {
return 0.0;
@ -180,8 +179,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'state',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0,
colorFn: (EntityHistoryMoment historyMoment, __) =>
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),

View File

@ -28,7 +28,7 @@ class HistoryControlWidget extends StatelessWidget {
Expanded(
child: Padding(
padding: EdgeInsets.only(right: 10.0),
child: _buildStates(context),
child: _buildStates(),
),
),
_buildTime(),
@ -46,16 +46,18 @@ class HistoryControlWidget extends StatelessWidget {
}
}
Widget _buildStates(BuildContext context) {
Widget _buildStates() {
List<Widget> children = [];
for (int i = 0; i < selectedStates.length; i++) {
children.add(
Text(
"${selectedStates[i] ?? '-'}",
textAlign: TextAlign.right,
style: Theme.of(context).textTheme.title.copyWith(
color: HAClientTheme().historyStateColor(selectedStates[i], colorIndexes[i], context)
)
style: TextStyle(
fontWeight: FontWeight.bold,
color: EntityColor.historyStateColor(selectedStates[i], colorIndexes[i]),
fontSize: 22.0
),
)
);
}

View File

@ -108,8 +108,7 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
return [
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
colorFn: (EntityHistoryMoment historyMoment, __) =>
HAClientTheme().chartHistoryStateColor(EntityState.on, -1, context),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(EntityState.on, -1),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
data: data,

View File

@ -107,8 +107,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) =>
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
@ -116,8 +115,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) =>
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
@ -125,8 +123,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) =>
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,

View File

@ -12,8 +12,6 @@ class StateChangedEvent {
});
}
class LovelaceChangedEvent {}
class SettingsChangedEvent {
bool reconnect;
@ -28,13 +26,6 @@ class ReloadUIEvent {
ReloadUIEvent();
}
class ChangeThemeEvent {
final AppTheme theme;
ChangeThemeEvent(this.theme);
}
class StartAuthEvent {
String oauthUrl;
bool showButton;
@ -42,12 +33,13 @@ class StartAuthEvent {
StartAuthEvent(this.oauthUrl, this.showButton);
}
class NotifyServiceCallEvent {
class ServiceCallEvent {
String domain;
String service;
var entityId;
String entityId;
Map<String, dynamic> additionalParams;
NotifyServiceCallEvent(this.domain, this.service, this.entityId);
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
}
class ShowPopupDialogEvent {

View File

@ -6,51 +6,8 @@ class HomeAssistantUI {
bool get isEmpty => views == null || views.isEmpty;
HomeAssistantUI({rawLovelaceConfig}) {
if (rawLovelaceConfig == null) {
rawLovelaceConfig = _generateLovelaceConfig();
}
HomeAssistantUI() {
views = [];
Logger.d("--Title: ${rawLovelaceConfig["title"]}");
title = rawLovelaceConfig["title"];
int viewCounter = 0;
Logger.d("--Views count: ${rawLovelaceConfig['views'].length}");
rawLovelaceConfig["views"].forEach((rawView){
Logger.d("----view id: ${rawView['id']}");
HAView view = HAView(
count: viewCounter,
rawData: rawView
);
views.add(
view
);
viewCounter += 1;
});
}
Map _generateLovelaceConfig() {
Map result = {};
result['title'] = 'Home';
result['views'] = [
{
'icon': 'mdi:home',
'badges': HomeAssistant().entities.getByDomains(
includeDomains: ['sensor', 'binary_sensor', 'device_tracker', 'person', 'sun']
).map(
(en) => en.entityId
).toList(),
'cards': [{
'type': 'entities',
'entities': HomeAssistant().entities.getByDomains(
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
).map(
(en) => en.entityId
).toList()
}]
}
];
return result;
}
Widget build(BuildContext context, TabController tabController) {

View File

@ -35,28 +35,4 @@ class Launcher {
}
}
static void launchAuthenticatedWebView({BuildContext context, String url, String title}) {
if (url.contains("?")) {
url += "&external_auth=1";
} else {
url += "?external_auth=1";
}
final flutterWebViewPlugin = new standaloneWebview.FlutterWebviewPlugin();
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
if (viewState.type == standaloneWebview.WebViewState.startLoad) {
Logger.d("[WebView] Injecting external auth JS");
rootBundle.loadString('assets/js/externalAuth.js').then((js){
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
});
}
});
Navigator.of(context).pushNamed(
"/webview",
arguments: {
"url": "$url",
"title": "${title ?? ''}"
}
);
}
}

View File

@ -23,10 +23,6 @@ class Logger {
return inDebugMode;
}
static void p(data) {
print(data);
}
static void e(String message) {
_writeToLog("Error", message);
}

View File

@ -4,171 +4,77 @@ class HAView {
List<HACard> cards = [];
List<Entity> badges = [];
Entity linkedEntity;
String name;
String id;
String iconName;
final String name;
final String id;
final String iconName;
final int count;
bool isPanel;
final bool panel;
HAView({@required this.count, @required rawData}) {
id = "${rawData['id']}";
name = rawData['title'];
iconName = rawData['icon'];
isPanel = rawData['panel'] ?? false;
if (rawData['badges'] != null && rawData['badges'] is List) {
rawData['badges'].forEach((entity) {
if (entity is String) {
if (HomeAssistant().entities.isExist(entity)) {
Entity e = HomeAssistant().entities.get(entity);
badges.add(e);
HAView({
this.name,
this.id,
this.count,
this.iconName,
this.panel: false,
List<Entity> childEntities
}) {
if (childEntities != null) {
_fillView(childEntities);
}
} else {
String eId = '${entity['entity']}';
if (HomeAssistant().entities.isExist(eId)) {
Entity e = HomeAssistant().entities.get(eId);
badges.add(e);
}
}
});
}
cards.addAll(_createLovelaceCards(rawData["cards"] ?? []));
}
List<HACard> _createLovelaceCards(List rawCards) {
List<HACard> result = [];
rawCards.forEach((rawCard){
try {
//bool isThereCardOptionsInside = rawCard["card"] != null;
var rawCardInfo = rawCard["card"] ?? rawCard;
void _fillView(List<Entity> childEntities) {
List<HACard> autoGeneratedCards = [];
badges.addAll(childEntities.where((entity){ return entity.isBadge;}));
childEntities.where((entity){ return entity.domain == "media_player";}).forEach((e){
HACard card = HACard(
id: "card",
name: rawCardInfo["title"] ?? rawCardInfo["name"],
type: rawCardInfo['type'] ?? CardType.ENTITIES,
columnsCount: rawCardInfo['columns'] ?? 4,
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
showHeaderToggle: (rawCardInfo['show_header_toggle'] ?? rawCard['show_header_toggle']) ?? true,
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],
states: rawCardInfo['states'],
conditions: rawCard['conditions'] ?? [],
content: rawCardInfo['content'],
min: rawCardInfo['min'] ?? 0,
max: rawCardInfo['max'] ?? 100,
unit: rawCardInfo['unit'],
severity: rawCardInfo['severity']
name: e.displayName,
id: e.entityId,
linkedEntityWrapper: EntityWrapper(entity: e),
type: CardType.MEDIA_CONTROL
);
if (rawCardInfo["cards"] != null) {
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
}
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
rawEntities?.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
cards.add(card);
});
childEntities.where((e){return (!e.isBadge && e.domain != "media_player");}).forEach((entity) {
if (!entity.isGroup) {
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) {
HACard card = HACard(
id: groupIdToAdd,
name: entity.domain,
type: CardType.ENTITIES
);
card.entities.add(EntityWrapper(entity: entity));
autoGeneratedCards.add(card);
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(EntityWrapper(entity: entity));
}
} else {
if (rawEntity["type"] == "divider") {
card.entities.add(EntityWrapper(entity: Entity.divider()));
} else if (rawEntity["type"] == "section") {
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
} else if (rawEntity["type"] == "call-service") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.callService,
"service": rawEntity["service"],
"service_data": rawEntity["service_data"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
HACard card = HACard(
name: entity.displayName,
id: entity.entityId,
linkedEntityWrapper: EntityWrapper(entity: entity),
type: CardType.ENTITIES
);
} else if (rawEntity["type"] == "weblink") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.navigate,
"service": rawEntity["url"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
url: rawEntity["url"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
HACard mediaCard = HACard(
name: entity.displayName,
id: entity.entityId,
linkedEntityWrapper: EntityWrapper(entity: entity),
type: CardType.MEDIA_CONTROL
);
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
card.entities.add(
EntityWrapper(
entity: e,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
cards.add(mediaCard);
});
cards.add(card);
}
});
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
if (rawSingleEntity != null) {
var en = rawSingleEntity;
if (en is String) {
if (HomeAssistant().entities.isExist(en)) {
Entity e = HomeAssistant().entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
overrideIcon: rawCardInfo["icon"],
overrideName: rawCardInfo["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
}
} else {
if (HomeAssistant().entities.isExist(en["entity"])) {
Entity e = HomeAssistant().entities.get(en["entity"]);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
overrideIcon: en["icon"],
overrideName: en["name"],
stateFilter: en['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
}
}
}
result.add(card);
} catch (e) {
Logger.e("There was an error parsing card: ${e.toString()}");
}
});
return result;
cards.addAll(autoGeneratedCards);
}
Widget buildTab() {
if (linkedEntity == null) {
if (iconName != null && iconName.isNotEmpty) {
if (iconName != null) {
return
Tab(
icon:

View File

@ -10,28 +10,22 @@ class ViewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (this.view.isPanel) {
if (this.view.panel) {
return FractionallySizedBox(
widthFactor: 1,
heightFactor: 1,
child: _buildPanelChild(context),
);
} else {
Widget cardsContainer;
if (this.view.cards.isNotEmpty) {
cardsContainer = DynamicMultiColumnLayout(
minColumnWidth: Sizes.minViewColumnWidth,
children: this.view.cards.map((card) => card.build(context)).toList(),
);
} else {
cardsContainer = Container();
}
return ListView(
shrinkWrap: true,
padding: EdgeInsets.all(0),
children: <Widget>[
_buildBadges(context),
cardsContainer
DynamicMultiColumnLayout(
minColumnWidth: Sizes.minViewColumnWidth,
children: this.view.cards.map((card) => card.build(context)).toList(),
)
]
);
}

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