Compare commits

...

49 Commits
0.4.4 ... 0.5.4

Author SHA1 Message Date
5211d1ff46 v. 0.5.4 2019-05-09 14:15:21 +03:00
d1c9fddba6 v. 0.5.4 2019-05-09 13:17:30 +03:00
14cc55a2c7 Resolves #373 2019-05-09 13:16:35 +03:00
277c67fc6f Add padding for links in About dialog 2019-04-04 21:54:41 +03:00
2a01ff8a03 Bump version in UI 2019-04-04 21:51:05 +03:00
b246b7bc1d 0.5.3 and new build numbers 2019-04-04 21:44:16 +03:00
e1868b9a14 Add privacy polici and terms and conditions links 2019-04-04 21:43:23 +03:00
125f3ac16c Resolves #327 Timer duration parsing error 2019-04-04 21:38:23 +03:00
be502b5668 Discord icon fix 2019-04-04 21:38:05 +03:00
6f33fdca9f New app icon 2019-04-04 21:37:41 +03:00
db77cc43aa Version 0.5.0 2019-03-13 22:42:03 +02:00
b2269cc96d Resolves #293 Fix updater icon 2019-03-13 22:40:54 +02:00
8b28bb2e9e Resolves #314 card icon priority 2019-03-13 22:12:01 +02:00
fb456878bc Resolves #258 Timer support 2019-03-13 21:33:58 +02:00
8b961ebd69 Resolves #83 Calendar support 2019-03-13 20:07:44 +02:00
9bd3a41cf5 Resolves #140 Scenes 2019-03-13 18:06:43 +02:00
491ae55a2a Resolves #299, Resolves #234 Fix entity picture url issue 2019-03-13 17:48:49 +02:00
e1d2981782 Add 'Open Web UI' menu link 2019-03-13 17:25:08 +02:00
74572168ae Resolves #116 Add Iframe panel support 2019-03-13 17:23:23 +02:00
92d0b5c055 Migrate to AndroidX 2019-03-13 17:05:15 +02:00
3504d3276c Resolves #11 Add Panels fetching 2019-03-13 16:39:23 +02:00
736b38b64c Some UI improvements for #245 2019-03-13 14:08:54 +02:00
cb118b599a Resolves #245 Add special row elements support for entities card 2019-03-13 00:56:57 +02:00
a08a056cff Resolves #254 Missed entities 2019-03-12 23:35:33 +02:00
0ef2ebfe31 Fix 'Paste color' button background when saved color is null 2019-03-10 23:49:05 +02:00
4f4ac3b574 Resolves #310 Add assumed state for locks 2019-03-10 23:41:14 +02:00
7064cb0e30 Resolves #272 Add 'Copy color' and 'Past color' 2019-03-10 23:28:23 +02:00
91a99e17e0 Resolves #320 Fix eEntity_picture size 2019-03-10 22:50:39 +02:00
2e9b7d20b9 Fix broken icons 2019-03-10 19:28:11 +02:00
b8aa808de4 Update Material Design Icons to 3.5.95 2019-03-09 13:26:45 +00:00
2cfa92a42b Reverts #308 2019-03-06 16:50:30 +00:00
146efef72d Gradle config for Chrome OS build 2019-03-06 16:42:05 +00:00
8c9804e16f WIP #308 2019-03-02 20:13:24 +02:00
a4736bfb5a Message handling improvements 2019-03-02 18:00:25 +02:00
15c54df629 Update README.md 2019-02-26 11:31:39 +02:00
32ffef21e9 Update README.md 2019-02-26 11:31:08 +02:00
848d3cb510 Update README.md 2019-02-26 10:45:25 +02:00
8a4caeebba Update README.md 2019-02-26 10:43:47 +02:00
aa923f0fba Update README.md 2019-02-26 10:39:09 +02:00
4d8f50ddd5 Update README.md 2019-02-26 10:33:34 +02:00
fe06b21a6c Update README.md 2019-02-26 10:30:08 +02:00
efed7fb1b5 Update README.md 2019-02-26 10:23:03 +02:00
df2cbb7d13 Resolves #313 Fix missed mute button for media_player 2019-02-22 15:39:53 +02:00
03edaa9ca2 Resolves #168 Fix error when entity view closed before history loaded 2019-02-22 15:33:10 +02:00
1a7457abf9 Resolves #311 Rebuild tabs only if views count changes 2019-02-22 15:28:11 +02:00
00889b13e0 Resolves #312 Add white value control for light 2019-02-22 15:15:27 +02:00
0615073ec4 Get color from rgb_color if there is no hsv_color attribute 2019-02-22 14:20:01 +02:00
eb7d17d147 WIP #308 Move entity icon generation into EntityIcon widget 2019-02-21 16:32:55 +02:00
24f80feeee Resolves #187 Fix crash on view count changes 2019-02-21 15:35:58 +02:00
52 changed files with 1850 additions and 413 deletions

View File

@ -1,13 +1,12 @@
[![flutter](https://somegeeky.website/assets/badges/flutter_badge_v3.svg)](https://somegeeky.website/badges/flutter) [![dart](https://somegeeky.website/assets/badges/dart_badge_v3.svg)](https://somegeeky.website/badges/dart)
# HA Client # HA Client
## Native Android client for Home Assistant ## Native Android client for Home Assistant
### With Lovelace UI support ### With Lovelace UI support
Home Assistant Android client on Dart with Flutter. Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
Visit [www.vynn.co](https://www.vynn.co/ha-client) for more info.
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or in [Discord](https://discord.gg/NSaQEQ8) Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)

View File

@ -29,7 +29,7 @@ def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android { android {
compileSdkVersion 27 compileSdkVersion 28
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -43,7 +43,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.keyboardcrumbs.haclient" applicationId "com.keyboardcrumbs.haclient"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 27 targetSdkVersion 28
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

0
android/gradlew vendored Normal file → Executable file
View File

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -9,7 +9,7 @@ class ButtonEntity extends Entity {
entityId: entityId, entityId: entityId,
serviceDomain: domain, serviceDomain: domain,
serviceName: 'turn_on', serviceName: 'turn_on',
text: "EXECUTE", text: domain == "scene" ? "ACTIVATE" : "EXECUTE",
); );
} }
} }

View File

@ -28,6 +28,7 @@ class EntityState {
static const unavailable = 'unavailable'; static const unavailable = 'unavailable';
static const ok = 'ok'; static const ok = 'ok';
static const problem = 'problem'; static const problem = 'problem';
static const active = 'active';
} }
class EntityUIAction { class EntityUIAction {

View File

@ -1,5 +1,14 @@
part of '../main.dart'; part of '../main.dart';
class StatelessEntityType {
static const NONE = 0;
static const MISSED = 1;
static const DIVIDER = 2;
static const SECTION = 3;
static const CALL_SERVICE = 4;
static const WEBLINK = 5;
}
class Entity { class Entity {
static List badgeDomains = [ static List badgeDomains = [
@ -67,9 +76,9 @@ class Entity {
String state; String state;
String displayState; String displayState;
DateTime _lastUpdated; DateTime _lastUpdated;
int statelessType = 0;
List<Entity> childEntities = []; List<Entity> childEntities = [];
List<String> attributesToShow = ["all"];
String deviceClass; String deviceClass;
EntityHistoryConfig historyConfig = EntityHistoryConfig( EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.simple chartType: EntityHistoryWidgetType.simple
@ -85,7 +94,7 @@ class Entity {
bool get isBadge => Entity.badgeDomains.contains(domain); bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? ""; String get icon => attributes["icon"] ?? "";
bool get isOn => state == EntityState.on; bool get isOn => state == EntityState.on;
String get entityPicture => attributes["entity_picture"]; String get entityPicture => _getEntityPictureUrl();
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? ""; String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? []; List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted(); String get lastUpdated => _getLastUpdatedFormatted();
@ -93,10 +102,52 @@ class Entity {
double get doubleState => double.tryParse(state) ?? 0.0; double get doubleState => double.tryParse(state) ?? 0.0;
int get supportedFeatures => attributes["supported_features"] ?? 0; int get supportedFeatures => attributes["supported_features"] ?? 0;
String _getEntityPictureUrl() {
String result = attributes["entity_picture"];
if (result == null) return result;
if (!result.startsWith("http")) {
if (result.startsWith("/")) {
result = "$homeAssistantWebHost$result";
} else {
result = "$homeAssistantWebHost/$result";
}
}
return result;
}
Entity(Map rawData) { Entity(Map rawData) {
update(rawData); update(rawData);
} }
Entity.missed(String entityId) {
statelessType = StatelessEntityType.MISSED;
attributes = {"hidden": false};
this.entityId = entityId;
}
Entity.divider() {
statelessType = StatelessEntityType.DIVIDER;
attributes = {"hidden": false};
}
Entity.section(String label) {
statelessType = StatelessEntityType.SECTION;
attributes = {"hidden": false, "friendly_name": "$label"};
}
Entity.callService({String icon, String name, String service, String actionName}) {
statelessType = StatelessEntityType.CALL_SERVICE;
entityId = service;
displayState = actionName?.toUpperCase() ?? "RUN";
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
}
Entity.weblink({String url, String name, String icon}) {
statelessType = StatelessEntityType.WEBLINK;
entityId = "custom.custom"; //TODO wtf??
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
}
void update(Map rawData) { void update(Map rawData) {
attributes = rawData["attributes"] ?? {}; attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0]; domain = rawData["entity_id"].split(".")[0];

View File

@ -4,6 +4,7 @@ class EntityWrapper {
String displayName; String displayName;
String icon; String icon;
String entityPicture;
EntityUIAction uiAction; EntityUIAction uiAction;
Entity entity; Entity entity;
@ -14,10 +15,15 @@ class EntityWrapper {
String displayName, String displayName,
this.uiAction this.uiAction
}) { }) {
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
this.icon = icon ?? entity.icon; this.icon = icon ?? entity.icon;
if (icon == null) {
entityPicture = entity.entityPicture;
}
this.displayName = displayName ?? entity.displayName; this.displayName = displayName ?? entity.displayName;
if (this.uiAction == null) { if (uiAction == null) {
this.uiAction = EntityUIAction(); uiAction = EntityUIAction();
}
} }
} }
@ -49,6 +55,16 @@ class EntityWrapper {
break; break;
} }
case EntityUIAction.navigate: {
if (uiAction.tapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.tapService);
}
break;
}
default: { default: {
break; break;
} }
@ -79,6 +95,16 @@ class EntityWrapper {
break; break;
} }
case EntityUIAction.navigate: {
if (uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.holdService);
}
break;
}
default: { default: {
break; break;
} }

View File

@ -33,6 +33,7 @@ class LightEntity extends Entity {
LightEntity.SUPPORT_WHITE_VALUE); LightEntity.SUPPORT_WHITE_VALUE);
int get brightness => _getIntAttributeValue("brightness"); int get brightness => _getIntAttributeValue("brightness");
int get whiteValue => _getIntAttributeValue("white_value");
String get effect => attributes["effect"]; String get effect => attributes["effect"];
int get colorTemp => _getIntAttributeValue("color_temp"); int get colorTemp => _getIntAttributeValue("color_temp");
double get maxMireds => _getDoubleAttributeValue("max_mireds"); double get maxMireds => _getDoubleAttributeValue("max_mireds");
@ -45,11 +46,14 @@ class LightEntity extends Entity {
HSVColor _getColor() { HSVColor _getColor() {
List hs = attributes["hs_color"]; List hs = attributes["hs_color"];
List rgb = attributes["rgb_color"];
try { try {
if ((hs != null) && (hs.length > 0)) { if (hs != null && hs.isNotEmpty) {
double sat = hs[1]/100; double sat = hs[1]/100;
String ssat = sat.toStringAsFixed(2); String ssat = sat.toStringAsFixed(2);
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0); return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
} else if (rgb != null && rgb.isNotEmpty) {
return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2]));
} else { } else {
return null; return null;
} }

View File

@ -7,6 +7,15 @@ class LockEntity extends Entity {
@override @override
Widget _buildStatePart(BuildContext context) { Widget _buildStatePart(BuildContext context) {
return LockStateWidget(); return LockStateWidget(
assumedState: false,
);
}
@override
Widget _buildStatePartForPage(BuildContext context) {
return LockStateWidget(
assumedState: true,
);
} }
} }

View File

@ -0,0 +1,45 @@
part of '../main.dart';
class TimerEntity extends Entity {
TimerEntity(Map rawData) : super(rawData);
Duration duration;
@override
void update(Map rawData) {
super.update(rawData);
String durationSource = "${attributes["duration"]}";
if (durationSource != null && durationSource.isNotEmpty) {
try {
List<String> durationList = durationSource.split(":");
if (durationList.length == 1) {
duration = Duration(seconds: int.tryParse(durationList[0] ?? 0));
} else if (durationList.length == 2) {
duration = Duration(
hours: int.tryParse(durationList[0]) ?? 0,
minutes: int.tryParse(durationList[1]) ?? 0
);
} else if (durationList.length == 3) {
duration = Duration(
hours: int.tryParse(durationList[0]) ?? 0,
minutes: int.tryParse(durationList[1]) ?? 0,
seconds: int.tryParse(durationList[2]) ?? 0
);
} else {
Logger.e("Strange $entityId duration format: $durationSource");
duration = Duration(seconds: 0);
}
} catch (e) {
Logger.e("Error parsing duration for $entityId: ${e.toString()}");
duration = Duration(seconds: 0);
}
} else {
duration = Duration(seconds: 0);
}
}
@override
Widget _buildStatePart(BuildContext context) {
return TimerState();
}
}

View File

@ -92,6 +92,9 @@ class EntityCollection {
case "alarm_control_panel": { case "alarm_control_panel": {
return AlarmControlPanelEntity(rawEntityData); return AlarmControlPanelEntity(rawEntityData);
} }
case "timer": {
return TimerEntity(rawEntityData);
}
default: { default: {
return Entity(rawEntityData); return Entity(rawEntityData);
} }

View File

@ -9,7 +9,12 @@ class ButtonEntityContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
return InkWell( return InkWell(
onTap: () => entityWrapper.handleTap(), onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(), onLongPress: () => entityWrapper.handleHold(),
@ -22,7 +27,7 @@ class ButtonEntityContainer extends StatelessWidget {
fit: BoxFit.fitHeight, fit: BoxFit.fitHeight,
child: EntityIcon( child: EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0), padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
iconSize: Sizes.iconSize, size: Sizes.iconSize,
) )
), ),
), ),

View File

@ -14,11 +14,11 @@ class BadgeWidget extends StatelessWidget {
{ {
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon" badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
? Icon( ? Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc), MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
size: iconSize, size: iconSize,
) )
: Icon( : Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8), MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
size: iconSize, size: iconSize,
); );
break; break;
@ -27,14 +27,20 @@ class BadgeWidget extends StatelessWidget {
case "media_player": case "media_player":
case "binary_sensor": case "binary_sensor":
{ {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData( badgeIcon = EntityIcon(
entityModel.entityWrapper, iconSize, Colors.black); padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
break; break;
} }
case "device_tracker": case "device_tracker":
{ {
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData( badgeIcon = EntityIcon(
entityModel.entityWrapper, iconSize, Colors.black); padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
onBadgeTextValue = entityModel.entityWrapper.entity.state; onBadgeTextValue = entityModel.entityWrapper.entity.state;
break; break;
} }

View File

@ -7,20 +7,9 @@ class EntityAttributesList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
List<Widget> attrs = []; List<Widget> attrs = [];
if ((entityModel.entityWrapper.entity.attributesToShow == null) ||
(entityModel.entityWrapper.entity.attributesToShow.contains("all"))) {
entityModel.entityWrapper.entity.attributes.forEach((name, value) { entityModel.entityWrapper.entity.attributes.forEach((name, value) {
attrs.add(_buildSingleAttribute("$name", "$value")); attrs.add(_buildSingleAttribute("$name", "${value ?? '-'}"));
}); });
} else {
entityModel.entityWrapper.entity.attributesToShow.forEach((String attr) {
String attrValue = entityModel.entityWrapper.entity.getAttribute("$attr");
if (attrValue != null) {
attrs.add(
_buildSingleAttribute("$attr", "$attrValue"));
}
});
}
return Padding( return Padding(
padding: EdgeInsets.only(bottom: Sizes.rowPadding), padding: EdgeInsets.only(bottom: Sizes.rowPadding),
child: Column( child: Column(
@ -49,7 +38,7 @@ class EntityAttributesList extends StatelessWidget {
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), 0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
child: Text( child: Text(
"$value", "${value}",
textAlign: TextAlign.right, textAlign: TextAlign.right,
), ),
), ),

View File

@ -440,13 +440,13 @@ class TemperatureControlWidget extends StatelessWidget {
Column( Column(
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-up')), 'mdi:chevron-up')),
iconSize: 30.0, iconSize: 30.0,
onPressed: () => onInc(), onPressed: () => onInc(),
), ),
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-down')), 'mdi:chevron-down')),
iconSize: 30.0, iconSize: 30.0,
onPressed: () => onDec(), onPressed: () => onDec(),

View File

@ -157,7 +157,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
if (entity.supportOpenTilt) { if (entity.supportOpenTilt) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
"mdi:arrow-top-right"), "mdi:arrow-top-right"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
@ -170,7 +170,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
if (entity.supportStopTilt) { if (entity.supportStopTilt) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"), MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
onPressed: () => _stop(entity))); onPressed: () => _stop(entity)));
@ -182,7 +182,7 @@ class CoverTiltControlsWidget extends StatelessWidget {
if (entity.supportCloseTilt) { if (entity.supportCloseTilt) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
"mdi:arrow-bottom-left"), "mdi:arrow-bottom-left"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),

View File

@ -10,6 +10,7 @@ class LightControlsWidget extends StatefulWidget {
class _LightControlsWidgetState extends State<LightControlsWidget> { class _LightControlsWidgetState extends State<LightControlsWidget> {
int _tmpBrightness; int _tmpBrightness;
int _tmpWhiteValue;
int _tmpColorTemp = 0; int _tmpColorTemp = 0;
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0); HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
bool _changedHere = false; bool _changedHere = false;
@ -17,6 +18,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
void _resetState(LightEntity entity) { void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 0; _tmpBrightness = entity.brightness ?? 0;
_tmpWhiteValue = entity.whiteValue ?? 0;
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt(); _tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
_tmpColor = entity.color ?? _tmpColor; _tmpColor = entity.color ?? _tmpColor;
_tmpEffect = entity.effect; _tmpEffect = entity.effect;
@ -38,6 +40,17 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}); });
} }
void _setWhiteValue(LightEntity entity, double value) {
setState(() {
_tmpWhiteValue = value.round();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"white_value": _tmpWhiteValue}));
});
}
void _setColorTemp(LightEntity entity, double value) { void _setColorTemp(LightEntity entity, double value) {
setState(() { setState(() {
_tmpColorTemp = value.round(); _tmpColorTemp = value.round();
@ -84,6 +97,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
_buildBrightnessControl(entity), _buildBrightnessControl(entity),
_buildWhiteValueControl(entity),
_buildColorTempControl(entity), _buildColorTempControl(entity),
_buildColorControl(entity), _buildColorControl(entity),
_buildEffectControl(entity) _buildEffectControl(entity)
@ -112,6 +126,27 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
} }
} }
Widget _buildWhiteValueControl(LightEntity entity) {
if ((entity.supportWhiteValue) && (_tmpWhiteValue != null)) {
return UniversalSlider(
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpWhiteValue = value.round();
});
},
min: 0.0,
max: 255.0,
onChangeEnd: (value) => _setWhiteValue(entity, value),
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
title: "White",
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildColorTempControl(LightEntity entity) { Widget _buildColorTempControl(LightEntity entity) {
if (entity.supportColorTemp) { if (entity.supportColorTemp) {
return UniversalSlider( return UniversalSlider(
@ -136,30 +171,45 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildColorControl(LightEntity entity) { Widget _buildColorControl(LightEntity entity) {
if (entity.supportColor) { if (entity.supportColor) {
return LightColorPicker( HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
LightColorPicker(
color: _tmpColor, color: _tmpColor,
onColorSelected: (color) => _setColor(entity, color), onColorSelected: (color) => _setColor(entity, color),
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
color: _tmpColor.toColor(),
child: Text('Copy color'),
onPressed: _tmpColor == null ? null : () {
setState(() {
HomeAssistantModel
.of(context)
.homeAssistant
.savedColor = _tmpColor;
});
},
),
FlatButton(
color: savedColor?.toColor() ?? Colors.transparent,
child: Text('Paste color'),
onPressed: savedColor == null ? null : () {
_setColor(entity, savedColor);
},
)
],
)
],
); );
} else { } else {
return Container(width: 0.0, height: 0.0); return Container(width: 0.0, height: 0.0);
} }
} }
void _showColorPicker(LightEntity entity) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
titlePadding: EdgeInsets.all(0.0),
contentPadding: EdgeInsets.all(0.0),
content: LightColorPicker(
color: _tmpColor,
),
);
},
);
}
Widget _buildEffectControl(LightEntity entity) { Widget _buildEffectControl(LightEntity entity) {
if ((entity.supportEffect) && (entity.effectList != null)) { if ((entity.supportEffect) && (entity.effectList != null)) {
return ModeSelectorWidget( return ModeSelectorWidget(

View File

@ -81,7 +81,7 @@ class MediaPlayerWidget extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: Image( child: Image(
image: CachedNetworkImageProvider("$homeAssistantWebHost${entity.entityPicture}"), image: CachedNetworkImageProvider("${entity.entityPicture}"),
height: 240.0, height: 240.0,
//width: 320.0, //width: 320.0,
fit: BoxFit.contain, fit: BoxFit.contain,
@ -95,7 +95,7 @@ class MediaPlayerWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:movie"), MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
size: 150.0, size: 150.0,
color: EntityColor.stateColor("$state"), color: EntityColor.stateColor("$state"),
) )
@ -227,7 +227,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
if (showMenu) { if (showMenu) {
result.add( result.add(
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
) )
@ -307,11 +307,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) { if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
Widget muteWidget; Widget muteWidget;
Widget volumeStepWidget; Widget volumeStepWidget;
if (entity.supportVolumeMute) { if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
bool isMuted = entity.attributes["is_volume_muted"] ?? false; bool isMuted = entity.attributes["is_volume_muted"] ?? false;
muteWidget = muteWidget =
IconButton( IconButton(
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up), icon: Icon(isMuted ? Icons.volume_up : Icons.volume_off),
onPressed: () => _setVolumeMute(!isMuted, entity.entityId) onPressed: () => _setVolumeMute(!isMuted, entity.entityId)
); );
} else { } else {
@ -322,11 +322,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:plus")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId) onPressed: () => _setVolumeUp(entity.entityId)
), ),
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:minus")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
onPressed: () => _setVolumeDown(entity.entityId) onPressed: () => _setVolumeDown(entity.entityId)
) )
], ],

View File

@ -11,6 +11,25 @@ class DefaultEntityContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context); final EntityModel entityModel = EntityModel.of(context);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
return Divider();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Divider(),
Text(
"${entityModel.entityWrapper.entity.displayName}",
style: TextStyle(color: Colors.blue),
)
],
);
}
return InkWell( return InkWell(
onLongPress: () { onLongPress: () {
if (entityModel.handleTap) { if (entityModel.handleTap) {
@ -30,7 +49,9 @@ class DefaultEntityContainer extends StatelessWidget {
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 3, flex: 3,
child: EntityName(), child: EntityName(
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
),
), ),
state state
], ],

View File

@ -2,6 +2,8 @@ part of '../main.dart';
class EntityColor { class EntityColor {
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
static const badgeColors = { static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0), "default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0) "binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
@ -10,15 +12,16 @@ class EntityColor {
static const _stateColors = { static const _stateColors = {
EntityState.on: Colors.amber, EntityState.on: Colors.amber,
"auto": Colors.amber, "auto": Colors.amber,
EntityState.idle: Colors.amber, EntityState.active: Colors.amber,
EntityState.playing: Colors.amber, EntityState.playing: Colors.amber,
"above_horizon": Colors.amber, "above_horizon": Colors.amber,
EntityState.home: Colors.amber, EntityState.home: Colors.amber,
EntityState.open: Colors.amber, EntityState.open: Colors.amber,
EntityState.off: Color.fromRGBO(68, 115, 158, 1.0), EntityState.off: defaultStateColor,
EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0), EntityState.closed: defaultStateColor,
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0), "below_horizon": defaultStateColor,
"default": Color.fromRGBO(68, 115, 158, 1.0), "default": defaultStateColor,
EntityState.idle: defaultStateColor,
"heat": Colors.redAccent, "heat": Colors.redAccent,
"cool": Colors.lightBlue, "cool": Colors.lightBlue,
EntityState.unavailable: Colors.black26, EntityState.unavailable: Colors.black26,

View File

@ -3,20 +3,71 @@ part of '../main.dart';
class EntityIcon extends StatelessWidget { class EntityIcon extends StatelessWidget {
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final double iconSize; final double size;
final Color color;
const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB( const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : 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, Color color) {
if (data == null) {
return null;
}
if (data.entityPicture != null) {
return Container(
height: size+12,
width: size+12,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit:BoxFit.cover,
image: CachedNetworkImageProvider(
"${data.entityPicture}"
),
)
),
);
}
String iconName = data.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
return Padding(
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
)
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding( return Padding(
padding: padding, padding: padding,
child: MaterialDesignIcons.createIconWidgetFromEntityData( child: buildIcon(
entityWrapper, entityWrapper,
iconSize, color ?? EntityColor.stateColor(entityWrapper.entity.state)
EntityColor.stateColor(entityWrapper.entity.state)
), ),
); );
} }

View File

@ -14,6 +14,10 @@ class EntityName extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
TextStyle textStyle = TextStyle(fontSize: fontSize);
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
}
return Padding( return Padding(
padding: padding, padding: padding,
child: Text( child: Text(
@ -21,7 +25,7 @@ class EntityName extends StatelessWidget {
overflow: textOverflow, overflow: textOverflow,
softWrap: wordsWrap, softWrap: wordsWrap,
maxLines: maxLines, maxLines: maxLines,
style: TextStyle(fontSize: fontSize), style: textStyle,
textAlign: textAlign, textAlign: textAlign,
), ),
); );

View File

@ -22,6 +22,12 @@ class GlanceEntityContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = []; List<Widget> result = [];
if (!nameInTheBottom) { if (!nameInTheBottom) {
if (showName) { if (showName) {
@ -35,7 +41,7 @@ class GlanceEntityContainer extends StatelessWidget {
result.add( result.add(
EntityIcon( EntityIcon(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
iconSize: iconSize, size: iconSize,
) )
); );
if (!nameInTheBottom) { if (!nameInTheBottom) {

View File

@ -32,6 +32,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
List _history; List _history;
bool _needToUpdateHistory; bool _needToUpdateHistory;
DateTime _historyLastUpdated; DateTime _historyLastUpdated;
bool _disposed = false;
@override @override
void initState() { void initState() {
@ -47,16 +48,20 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) { if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now; _historyLastUpdated = now;
ha.getHistory(entityId).then((history){ ha.getHistory(entityId).then((history){
if (!_disposed) {
setState(() { setState(() {
_history = history.isNotEmpty ? history[0] : []; _history = history.isNotEmpty ? history[0] : [];
_needToUpdateHistory = false; _needToUpdateHistory = false;
}); });
}
}).catchError((e) { }).catchError((e) {
Logger.e("Error loading $entityId history: $e"); Logger.e("Error loading $entityId history: $e");
if (!_disposed) {
setState(() { setState(() {
_history = []; _history = [];
_needToUpdateHistory = false; _needToUpdateHistory = false;
}); });
}
}); });
} }
} }
@ -131,4 +136,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
} }
@override
void dispose() {
_disposed = true;
super.dispose();
}
} }

View File

@ -0,0 +1,19 @@
part of '../main.dart';
class MissedEntityWidget extends StatelessWidget {
MissedEntityWidget({
Key key
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
return Container(
child: Padding(
padding: EdgeInsets.all(5.0),
child: Text("Entity not available: ${entityModel.entityWrapper.entity.entityId}"),
),
color: Colors.amber[100],
);
}
}

View File

@ -24,7 +24,7 @@ class CoverStateWidget extends StatelessWidget {
if (entity.supportOpen) { if (entity.supportOpen) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"), MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-up"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
onPressed: entity.canBeOpened ? () => _open(entity) : null)); onPressed: entity.canBeOpened ? () => _open(entity) : null));
@ -36,7 +36,7 @@ class CoverStateWidget extends StatelessWidget {
if (entity.supportStop) { if (entity.supportStop) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"), MaterialDesignIcons.getIconDataFromIconName("mdi:stop"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
onPressed: () => _stop(entity))); onPressed: () => _stop(entity)));
@ -48,7 +48,7 @@ class CoverStateWidget extends StatelessWidget {
if (entity.supportClose) { if (entity.supportClose) {
buttons.add(IconButton( buttons.add(IconButton(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"), MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-down"),
size: Sizes.iconSize, size: Sizes.iconSize,
), ),
onPressed: entity.canBeClosed ? () => _close(entity) : null)); onPressed: entity.canBeClosed ? () => _close(entity) : null));

View File

@ -2,6 +2,10 @@ part of '../../main.dart';
class LockStateWidget extends StatelessWidget { class LockStateWidget extends StatelessWidget {
final bool assumedState;
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
void _lock(Entity entity) { void _lock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null)); eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
} }
@ -14,6 +18,35 @@ class LockStateWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
final LockEntity entity = entityModel.entityWrapper.entity; final LockEntity entity = entityModel.entityWrapper.entity;
if (assumedState) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
height: 34.0,
child: FlatButton(
onPressed: () => _unlock(entity),
child: Text("UNLOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
),
)
),
SizedBox(
height: 34.0,
child: FlatButton(
onPressed: () => _lock(entity),
child: Text("LOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
),
)
)
],
);
} else {
return SizedBox( return SizedBox(
height: 34.0, height: 34.0,
child: FlatButton( child: FlatButton(
@ -30,3 +63,4 @@ class LockStateWidget extends StatelessWidget {
); );
} }
} }
}

View File

@ -6,14 +6,26 @@ class SimpleEntityState extends StatelessWidget {
final TextAlign textAlign; final TextAlign textAlign;
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final int maxLines; final int maxLines;
final String customValue;
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0)}) : super(key: key); const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
String state = entityModel.entityWrapper.entity.displayState ?? ""; String state;
if (customValue == null) {
state = entityModel.entityWrapper.entity.displayState ?? "";
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim(); state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
} else {
state = customValue;
}
TextStyle textStyle = TextStyle(
fontSize: Sizes.stateFontSize,
);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
textStyle = textStyle.apply(color: Colors.blue);
}
while (state.contains(" ")){ while (state.contains(" ")){
state = state.replaceAll(" ", " "); state = state.replaceAll(" ", " ");
} }
@ -25,9 +37,7 @@ class SimpleEntityState extends StatelessWidget {
maxLines: maxLines, maxLines: maxLines,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
softWrap: true, softWrap: true,
style: new TextStyle( style: textStyle
fontSize: Sizes.stateFontSize,
)
) )
); );
if (expanded) { if (expanded) {

View File

@ -71,13 +71,13 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
onPressed: () => _setNewState(false, entity), onPressed: () => _setNewState(false, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash-off")),
color: newState == EntityState.on ? Colors.black : Colors.blue, color: newState == EntityState.on ? Colors.black : Colors.blue,
iconSize: Sizes.iconSize, iconSize: Sizes.iconSize,
), ),
IconButton( IconButton(
onPressed: () => _setNewState(true, entity), onPressed: () => _setNewState(true, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")), icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash")),
color: newState == EntityState.on ? Colors.blue : Colors.black, color: newState == EntityState.on ? Colors.blue : Colors.black,
iconSize: Sizes.iconSize iconSize: Sizes.iconSize
) )

View File

@ -0,0 +1,65 @@
part of '../../main.dart';
class TimerState extends StatefulWidget {
//final bool expanded;
//final TextAlign textAlign;
//final EdgeInsetsGeometry padding;
//final int maxLines;
const TimerState({Key key}) : super(key: key);
@override
_TimerStateState createState() => _TimerStateState();
}
class _TimerStateState extends State<TimerState> {
Timer timer;
Duration remaining = Duration(seconds: 0);
void checkState(TimerEntity entity) {
if (entity.state == EntityState.active) {
//Logger.d("Timer is active");
if (timer == null || !timer.isActive) {
timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
try {
int passed = DateTime
.now()
.difference(entity._lastUpdated)
.inSeconds;
remaining = Duration(seconds: entity.duration.inSeconds - passed);
} catch (e) {
Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}");
remaining = Duration(seconds: 0);
}
});
});
}
} else {
timer?.cancel();
}
}
@override
Widget build(BuildContext context) {
EntityModel model = EntityModel.of(context);
TimerEntity entity = model.entityWrapper.entity;
checkState(entity);
if (entity.state != EntityState.active) {
return SimpleEntityState();
} else {
return SimpleEntityState(
customValue: "${remaining.toString().split('.')[0]}",
);
}
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
}

View File

@ -9,26 +9,20 @@ class HomeAssistant {
SendMessageQueue _messageQueue; SendMessageQueue _messageQueue;
int _currentMessageId = 0; int _currentMessageId = 0;
int _statesMessageId = 0;
int _servicesMessageId = 0;
int _subscriptionMessageId = 0; int _subscriptionMessageId = 0;
int _configMessageId = 0; Map<int, Completer> _messageResolver = {};
int _userInfoMessageId = 0;
int _lovelaceMessageId = 0;
EntityCollection entities; EntityCollection entities;
HomeAssistantUI ui; HomeAssistantUI ui;
Map _instanceConfig = {}; Map _instanceConfig = {};
String _userName; String _userName;
HSVColor savedColor;
Map _rawLovelaceData; Map _rawLovelaceData;
List<Panel> panels = [];
Completer _fetchCompleter; Completer _fetchCompleter;
Completer _statesCompleter;
Completer _servicesCompleter;
Completer _lovelaceCompleter;
Completer _configCompleter;
Completer _connectionCompleter; Completer _connectionCompleter;
Completer _userInfoCompleter;
Timer _connectionTimer; Timer _connectionTimer;
Timer _fetchTimer; Timer _fetchTimer;
bool autoReconnect = false; bool autoReconnect = false;
@ -167,6 +161,7 @@ class HomeAssistant {
futures.add(_getConfig()); futures.add(_getConfig());
futures.add(_getServices()); futures.add(_getServices());
futures.add(_getUserInfo()); futures.add(_getUserInfo());
futures.add(_getPanels());
try { try {
await Future.wait(futures); await Future.wait(futures);
_createUI(); _createUI();
@ -211,7 +206,7 @@ class HomeAssistant {
_handleMessage(String message) { _handleMessage(String message) {
var data = json.decode(message); var data = json.decode(message);
if (data["type"] == "auth_required") { if (data["type"] == "auth_required") {
_sendAuthMessageRaw('{"type": "auth","access_token": "$_password"}'); _sendAuthMessage('{"type": "auth","access_token": "$_password"}');
} else if (data["type"] == "auth_ok") { } else if (data["type"] == "auth_ok") {
_completeConnecting(null); _completeConnecting(null);
_sendSubscribe(); _sendSubscribe();
@ -219,17 +214,8 @@ class HomeAssistant {
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"}); _completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") { } else if (data["type"] == "result") {
Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}"); Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
if (data["id"] == _configMessageId) { _messageResolver[data["id"]]?.complete(data);
_parseConfig(data); _messageResolver.remove(data["id"]);
} else if (data["id"] == _statesMessageId) {
_parseEntities(data);
} else if (data["id"] == _lovelaceMessageId) {
_handleLovelace(data);
} else if (data["id"] == _servicesMessageId) {
_parseServices(data);
} else if (data["id"] == _userInfoMessageId) {
_parseUserInfo(data);
}
} else if (data["type"] == "event") { } else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) { if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}"); Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
@ -247,64 +233,68 @@ class HomeAssistant {
void _sendSubscribe() { void _sendSubscribe() {
_incrementMessageId(); _incrementMessageId();
_subscriptionMessageId = _currentMessageId; _subscriptionMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false); _send('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
} }
Future _getConfig() { Future _getConfig() async {
_configCompleter = new Completer(); await _sendInitialMessage("get_config").then((data) => _instanceConfig = Map.from(data["result"]));
_incrementMessageId();
_configMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
return _configCompleter.future;
} }
Future _getStates() { Future _getStates() async {
_statesCompleter = new Completer(); await _sendInitialMessage("get_states").then((data) => entities.parse(data["result"]));
_incrementMessageId();
_statesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
return _statesCompleter.future;
} }
Future _getLovelace() { Future _getLovelace() async {
_lovelaceCompleter = new Completer(); await _sendInitialMessage("lovelace/config").then((data) => _rawLovelaceData = data["result"]);
_incrementMessageId();
_lovelaceMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false);
return _lovelaceCompleter.future;
} }
Future _getUserInfo() { Future _getUserInfo() async {
_userInfoCompleter = new Completer(); _userName = null;
_incrementMessageId(); await _sendInitialMessage("auth/current_user").then((data) => _userName = data["result"]["name"]);
_userInfoMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
return _userInfoCompleter.future;
} }
Future _getServices() { Future _getServices() async {
_servicesCompleter = new Completer(); await _sendInitialMessage("get_services").then((data) => Logger.d("We actually don`t need the list of servcies for now"));
_incrementMessageId(); }
_servicesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
return _servicesCompleter.future; Future _getPanels() async {
panels.clear();
await _sendInitialMessage("get_panels").then((data) {
if (data["success"]) {
data["result"].forEach((k,v) {
String title = v['title'] == null ? "${k[0]?.toUpperCase()}${k?.substring(1)}" : "${v['title'][0]?.toUpperCase()}${v['title']?.substring(1)}";
panels.add(Panel(
id: k,
type: v["component_name"],
title: title,
urlPath: v["url_path"],
config: v["config"],
icon: v["icon"]
)
);
});
}
});
} }
_incrementMessageId() { _incrementMessageId() {
_currentMessageId += 1; _currentMessageId += 1;
} }
void _sendAuthMessageRaw(String message) { void _sendAuthMessage(String message) {
Logger.d( "[Sending] ==> auth request"); Logger.d( "[Sending] ==> auth request");
_hassioChannel.sink.add(message); _hassioChannel.sink.add(message);
} }
_sendMessageRaw(String message, bool queued) { Future _sendInitialMessage(String type) {
Completer _completer = Completer();
_incrementMessageId();
_messageResolver[_currentMessageId] = _completer;
_send('{"id": $_currentMessageId, "type": "$type"}', false);
return _completer.future;
}
_send(String message, bool queued) {
var sendCompleter = Completer(); var sendCompleter = Completer();
if (queued) _messageQueue.add(message); if (queued) _messageQueue.add(message);
_connection().then((r) { _connection().then((r) {
@ -359,7 +349,7 @@ class HomeAssistant {
} }
message += '}'; message += '}';
} }
return _sendMessageRaw(message, true); return _send(message, true);
} }
void _handleEntityStateChange(Map eventData) { void _handleEntityStateChange(Map eventData) {
@ -371,39 +361,6 @@ class HomeAssistant {
)); ));
} }
void _parseConfig(Map data) {
if (data["success"] == true) {
_instanceConfig = Map.from(data["result"]);
_configCompleter.complete();
} else {
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
}
}
void _parseUserInfo(Map data) {
if (data["success"] == true) {
_userName = data["result"]["name"];
} else {
_userName = null;
Logger.w("There was an error getting current user: $data");
}
_userInfoCompleter.complete();
}
void _parseServices(response) {
_servicesCompleter.complete();
}
void _handleLovelace(response) {
if (response["success"] == true) {
_rawLovelaceData = response["result"];
} else {
Logger.e("There was an error getting Lovelace config: $response");
_rawLovelaceData = null;
}
_lovelaceCompleter.complete();
}
void _parseLovelace() { void _parseLovelace() {
Logger.d("--Title: ${_rawLovelaceData["title"]}"); Logger.d("--Title: ${_rawLovelaceData["title"]}");
ui.title = _rawLovelaceData["title"]; ui.title = _rawLovelaceData["title"];
@ -467,9 +424,51 @@ class HomeAssistant {
if (rawEntity is String) { if (rawEntity is String) {
if (entities.isExist(rawEntity)) { if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity))); card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
} }
} else { } else {
if (entities.isExist(rawEntity["entity"])) { 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"]); Entity e = entities.get(rawEntity["entity"]);
card.entities.add( card.entities.add(
EntityWrapper( EntityWrapper(
@ -479,6 +478,8 @@ class HomeAssistant {
uiAction: EntityUIAction(rawEntityData: rawEntity) uiAction: EntityUIAction(rawEntityData: rawEntity)
) )
); );
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
} }
} }
}); });
@ -493,6 +494,8 @@ class HomeAssistant {
displayName: rawCard["name"], displayName: rawCard["name"],
uiAction: EntityUIAction(rawEntityData: rawCard) uiAction: EntityUIAction(rawEntityData: rawCard)
); );
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
} }
} else { } else {
if (entities.isExist(en["entity"])) { if (entities.isExist(en["entity"])) {
@ -503,6 +506,8 @@ class HomeAssistant {
displayName: en["name"], displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard) uiAction: EntityUIAction(rawEntityData: rawCard)
); );
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
} }
} }
} }
@ -514,15 +519,6 @@ class HomeAssistant {
return result; return result;
} }
void _parseEntities(response) async {
if (response["success"] == false) {
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
return;
}
entities.parse(response["result"]);
_statesCompleter.complete();
}
void _createUI() { void _createUI() {
ui = HomeAssistantUI(); ui = HomeAssistantUI();
if ((_useLovelace) && (_rawLovelaceData != null)) { if ((_useLovelace) && (_rawLovelaceData != null)) {
@ -559,8 +555,8 @@ class HomeAssistant {
} }
} }
Widget buildViews(BuildContext context, bool lovelace) { Widget buildViews(BuildContext context, bool lovelace, TabController tabController) {
return ui.build(context); return ui.build(context, tabController);
} }
Future<List> getHistory(String entityId) async { Future<List> getHistory(String entityId) async {

View File

@ -8,7 +8,7 @@ import 'package:web_socket_channel/io.dart';
import 'package:event_bus/event_bus.dart'; import 'package:event_bus/event_bus.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart' as urlLauncher;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:date_format/date_format.dart'; import 'package:date_format/date_format.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -16,10 +16,12 @@ import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart'; import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
part 'entity_class/const.dart'; part 'entity_class/const.dart';
part 'entity_class/entity.class.dart'; part 'entity_class/entity.class.dart';
part 'entity_class/entity_wrapper.class.dart'; part 'entity_class/entity_wrapper.class.dart';
part 'entity_class/timer_entity.dart';
part 'entity_class/switch_entity.class.dart'; part 'entity_class/switch_entity.class.dart';
part 'entity_class/button_entity.class.dart'; part 'entity_class/button_entity.class.dart';
part 'entity_class/text_entity.class.dart'; part 'entity_class/text_entity.class.dart';
@ -40,6 +42,7 @@ part 'entity_class/alarm_control_panel.class.dart';
part 'entity_widgets/common/badge.dart'; part 'entity_widgets/common/badge.dart';
part 'entity_widgets/model_widgets.dart'; part 'entity_widgets/model_widgets.dart';
part 'entity_widgets/default_entity_container.dart'; part 'entity_widgets/default_entity_container.dart';
part 'entity_widgets/missed_entity.dart';
part 'entity_widgets/glance_entity_container.dart'; part 'entity_widgets/glance_entity_container.dart';
part 'entity_widgets/button_entity_container.dart'; part 'entity_widgets/button_entity_container.dart';
part 'entity_widgets/common/entity_attributes_list.dart'; part 'entity_widgets/common/entity_attributes_list.dart';
@ -65,6 +68,7 @@ part 'entity_widgets/controls/slider_controls.dart';
part 'entity_widgets/state/text_input_state.dart'; part 'entity_widgets/state/text_input_state.dart';
part 'entity_widgets/state/select_state.dart'; part 'entity_widgets/state/select_state.dart';
part 'entity_widgets/state/simple_state.dart'; part 'entity_widgets/state/simple_state.dart';
part 'entity_widgets/state/timer_state.dart';
part 'entity_widgets/state/climate_state.dart'; part 'entity_widgets/state/climate_state.dart';
part 'entity_widgets/state/cover_state.dart'; part 'entity_widgets/state/cover_state.dart';
part 'entity_widgets/state/date_time_state.dart'; part 'entity_widgets/state/date_time_state.dart';
@ -76,7 +80,7 @@ part 'entity_widgets/controls/media_player_widgets.dart';
part 'entity_widgets/controls/fan_controls.dart'; part 'entity_widgets/controls/fan_controls.dart';
part 'entity_widgets/controls/alarm_control_panel_controls.dart'; part 'entity_widgets/controls/alarm_control_panel_controls.dart';
part 'settings.page.dart'; part 'settings.page.dart';
part 'configuration.page.dart'; part 'panel.page.dart';
part 'home_assistant.class.dart'; part 'home_assistant.class.dart';
part 'log.page.dart'; part 'log.page.dart';
part 'entity.page.dart'; part 'entity.page.dart';
@ -87,14 +91,16 @@ part 'ui_class/ui.dart';
part 'ui_class/view.class.dart'; part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart'; part 'ui_class/card.class.dart';
part 'ui_class/sizes_class.dart'; part 'ui_class/sizes_class.dart';
part 'ui_class/panel_class.dart';
part 'ui_widgets/view.dart'; part 'ui_widgets/view.dart';
part 'ui_widgets/card_widget.dart'; part 'ui_widgets/card_widget.dart';
part 'ui_widgets/card_header_widget.dart'; part 'ui_widgets/card_header_widget.dart';
part 'ui_widgets/config_panel_widget.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
const String appName = "HA Client"; const String appName = "HA Client";
const appVersion = "0.4.4"; const appVersion = "0.5.4";
String homeAssistantWebHost; String homeAssistantWebHost;
@ -130,7 +136,7 @@ class HAClientApp extends StatelessWidget {
routes: { routes: {
"/": (context) => MainPage(title: 'HA Client'), "/": (context) => MainPage(title: 'HA Client'),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/configuration": (context) => ConfigurationPage(title: "Configuration"), "/configuration": (context) => PanelPage(title: "Configuration"),
"/log-view": (context) => LogViewPage(title: "Log") "/log-view": (context) => LogViewPage(title: "Log")
}, },
); );
@ -146,7 +152,7 @@ class MainPage extends StatefulWidget {
_MainPageState createState() => new _MainPageState(); _MainPageState createState() => new _MainPageState();
} }
class _MainPageState extends State<MainPage> with WidgetsBindingObserver { class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
HomeAssistant _homeAssistant; HomeAssistant _homeAssistant;
//Map _instanceConfig; //Map _instanceConfig;
String _webSocketApiEndpoint; String _webSocketApiEndpoint;
@ -161,6 +167,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
bool _settingsLoaded = false; bool _settingsLoaded = false;
bool _accountMenuExpanded = false; bool _accountMenuExpanded = false;
bool _useLovelaceUI; bool _useLovelaceUI;
int _previousViewCount;
@override @override
void initState() { void initState() {
@ -255,6 +262,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_showInfoBottomBar(progress: true,); _showInfoBottomBar(progress: true,);
await _homeAssistant.fetch().then((result) { await _homeAssistant.fetch().then((result) {
_hideBottomBar(); _hideBottomBar();
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) { }).catchError((e) {
_setErrorState(e); _setErrorState(e);
}); });
@ -340,15 +353,28 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Divider(), Divider(),
]); ]);
} else { } else {
if (_homeAssistant != null && _homeAssistant.panels.isNotEmpty) {
_homeAssistant.panels.forEach((Panel panel) {
if (!panel.isHidden) {
menuItems.add(
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
title: Text("${panel.title}"),
onTap: () => panel.handleOpen(context)
)
);
}
});
menuItems.addAll([ menuItems.addAll([
new ListTile( new ListTile(
leading: Icon(Icons.settings), leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
title: Text("Configuration"), title: Text("Open Web UI"),
onTap: () { onTap: () => HAUtils.launchURL(homeAssistantWebHost),
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/configuration');
},
), ),
Divider()
]);
}
menuItems.addAll([
new ListTile( new ListTile(
leading: Icon(Icons.insert_drive_file), leading: Icon(Icons.insert_drive_file),
title: Text("Log"), title: Text("Log"),
@ -358,7 +384,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}, },
), ),
new ListTile( new ListTile(
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")), leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
title: Text("Report an issue"), title: Text("Report an issue"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -366,6 +392,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}, },
), ),
Divider(), Divider(),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
title: Text("Join Discord server"),
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("https://discord.gg/AUzEvwn");
},
),
new AboutListTile( new AboutListTile(
aboutBoxChildren: <Widget>[ aboutBoxChildren: <Widget>[
GestureDetector( GestureDetector(
@ -380,6 +414,38 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
decoration: TextDecoration.underline decoration: TextDecoration.underline
), ),
), ),
),
Container(
height: 10.0,
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/terms_and_conditions");
},
child: Text(
"Terms and Conditions",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
),
Container(
height: 10.0,
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/privacy_policy");
},
child: Text(
"Privacy Policy",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
) )
], ],
applicationName: appName, applicationName: appName,
@ -529,7 +595,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""), title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,), "mdi:dots-vertical"), color: Colors.white,),
onPressed: () { onPressed: () {
showMenu( showMenu(
@ -557,6 +623,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}, },
), ),
bottom: empty ? null : TabBar( bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(), tabs: buildUIViewTabs(),
isScrollable: true, isScrollable: true,
), ),
@ -570,7 +637,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant"),
size: 100.0, size: 100.0,
color: Colors.blue, color: Colors.blue,
), ),
@ -578,10 +645,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
), ),
) )
: :
_homeAssistant.buildViews(context, _useLovelaceUI), _homeAssistant.buildViews(context, _useLovelaceUI, _viewsTabController),
); );
} }
TabController _viewsTabController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget bottomBar; Widget bottomBar;
@ -647,9 +716,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
drawer: _buildAppDrawer(), drawer: _buildAppDrawer(),
primary: false, primary: false,
bottomNavigationBar: bottomBar, bottomNavigationBar: bottomBar,
body: DefaultTabController( body: HomeAssistantModel(
length: _homeAssistant.ui?.views?.length ?? 0,
child: _buildScaffoldBody(false), child: _buildScaffoldBody(false),
homeAssistant: _homeAssistant
), ),
); );
} }
@ -658,6 +727,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_viewsTabController.dispose();
if (_stateSubscription != null) _stateSubscription.cancel(); if (_stateSubscription != null) _stateSubscription.cancel();
if (_settingsSubscription != null) _settingsSubscription.cancel(); if (_settingsSubscription != null) _settingsSubscription.cancel();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();

File diff suppressed because it is too large Load Diff

40
lib/panel.page.dart Normal file
View File

@ -0,0 +1,40 @@
part of 'main.dart';
class PanelPage extends StatefulWidget {
PanelPage({Key key, this.title, this.panel}) : super(key: key);
final String title;
final Panel panel;
@override
_PanelPageState createState() => new _PanelPageState();
}
class _PanelPageState extends State<PanelPage> {
List<ConfigurationItem> _items;
@override
void initState() {
super.initState();
}
@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: widget.panel.getWidget(),
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -0,0 +1,57 @@
part of '../main.dart';
class Panel {
static const iconsByComponent = {
"config": "mdi:settings",
"history": "mdi:poll-box",
"map": "mdi:tooltip-account",
"logbook": "mdi:format-list-bulleted-type",
"custom": "mdi:home-assistant"
};
final String id;
final String type;
final String title;
final String urlPath;
final Map config;
String icon;
bool isHidden = true;
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
if (icon == null || !icon.startsWith("mdi:")) {
icon = Panel.iconsByComponent[type];
}
isHidden = (type != "iframe" && type != "config");
}
void handleOpen(BuildContext context) {
if (type == "iframe") {
Logger.d("Launching custom tab with ${config["url"]}");
HAUtils.launchURLInCustomTab(context, config["url"]);
} else if (type == "config") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this),
)
);
} else {
String url = "$homeAssistantWebHost/$urlPath";
Logger.d("Launching custom tab with $url");
HAUtils.launchURLInCustomTab(context, url);
}
}
Widget getWidget() {
switch (type) {
case "config": {
return ConfigPanelWidget();
}
default: {
return Text("Unsupported panel component: $type");
}
}
}
}

View File

@ -8,8 +8,9 @@ class HomeAssistantUI {
views = []; views = [];
} }
Widget build(BuildContext context) { Widget build(BuildContext context, TabController tabController) {
return TabBarView( return TabBarView(
controller: tabController,
children: _buildViews(context) children: _buildViews(context)
); );
} }

View File

@ -77,7 +77,7 @@ class HAView {
Tab( Tab(
icon: icon:
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
iconName ?? "mdi:home-assistant"), iconName ?? "mdi:home-assistant"),
size: 24.0, size: 24.0,
) )
@ -85,21 +85,21 @@ class HAView {
} else { } else {
return return
Tab( Tab(
text: name.toUpperCase(), text: "${name?.toUpperCase()}",
); );
} }
} else { } else {
if (linkedEntity.icon != null && linkedEntity.icon.length > 0) { if (linkedEntity.icon != null && linkedEntity.icon.length > 0) {
return Tab( return Tab(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
linkedEntity.icon), linkedEntity.icon),
size: 24.0, size: 24.0,
) )
); );
} else { } else {
return Tab( return Tab(
text: linkedEntity.displayName.toUpperCase(), text: "${linkedEntity?.displayName?.toUpperCase()}",
); );
} }

View File

@ -11,9 +11,18 @@ class CardWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if ((card.linkedEntityWrapper!= null) && (card.linkedEntityWrapper.entity.isHidden)) { if (card.linkedEntityWrapper!= null) {
if (card.linkedEntityWrapper.entity.isHidden) {
return Container(width: 0.0, height: 0.0,); return Container(width: 0.0, height: 0.0,);
} }
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: MissedEntityWidget(),
handleTap: false,
);
}
}
switch (card.type) { switch (card.type) {
@ -103,7 +112,7 @@ class CardWidget extends StatelessWidget {
if (!entity.entity.isHidden) { if (!entity.entity.isHidden) {
body.add( body.add(
Padding( Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), padding: EdgeInsets.fromLTRB(10.0, 4.0, 0.0, 4.0),
child: EntityModel( child: EntityModel(
entityWrapper: entity, entityWrapper: entity,
handleTap: true, handleTap: true,
@ -133,9 +142,6 @@ class CardWidget extends StatelessWidget {
} }
Widget _buildAlarmPanelCard(BuildContext context) { Widget _buildAlarmPanelCard(BuildContext context) {
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) {
return Container(width: 0, height: 0,);
} else {
List<Widget> body = []; List<Widget> body = [];
body.add(CardHeaderWidget( body.add(CardHeaderWidget(
name: card.name ?? "", name: card.name ?? "",
@ -149,14 +155,14 @@ class CardWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
EntityIcon( EntityIcon(
iconSize: 50.0, size: 50.0,
), ),
Container( Container(
width: 26.0, width: 26.0,
child: IconButton( child: IconButton(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
) )
@ -182,7 +188,6 @@ class CardWidget extends StatelessWidget {
) )
); );
} }
}
Widget _buildGlanceCard(BuildContext context) { Widget _buildGlanceCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow(); List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
@ -227,9 +232,6 @@ class CardWidget extends StatelessWidget {
} }
Widget _buildMediaControlsCard(BuildContext context) { Widget _buildMediaControlsCard(BuildContext context) {
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) {
return Container(width: 0, height: 0,);
} else {
return Card( return Card(
child: EntityModel( child: EntityModel(
entityWrapper: card.linkedEntityWrapper, entityWrapper: card.linkedEntityWrapper,
@ -238,14 +240,10 @@ class CardWidget extends StatelessWidget {
) )
); );
} }
}
Widget _buildEntityButtonCard(BuildContext context) { Widget _buildEntityButtonCard(BuildContext context) {
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) {
return Container(width: 0, height: 0,);
} else {
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ?? card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName.toUpperCase(); card.linkedEntityWrapper.displayName?.toUpperCase();
return Card( return Card(
child: EntityModel( child: EntityModel(
entityWrapper: card.linkedEntityWrapper, entityWrapper: card.linkedEntityWrapper,
@ -254,7 +252,6 @@ class CardWidget extends StatelessWidget {
) )
); );
} }
}
Widget _buildUnsupportedCard(BuildContext context) { Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = []; List<Widget> body = [];

View File

@ -1,12 +1,10 @@
part of 'main.dart'; part of '../main.dart';
class ConfigurationPage extends StatefulWidget { class ConfigPanelWidget extends StatefulWidget {
ConfigurationPage({Key key, this.title}) : super(key: key); ConfigPanelWidget({Key key}) : super(key: key);
final String title;
@override @override
_ConfigurationPageState createState() => new _ConfigurationPageState(); _ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState();
} }
class ConfigurationItem { class ConfigurationItem {
@ -17,7 +15,7 @@ class ConfigurationItem {
final Widget body; final Widget body;
} }
class _ConfigurationPageState extends State<ConfigurationPage> { class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
List<ConfigurationItem> _items; List<ConfigurationItem> _items;
@ -64,14 +62,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return ListView(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text(widget.title),
),
body: ListView(
children: [ children: [
new ExpansionPanelList( new ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) { expansionCallback: (int index, bool isExpanded) {
@ -94,7 +85,6 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
}).toList(), }).toList(),
), ),
], ],
),
); );
} }

View File

@ -47,12 +47,44 @@ class Logger {
class HAUtils { class HAUtils {
static void launchURL(String url) async { static void launchURL(String url) async {
if (await canLaunch(url)) { if (await urlLauncher.canLaunch(url)) {
await launch(url); await urlLauncher.launch(url);
} else { } else {
Logger.e( "Could not launch $url"); Logger.e( "Could not launch $url");
} }
} }
static void launchURLInCustomTab(BuildContext context, String url) async {
try {
await launch(
"$url",
option: new CustomTabsOption(
toolbarColor: Theme.of(context).primaryColor,
enableDefaultShare: true,
enableUrlBarHiding: true,
showPageTitle: true,
animation: new CustomTabsAnimation.slideIn()
// or user defined animation.
/*animation: new CustomTabsAnimation(
startEnter: 'slide_up',
startExit: 'android:anim/fade_out',
endEnter: 'android:anim/fade_in',
endExit: 'slide_down',
)*/,
extraCustomTabs: <String>[
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
'org.mozilla.firefox',
// ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx
'com.microsoft.emmx',
],
),
);
} catch (e) {
Logger.w("Can't open custom tab: ${e.toString()}");
Logger.w("Launching in default browser");
HAUtils.launchURL(url);
}
}
} }
class StateChangedEvent { class StateChangedEvent {

View File

@ -35,7 +35,7 @@ packages:
name: cached_network_image name: cached_network_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.0+1" version: "0.8.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -100,7 +100,7 @@ packages:
name: event_bus name: event_bus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.1.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -112,7 +112,14 @@ packages:
name: flutter_cache_manager name: flutter_cache_manager
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.0" version: "0.3.2"
flutter_custom_tabs:
dependency: "direct main"
description:
name: flutter_custom_tabs
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -133,7 +140,7 @@ packages:
name: flutter_svg name: flutter_svg
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.10.3" version: "0.10.4"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -145,7 +152,7 @@ packages:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.0+1" version: "0.12.0+2"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -159,14 +166,14 @@ packages:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.7" version: "2.0.8"
intl: intl:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.15.7" version: "0.15.8"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -180,7 +187,7 @@ packages:
name: markdown name: markdown
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -208,14 +215,14 @@ packages:
name: path_drawing name: path_drawing
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.0" version: "0.4.1"
path_parsing: path_parsing:
dependency: transitive dependency: transitive
description: description:
name: path_parsing name: path_parsing
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.3" version: "0.1.4"
path_provider: path_provider:
dependency: transitive dependency: transitive
description: description:
@ -223,6 +230,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.0+1" version: "0.5.0+1"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -250,7 +264,7 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.1+1" version: "0.5.2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -262,14 +276,14 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.1" version: "1.5.4"
sqflite: sqflite:
dependency: transitive dependency: transitive
description: description:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0+1" version: "1.1.5"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -297,21 +311,21 @@ packages:
name: synchronized name: synchronized
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2+1" version: "2.1.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.1.0"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.1" version: "0.2.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -325,14 +339,14 @@ packages:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.1" version: "5.0.2"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
name: uuid name: uuid
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -346,7 +360,7 @@ packages:
name: web_socket_channel name: web_socket_channel
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.9" version: "1.0.12"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@ -363,4 +377,4 @@ packages:
version: "2.1.15" version: "2.1.15"
sdks: sdks:
dart: ">=2.1.0 <3.0.0" dart: ">=2.1.0 <3.0.0"
flutter: ">=0.7.3 <2.0.0" flutter: ">=1.2.1 <2.0.0"

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 0.4.4+95 version: 0.5.4+541
environment: environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0" sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -19,6 +19,7 @@ dependencies:
charts_flutter: any charts_flutter: any
flutter_markdown: any flutter_markdown: any
flutter_svg: ^0.10.3 flutter_svg: ^0.10.3
flutter_custom_tabs: ^0.6.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -59,7 +60,7 @@ flutter:
fonts: fonts:
- family: "Material Design Icons" - family: "Material Design Icons"
fonts: fonts:
- asset: fonts/materialdesignicons-webfont.ttf - asset: fonts/materialdesignicons-webfont-3-5-95.ttf
# fonts: # fonts:
# - family: Schyler # - family: Schyler
# fonts: # fonts:

File diff suppressed because one or more lines are too long