Compare commits

...

36 Commits

Author SHA1 Message Date
c844e21e76 WIP: Location tracking using foreground service 2020-05-12 22:18:41 +00:00
24d42c9597 Fix badge entity_picture size 2020-05-09 20:47:28 +00:00
9078ad81e8 1100 2020-05-09 18:47:30 +00:00
7cba6c8a10 Revert state filter as map fix 2020-05-09 18:46:53 +00:00
c1f9c8c16d Fix for states in state filter not strings 2020-05-09 18:28:06 +00:00
1d1d132b33 Resolves #530 Badges refactoring 2020-05-09 18:08:42 +00:00
e258b3bc2c Avoid null for options in ModeSelectorWidget 2020-05-09 15:20:01 +00:00
13508ee92f WIP #530 Badges refactoring 2020-05-09 13:38:05 +00:00
4fbf58e707 Change default hold action to more-info 2020-05-09 11:39:53 +00:00
a3442f84ca Glance card padding fixes 2020-05-08 14:23:35 +00:00
6a6ab3b2cb Fix some padding inside cards 2020-05-08 13:59:04 +00:00
d9fa553e2f Fix cions 2020-05-08 13:44:08 +00:00
cde5d9b912 Fix cions 2020-05-08 13:42:25 +00:00
3468446b5b Increase auth callback timeout for authenticated webview 2020-05-08 13:36:57 +00:00
326434273a Disable external bus for authenticated webview 2020-05-08 13:29:54 +00:00
470d3be946 Fix for empty stack cards 2020-05-08 13:20:01 +00:00
d1032be6a6 Fix getting lovelace config for HA < 0.107 2020-05-08 12:59:06 +00:00
cffac8e1f8 Safe display name parse 2020-05-08 12:55:44 +00:00
870bc25dd9 Timer duration parsing error report improvement 2020-05-08 12:47:43 +00:00
de713024f6 Safe icon parse 2020-05-08 12:37:53 +00:00
4d4add4581 Dont parse castom cards. Hide unsupported 2020-05-08 12:36:13 +00:00
1670c8e505 Hide unsupported cards 2020-05-07 16:14:23 +00:00
55eb1b5125 Fix mistakes handling in gauge card 2020-05-07 15:48:20 +00:00
dbeaaaf91e Fix null data referense for panels 2020-05-07 15:38:38 +00:00
8166d8ce6d Fix null data referense 2020-05-07 15:33:45 +00:00
35bcf0c1fa Fix nu data referense 2020-05-07 15:31:18 +00:00
9c1d240962 1013 2020-05-06 19:05:11 +00:00
a76652b552 1012 2020-05-06 18:46:05 +00:00
a140f993d0 1011 2020-05-06 18:23:01 +00:00
ded60a2867 not null 2020-05-06 18:22:17 +00:00
b86602bcdb app version and whats new 2020-05-06 18:19:41 +00:00
02ea45469f 1.0.1 2020-05-06 17:52:31 +00:00
90105c3b09 Cache reading issues fix 2020-05-06 17:51:06 +00:00
3d828914cc Fix error handling 2020-05-06 17:24:13 +00:00
8cd5776bc6 Make integration work with HA < 0.104 2020-05-06 16:40:19 +00:00
17ec73b176 Report foreground location errors 2020-05-06 16:19:42 +00:00
29 changed files with 775 additions and 475 deletions

View File

@ -5,8 +5,13 @@
android:required="false" />
<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_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!--
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
-->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
@ -53,6 +58,17 @@
</intent-filter>
</activity>
<receiver android:name="rekab.app.background_locator.LocatorBroadcastReceiver"
android:enabled="true"
android:exported="true"/>
<service android:name="rekab.app.background_locator.LocatorService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
<service android:name="rekab.app.background_locator.IsolateHolderService"
android:permission="android.permission.FOREGROUND_SERVICE"
android:exported="true"/>
<!--
<service
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"
@ -67,5 +83,6 @@
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
</intent-filter>
</receiver>
-->
</application>
</manifest>

View File

@ -11,9 +11,10 @@ window.externalApp.getExternalAuth = function(options) {
setTimeout(function(){
console.log("Calling a callback");
window[options.callback](true, responseData);
}, 500);
}, 900);
}
};
/*
window.externalApp.externalBus = function(message) {
console.log("External bus message: " + message);
var messageObj = JSON.parse(message);
@ -32,4 +33,5 @@ window.externalApp.externalBus = function(message) {
} else if (messageObj.type == "config_screen/show") {
HAClient.postMessage('show-settings');
}
};
};
*/

196
lib/cards/badges.dart Normal file
View File

@ -0,0 +1,196 @@
part of '../main.dart';
class Badges extends StatelessWidget {
final BadgesData badges;
const Badges({Key key, this.badges}) : super(key: key);
@override
Widget build(BuildContext context) {
List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow();
if (entitiesToShow.isNotEmpty) {
if (ConnectionManager().scrollBadges) {
return ConstrainedBox(
constraints: BoxConstraints.tightFor(height: 112),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: entitiesToShow.map((entity) =>
EntityModel(
entityWrapper: entity,
child: Padding(
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
child: BadgeWidget(),
),
handleTap: true,
)).toList()
),
)
);
} else {
return Padding(
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 5,
children: entitiesToShow.map((entity) =>
EntityModel(
entityWrapper: entity,
child: BadgeWidget(),
handleTap: true,
)).toList(),
)
);
}
}
return Container(height: 0.0, width: 0.0,);
}
}
class BadgeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
Widget badgeIcon;
String onBadgeTextValue;
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
switch (entityModel.entityWrapper.entity.domain) {
case "sun":
{
IconData iconData;
if (entityModel.entityWrapper.entity.state == "below_horizon") {
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc);
} else {
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8);
}
badgeIcon = Padding(
padding: EdgeInsets.all(10),
child: Icon(
iconData,
)
);
break;
}
case "camera":
case "media_player":
case "binary_sensor":
{
badgeIcon = EntityIcon(
imagePadding: EdgeInsets.all(0.0),
iconPadding: EdgeInsets.all(10),
color: Theme.of(context).textTheme.body2.color
);
break;
}
case "device_tracker":
case "person":
{
badgeIcon = EntityIcon(
imagePadding: EdgeInsets.all(0.0),
iconPadding: EdgeInsets.all(10),
color: Theme.of(context).textTheme.body2.color
);
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
break;
}
default:
{
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
badgeIcon = Padding(
padding: EdgeInsets.all(4),
child: Text(
"${entityModel.entityWrapper.entity.displayState}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.body1
)
);
break;
}
}
Widget onBadgeText;
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
onBadgeText = Container(width: 0.0, height: 0.0);
} else {
onBadgeText = Container(
constraints: BoxConstraints(maxWidth: 50),
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$onBadgeTextValue",
style: Theme.of(context).textTheme.overline.copyWith(
color: HAClientTheme().getOnBadgeTextColor()
),
textAlign: TextAlign.center,
softWrap: false,
overflow: TextOverflow.ellipsis
),
decoration: new BoxDecoration(
color: iconColor,
borderRadius: BorderRadius.circular(9.0),
)
);
}
return GestureDetector(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Stack(
overflow: Overflow.visible,
alignment: Alignment.center,
children: <Widget>[
Container(
width: 45,
height: 45,
decoration: new BoxDecoration(
// Circle shape
shape: BoxShape.circle,
color: Theme.of(context).cardColor,
// The border you want
border: Border.all(
width: 2.0,
color: iconColor,
),
),
),
SizedBox(
width: 41,
height: 41,
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: badgeIcon,
)
),
Positioned(
bottom: -6,
child: onBadgeText
)
],
),
Container(
constraints: BoxConstraints(maxWidth: 45),
padding: EdgeInsets.only(top: 10),
child: Text(
"${entityModel.entityWrapper.displayName}",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.caption.copyWith(
fontSize: 10
),
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
)
],
),
onTap: () => entityModel.entityWrapper.handleTap(),
onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(),
onLongPress: () => entityModel.entityWrapper.handleHold(),
);
}
}

View File

@ -13,17 +13,32 @@ class CardData {
factory CardData.parse(rawData) {
try {
if (rawData['type'] == null) {
rawData['type'] = CardType.ENTITIES;
} else if (!(rawData['type'] is String)) {
return CardData(null);
}
switch (rawData['type']) {
case CardType.ENTITIES:
case CardType.HISTORY_GRAPH:
case CardType.MAP:
case CardType.PICTURE_GLANCE:
case CardType.SENSOR:
case CardType.ENTITY:
case CardType.WEATHER_FORECAST:
case CardType.PLANT_STATUS:
if (rawData['entity'] != null) {
rawData['entities'] = [rawData['entity']];
}
return EntitiesCardData(rawData);
break;
case CardType.ALARM_PANEL:
return AlarmPanelCardData(rawData);
break;
case CardType.BUTTON:
return ButtonCardData(rawData);
break;
case CardType.ENTITY_BUTTON:
case CardType.LIGHT:
case CardType.BUTTON:
case CardType.PICTURE_ENTITY:
return ButtonCardData(rawData);
break;
case CardType.CONDITIONAL:
@ -42,6 +57,10 @@ class CardData {
return GaugeCardData(rawData);
break;
case CardType.GLANCE:
case CardType.THERMOSTAT:
if (rawData['entity'] != null) {
rawData['entities'] = [rawData['entity']];
}
return GlanceCardData(rawData);
break;
case CardType.HORIZONTAL_STACK:
@ -56,14 +75,11 @@ class CardData {
case CardType.MEDIA_CONTROL:
return MediaControlCardData(rawData);
break;
case CardType.BADGES:
return BadgesData(rawData);
break;
default:
if (rawData.containsKey('entities')) {
return EntitiesCardData(rawData);
} else if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
return EntitiesCardData(rawData);
}
return CardData(rawData);
return CardData(null);
}
} catch (error, stacktrace) {
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
@ -73,10 +89,10 @@ class CardData {
CardData(rawData) {
if (rawData != null && rawData is Map) {
type = rawData['type'] ?? CardType.ENTITIES;
type = rawData['type'];
conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true;
stateFilter = rawData['state_filter'] ?? [];
stateFilter = rawData['state_filter'] ?? [];
} else {
type = CardType.UNKNOWN;
conditions = [];
@ -159,6 +175,64 @@ class CardData {
}
class BadgesData extends CardData {
String title;
String icon;
bool showHeaderToggle;
@override
Widget buildCardWidget() {
return Badges(badges: this);
}
BadgesData(rawData) : super(rawData) {
if (rawData['badges'] is List) {
rawData['badges'].forEach((dynamic rawBadge) {
if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge)));
} else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) {
entities.add(
EntityWrapper(
entity: HomeAssistant().entities.get(rawBadge['entity']),
overrideName: rawBadge["name"],
overrideIcon: rawBadge["icon"],
)
);
} else if (rawBadge is Map && rawBadge.containsKey('entities')) {
_parseEntities(rawBadge);
}
});
}
}
void _parseEntities(rawData) {
var rawEntities = rawData['entities'] ?? [];
rawEntities.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(rawEntity),
stateFilter: rawData['state_filter'] ?? [],
));
}
} else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
entities.add(
EntityWrapper(
entity: e,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
}
});
}
}
class EntitiesCardData extends CardData {
String title;
@ -172,12 +246,12 @@ class EntitiesCardData extends CardData {
EntitiesCardData(rawData) : super(rawData) {
//Parsing card data
title = rawData["title"];
icon = rawData['icon'];
title = rawData['title'];
icon = rawData['icon'] is String ? rawData['icon'] : null;
stateColor = rawData['state_color'] ?? false;
showHeaderToggle = rawData['show_header_toggle'] ?? false;
//Parsing entities
var rawEntities = rawData["entities"] ?? [];
var rawEntities = rawData['entities'] ?? [];
rawEntities.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
@ -299,7 +373,7 @@ class ButtonCardData extends CardData {
ButtonCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
icon = rawData['icon'];
icon = rawData['icon'] is String ? rawData['icon'] : null;
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
stateColor = rawData['state_color'] ?? true;
@ -379,7 +453,7 @@ class GaugeCardData extends CardData {
}
severity = rawData['severity'];
//Parsing entity
var entitiId = rawData["entity"];
var entitiId = rawData["entity"] is List ? rawData["entity"][0] : rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
@ -390,7 +464,7 @@ class GaugeCardData extends CardData {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
}
}
@ -460,7 +534,7 @@ class HorizontalStackCardData extends CardData {
}
HorizontalStackCardData(rawData) : super(rawData) {
if (rawData.containsKey('cards')) {
if (rawData.containsKey('cards') && rawData['cards'] is List) {
childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard);
}).toList();
@ -481,7 +555,7 @@ class VerticalStackCardData extends CardData {
}
VerticalStackCardData(rawData) : super(rawData) {
if (rawData.containsKey('cards')) {
if (rawData.containsKey('cards') && rawData['cards'] is List) {
childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard);
}).toList();

View File

@ -62,13 +62,16 @@ class EntityButtonCard extends StatelessWidget {
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buttonIcon,
_buildName(context)
],
child: Padding(
padding: EdgeInsets.only(top: 5),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buttonIcon,
_buildName(context)
],
)
)
),
),

View File

@ -30,10 +30,13 @@ class GlanceCard extends StatelessWidget {
start, end
).map(
(EntityWrapper entity){
return EntityModel(
entityWrapper: entity,
child: _buildEntityContainer(context, entity),
handleTap: true
return Padding(
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
child: EntityModel(
entityWrapper: entity,
child: _buildEntityContainer(context, entity),
handleTap: true
)
);
}
).toList()
@ -50,17 +53,22 @@ class GlanceCard extends StatelessWidget {
);
}
return CardWrapper(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardHeader(name: card.title),
Padding(
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
child: Table(
children: rows
)
child: Center(
child: Padding(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardHeader(
name: card.title,
emptyPadding: Sizes.rowPadding,
),
Table(
children: rows
)
],
)
],
)
)
);
}
@ -85,16 +93,14 @@ class GlanceCard extends StatelessWidget {
result.add(_buildState());
}
return Center(
child: InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
return InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
);
}

View File

@ -7,11 +7,6 @@ class UnsupportedCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CardWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
)
);
return Container();
}
}

View File

@ -36,66 +36,6 @@ class EntityState {
}
class EntityUIAction {
static const moreInfo = 'more-info';
static const toggle = 'toggle';
static const callService = 'call-service';
static const navigate = 'navigate';
static const none = 'none';
String tapAction = EntityUIAction.moreInfo;
String tapNavigationPath;
String tapService;
Map<String, dynamic> tapServiceData;
String holdAction = EntityUIAction.none;
String holdNavigationPath;
String holdService;
Map<String, dynamic> holdServiceData;
String doubleTapAction = EntityUIAction.none;
String doubleTapNavigationPath;
String doubleTapService;
Map<String, dynamic> doubleTapServiceData;
EntityUIAction({rawEntityData}) {
if (rawEntityData != null) {
if (rawEntityData["tap_action"] != null) {
if (rawEntityData["tap_action"] is String) {
tapAction = rawEntityData["tap_action"];
} else {
tapAction =
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
tapService = rawEntityData["tap_action"]["service"];
tapServiceData = rawEntityData["tap_action"]["service_data"];
}
}
if (rawEntityData["hold_action"] != null) {
if (rawEntityData["hold_action"] is String) {
holdAction = rawEntityData["hold_action"];
} else {
holdAction =
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
holdService = rawEntityData["hold_action"]["service"];
holdServiceData = rawEntityData["hold_action"]["service_data"];
}
}
if (rawEntityData["double_tap_action"] != null) {
if (rawEntityData["double_tap_action"] is String) {
doubleTapAction = rawEntityData["double_tap_action"];
} else {
doubleTapAction =
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
doubleTapService = rawEntityData["double_tap_action"]["service"];
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
}
}
}
}
}
class CardType {
static const HORIZONTAL_STACK = "horizontal-stack";
static const VERTICAL_STACK = "vertical-stack";
@ -113,6 +53,7 @@ class CardType {
static const IFRAME = "iframe";
static const GAUGE = "gauge";
static const ENTITY_BUTTON = "entity-button";
static const ENTITY = "entity";
static const BUTTON = "button";
static const CONDITIONAL = "conditional";
static const ALARM_PANEL = "alarm-panel";
@ -120,6 +61,9 @@ class CardType {
static const LIGHT = "light";
static const ENTITY_FILTER = "entity-filter";
static const UNKNOWN = "unknown";
static const HISTORY_GRAPH = "history-graph";
static const PICTURE_GLANCE = "picture-glance";
static const BADGES = "badges";
}
class Sizes {

View File

@ -1,148 +0,0 @@
part of '../main.dart';
class BadgeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
double iconSize = 26.0;
Widget badgeIcon;
String onBadgeTextValue;
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
switch (entityModel.entityWrapper.entity.domain) {
case "sun":
{
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
? Icon(
MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
size: iconSize,
)
: Icon(
MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
size: iconSize,
);
break;
}
case "camera":
case "media_player":
case "binary_sensor":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Theme.of(context).textTheme.body1.color
);
break;
}
case "device_tracker":
case "person":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Theme.of(context).textTheme.body1.color
);
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
break;
}
default:
{
double stateFontSize;
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
stateFontSize = 18.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
stateFontSize = 15.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
stateFontSize = 10.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
stateFontSize = 8.0;
}
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${entityModel.entityWrapper.entity.displayState}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.body1.copyWith(
fontSize: stateFontSize
)
),
);
break;
}
}
Widget onBadgeText;
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
onBadgeText = Container(width: 0.0, height: 0.0);
} else {
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()
),
textAlign: TextAlign.center,
softWrap: false,
overflow: TextOverflow.fade),
decoration: new BoxDecoration(
// Circle shape
//shape: BoxShape.circle,
color: iconColor,
borderRadius: BorderRadius.circular(9.0),
));
}
return GestureDetector(
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
width: 50.0,
height: 50.0,
decoration: new BoxDecoration(
// Circle shape
shape: BoxShape.circle,
color: Theme.of(context).cardColor,
// The border you want
border: new Border.all(
width: 2.0,
color: iconColor,
),
),
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
width: 46.0,
height: 46.0,
top: 0.0,
left: 0.0,
child: badgeIcon,
),
Positioned(
//width: 50.0,
bottom: -9.0,
left: -10.0,
right: -10.0,
child: Center(
child: onBadgeText,
))
],
),
),
Container(
width: 60.0,
child: Text(
"${entityModel.entityWrapper.displayName}",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.caption,
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entityId: entityModel.entityWrapper.entity.entityId)));
}
}

View File

@ -11,7 +11,7 @@ class ModeSelectorWidget extends StatelessWidget {
ModeSelectorWidget({
Key key,
@required this.caption,
@required this.options,
this.options: const [],
this.value,
@required this.onChange,
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),

View File

@ -79,7 +79,8 @@ class Entity {
);
String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
attributes["friendly_name"] ??
(attributes["name"] ?? (entityId != null && entityId.contains('.')) ? entityId.split(".")[1].replaceAll("_", " ") : "");
bool get isView =>
(domain == "group") &&
@ -209,14 +210,6 @@ class Entity {
);
}
Widget buildBadgeWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: BadgeWidget(),
handleTap: true,
);
}
String getAttribute(String attributeName) {
if (attributes != null) {
return attributes["$attributeName"].toString();

View File

@ -3,10 +3,12 @@ part of '../main.dart';
class EntityIcon extends StatelessWidget {
final EdgeInsetsGeometry padding;
final EdgeInsetsGeometry iconPadding;
final EdgeInsetsGeometry imagePadding;
final double size;
final Color color;
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key);
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
if (entityId == null) {
@ -26,76 +28,6 @@ class EntityIcon extends StatelessWidget {
}
}
Widget buildIcon(BuildContext context, EntityWrapper data, Color color) {
Widget result;
if (data == null) {
return null;
}
if (data.entityPicture != null) {
result = Container(
height: size+12,
width: size+12,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit:BoxFit.cover,
image: CachedNetworkImageProvider(
"${data.entityPicture}"
),
)
),
);
} else {
String iconName = data.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
result = Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
);
if (data.entity is LightEntity &&
(data.entity as LightEntity).supportColor &&
(data.entity as LightEntity).color != null
) {
Color lightColor = (data.entity as LightEntity).color.toColor();
if (lightColor == Colors.white) {
return result;
}
result = Stack(
children: <Widget>[
result,
Positioned(
bottom: 0,
right: 0,
child: Container(
width: size / 3,
height: size / 3,
decoration: BoxDecoration(
color: lightColor,
shape: BoxShape.circle,
boxShadow: <BoxShadow>[
BoxShadow(
spreadRadius: 0,
blurRadius: 0,
offset: Offset(0.3, 0.3)
)
]
),
),
)
],
);
}
}
return result;
}
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
@ -107,13 +39,92 @@ class EntityIcon extends StatelessWidget {
} else {
iconColor = HAClientTheme().getOffStateColor(context);
}
Widget iconWidget;
bool isPicture = false;
if (entityWrapper == null) {
iconWidget = Container(
width: size,
height: size,
);
} else {
if (entityWrapper.entityPicture != null) {
iconWidget = Container(
height: size+12,
width: size+12,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit:BoxFit.cover,
image: CachedNetworkImageProvider(
"${entityWrapper.entityPicture}"
),
)
),
);
isPicture = true;
} else {
String iconName = entityWrapper.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
}
if (entityWrapper.entity is LightEntity &&
(entityWrapper.entity as LightEntity).supportColor &&
(entityWrapper.entity as LightEntity).color != null &&
(entityWrapper.entity as LightEntity).color.toColor() != Colors.white
) {
Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();
iconWidget = Stack(
children: <Widget>[
Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: iconColor,
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: size / 3,
height: size / 3,
decoration: BoxDecoration(
color: lightColor,
shape: BoxShape.circle,
boxShadow: <BoxShadow>[
BoxShadow(
spreadRadius: 0,
blurRadius: 0,
offset: Offset(0.3, 0.3)
)
]
),
),
)
],
);
} else {
iconWidget = Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: iconColor,
);
}
}
}
EdgeInsetsGeometry computedPadding;
if (isPicture && imagePadding != null) {
computedPadding = imagePadding;
} else if (!isPicture && iconPadding != null) {
computedPadding = iconPadding;
} else {
computedPadding = padding;
}
return Padding(
padding: padding,
child: buildIcon(
context,
entityWrapper,
iconColor
),
padding: computedPadding,
child: iconWidget,
);
}
}

View File

@ -154,4 +154,64 @@ class EntityWrapper {
}
}
}
class EntityUIAction {
static const moreInfo = 'more-info';
static const toggle = 'toggle';
static const callService = 'call-service';
static const navigate = 'navigate';
static const none = 'none';
String tapAction = EntityUIAction.moreInfo;
String tapNavigationPath;
String tapService;
Map<String, dynamic> tapServiceData;
String holdAction = EntityUIAction.moreInfo;
String holdNavigationPath;
String holdService;
Map<String, dynamic> holdServiceData;
String doubleTapAction = EntityUIAction.none;
String doubleTapNavigationPath;
String doubleTapService;
Map<String, dynamic> doubleTapServiceData;
EntityUIAction({rawEntityData}) {
if (rawEntityData != null) {
if (rawEntityData["tap_action"] != null) {
if (rawEntityData["tap_action"] is String) {
tapAction = rawEntityData["tap_action"];
} else {
tapAction =
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
tapService = rawEntityData["tap_action"]["service"];
tapServiceData = rawEntityData["tap_action"]["service_data"];
}
}
if (rawEntityData["hold_action"] != null) {
if (rawEntityData["hold_action"] is String) {
holdAction = rawEntityData["hold_action"];
} else {
holdAction =
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
holdService = rawEntityData["hold_action"]["service"];
holdServiceData = rawEntityData["hold_action"]["service_data"];
}
}
if (rawEntityData["double_tap_action"] != null) {
if (rawEntityData["double_tap_action"] is String) {
doubleTapAction = rawEntityData["double_tap_action"];
} else {
doubleTapAction =
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
doubleTapService = rawEntityData["double_tap_action"]["service"];
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
}
}
}
}
}

View File

@ -26,11 +26,11 @@ class TimerEntity extends Entity {
seconds: int.tryParse(durationList[2]) ?? 0
);
} else {
Logger.e("Strange $entityId duration format: $durationSource");
Logger.e("Strange timer duration format: $durationSource");
duration = Duration(seconds: 0);
}
} catch (e, stacktrace) {
Logger.e("Error parsing duration for $entityId: $e", stacktrace: stacktrace);
Logger.e("Error parsing timer duration \'$durationSource\': $e", stacktrace: stacktrace);
duration = Duration(seconds: 0);
}
} else {

View File

@ -140,9 +140,9 @@ class HomeAssistant {
Future _getConfig(SharedPreferences sharedPrefs) async {
_instanceConfig?.clear();
if (sharedPrefs != null) {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_config')) {
try {
var data = json.decode(sharedPrefs.getString('cached_config') ?? '{}');
var data = json.decode(sharedPrefs.getString('cached_config'));
_parseConfig(data);
} catch (e, stacktrace) {
Logger.e('Error gettong config from cache: $e', stacktrace: stacktrace);
@ -160,10 +160,10 @@ class HomeAssistant {
}
Future _getStates(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) {
try {
var data = json.decode(sharedPrefs.getString('cached_states') ?? '[]');
_parseStates(data);
var data = json.decode(sharedPrefs.getString('cached_states'));
_parseStates(data ?? []);
} catch (e, stacktrace) {
Logger.e('Error getting cached states: $e', stacktrace: stacktrace);
}
@ -171,7 +171,7 @@ class HomeAssistant {
await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => _parseStates(data)
).catchError((e) {
Logger.e('get_states error: $e');
Logger.e('get_states error: $e');
throw HACException("Error getting states: $e");
});
}
@ -183,9 +183,9 @@ class HomeAssistant {
}
Future _getLovelace(SharedPreferences sharedPrefs) {
if (sharedPrefs != null) {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_lovelace')) {
try {
var data = json.decode(sharedPrefs.getString('cached_lovelace') ?? '{}');
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
_rawLovelaceData = data;
} catch (e) {
autoUi = true;
@ -194,7 +194,7 @@ class HomeAssistant {
} else {
Completer completer = Completer();
var additionalData;
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
if (ConnectionManager().haVersion >= 107 && _lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
additionalData = {
'url_path': _lovelaceDashbordUrl
};
@ -221,10 +221,10 @@ class HomeAssistant {
Future _getServices(SharedPreferences prefs) async {
services?.clear();
if (prefs != null) {
if (prefs != null && prefs.containsKey('cached_services')) {
try {
var data = json.decode(prefs.getString('cached_services') ?? '{}');
_parseServices(data);
var data = json.decode(prefs.getString('cached_services'));
_parseServices(data ?? {});
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
}
@ -240,9 +240,19 @@ class HomeAssistant {
Future _getUserInfo(SharedPreferences sharedPrefs) async {
_userName = null;
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
Logger.e('auth/current_user error: $e');
});
if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) {
try {
var data = json.decode(sharedPrefs.getString('cached_user'));
_parseUserInfo(data ?? {});
} catch (e, stacktrace) {
Logger.e('Error getting cached user info: $e', stacktrace: stacktrace);
}
return Future.value();
} else {
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
Logger.e('auth/current_user error: $e');
});
}
}
void _parseUserInfo(data) {
@ -251,10 +261,10 @@ class HomeAssistant {
}
Future _getPanels(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) {
try {
var data = json.decode(sharedPrefs.getString('cached_panels') ?? '{}');
_parsePanels(data);
var data = json.decode(sharedPrefs.getString('cached_panels'));
_parsePanels(data ?? {});
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
panels.clear();

View File

@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:async';
import 'dart:isolate';
import 'dart:ui';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
@ -23,8 +25,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart';
import 'package:workmanager/workmanager.dart' as workManager;
import 'package:geolocator/geolocator.dart';
//import 'package:workmanager/workmanager.dart' as workManager;
//import 'package:geolocator/geolocator.dart';
import 'package:background_locator/background_locator.dart';
import 'package:background_locator/location_dto.dart';
import 'package:background_locator/location_settings.dart';
import 'package:battery/battery.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
@ -58,7 +63,6 @@ part 'entities/fan/fan_entity.class.dart';
part 'entities/automation/automation_entity.class.dart';
part 'entities/camera/camera_entity.class.dart';
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
part 'entities/badge.widget.dart';
part 'entities/entity_model.widget.dart';
part 'entities/default_entity_container.widget.dart';
part 'entities/missed_entity.widget.dart';
@ -153,14 +157,16 @@ part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
part 'pages/whats_new.page.dart';
part 'pages/fullscreen.page.dart';
part 'popups.dart';
part 'cards/badges.dart';
EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client";
const appVersionNumber = "1.0.0";
const appVersionAdd = "";
const appVersion = "$appVersionNumber$appVersionAdd";
const String appName = 'HA Client';
const appVersionNumber = '1.0.1';
final String appVersionAdd = secrets['version_type'] ?? '';
final String appVersion = '$appVersionNumber${appVersionAdd.isNotEmpty ? '-' : ''}$appVersionAdd';
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.0.1.md';
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console.
@ -212,9 +218,12 @@ class _HAClientAppState extends State<HAClientApp> {
StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription;
StreamSubscription _themeChangeSubscription;
AppTheme _currentTheme = AppTheme.defaultTheme;
ReceivePort port = ReceivePort();
@override
void initState() {
InAppPurchaseConnection.enablePendingPurchases();
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
@ -227,11 +236,18 @@ class _HAClientAppState extends State<HAClientApp> {
_currentTheme = event.theme;
});
});
/*
workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
*/
super.initState();
IsolateNameServer.registerPortWithName(port.sendPort, LocationManager.isolateName);
port.listen((dynamic data) {
// do something with data
});
initPlatformState();
}
void _handlePurchaseUpdates(purchase) {
@ -252,6 +268,10 @@ class _HAClientAppState extends State<HAClientApp> {
}
}
Future<void> initPlatformState() async {
await BackgroundLocator.initialize();
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {

View File

@ -19,6 +19,8 @@ class ConnectionManager {
String _tempToken;
String oauthUrl;
String webhookId;
double haVersion;
bool scrollBadges;
String mobileAppDeviceName;
bool settingsLoaded = false;
int appIntegrationVersion;
@ -47,6 +49,7 @@ class ConnectionManager {
webhookId = prefs.getString('app-webhook-id');
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
mobileAppDeviceName = prefs.getString('app-integration-device-name');
scrollBadges = prefs.getBool('scroll-badges') ?? true;
displayHostname = "$_domain:$_port";
_webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
@ -154,7 +157,12 @@ class ConnectionManager {
if (!connecting.isCompleted) connecting.completeError(e);
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
String v = data["ha_version"];
if (v != null && v.isNotEmpty) {
haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
}
Logger.d("Home assistant version: $v ($haVersion)");
Crashlytics.instance.setString('ha_version', v);
Logger.d("[Connection] Subscribing to events");
sendSocketMessage(
type: "subscribe_events",

View File

@ -26,7 +26,7 @@ class LocationManager {
defaultUpdateIntervalMinutes);
_isRunning = prefs.getBool("location-enabled") ?? false;
if (_isRunning) {
await _startLocationService();
//await _startLocationService();
}
}
@ -57,6 +57,20 @@ class LocationManager {
}
_startLocationService() async {
Logger.d('Starting location tracking');
BackgroundLocator.registerLocationUpdate(
locationCallback,
//optional
androidNotificationCallback: locationNotificationCallback,
settings: LocationSettings(
notificationTitle: "HA Client location tracking",
notificationMsg: "HA Client is updating your device location",
wakeLockTime: 20,
autoStop: false,
interval: 10
),
);
/*
String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) {
@ -100,53 +114,129 @@ class LocationManager {
);
}
}
*/
}
_stopLocationService() async {
Logger.d("Canceling previous schedule if any...");
await workManager.Workmanager.cancelAll();
Logger.d('Stopping location tracking');
IsolateNameServer.removePortNameMapping(isolateName);
BackgroundLocator.unRegisterLocationUpdate();
/*Logger.d("Canceling previous schedule if any...");
await workManager.Workmanager.cancelAll();*/
}
static const String isolateName = "HAClientLocatorIsolate";
static void locationCallback(LocationDto locationDto) async {
print('[Background location] Got location: $locationDto');
sendLocationData(locationDto);
final SendPort send = IsolateNameServer.lookupPortByName(isolateName);
send?.send(locationDto);
}
static Future<void> sendLocationData(LocationDto location) async {
print('[Background location] Loading settings...');
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port');
String webhookId = prefs.getString('app-webhook-id');
String httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$domain:$port";
if (webhookId != null && webhookId.isNotEmpty) {
String url = "$httpWebHost/api/webhook/$webhookId";
Map<String, String> headers = {};
headers["Content-Type"] = "application/json";
Map data = {
"type": "update_location",
"data": {
"gps": [],
"gps_accuracy": 0,
"battery": 100
}
};
try {
if (location.longitude != null && location.latitude != null) {
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
print('[Background location] Sending...');
try {
http.Response response = await http.post(
url,
headers: headers,
body: json.encode(data)
);
if (response.statusCode >= 200 && response.statusCode < 300) {
print('[Background location] Success!');
} else {
print('[Background location] Error sending data: ${response.statusCode}');
}
} catch(e) {
print('[Background location] Error sending data: $e');
}
} else {
print('[Background location] Error. Location is null');
}
} catch (e) {
print('[Background location] Error: $e');
}
}
}
static void locationNotificationCallback() {
print('[Background location] User clicked on the notification');
}
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 = {
"type": "update_location",
"data": {
"gps": [position.latitude, position.longitude],
"gps_accuracy": position.accuracy,
"battery": batteryLevel ?? 100
/*
try {
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 = {
"type": "update_location",
"data": {
"gps": [position.latitude, position.longitude],
"gps_accuracy": position.accuracy,
"battery": batteryLevel ?? 100
}
};
Logger.d("[Foreground location] Sending data home...");
http.Response response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(data)
);
if (response.statusCode >= 300) {
Logger.e('Foreground location update error: ${response.body}');
}
};
Logger.d("[Foreground location] Sending data home...");
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(data)
);
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
} else {
Logger.d("[Foreground location] No location. Aborting.");
}
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
} else {
Logger.d("[Foreground location] No location. Aborting.");
}
}
} catch (e, stack) {
Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack);
}
*/
}
}
/*
void updateDeviceLocationIsolate() {
workManager.Workmanager.executeTask((backgroundTask, data) async {
//print("[Background $backgroundTask] Started");
@ -234,4 +324,5 @@ void updateDeviceLocationIsolate() {
print("[Background $backgroundTask] Finished.");*/
return true;
});
}
}
*/

View File

@ -31,12 +31,16 @@ class MobileAppIntegrationManager {
Logger.d("Mobile app was not registered yet. Registering...");
var registrationData = Map.from(_appRegistrationData);
registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}",
"app_id": "ha_client",
"app_name": "$appName",
"os_name": DeviceInfoManager().osName,
"supports_encryption": false,
});
if (ConnectionManager().haVersion >= 104) {
registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}"
});
}
ConnectionManager().sendHTTPPost(
endPoint: "/api/mobile_app/registrations",
includeAuthHeader: true,

View File

@ -14,13 +14,13 @@ class StartupUserMessagesManager {
bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-1006";
static final _whatsNewMessageKey = "user-msg-whats-new-url";
void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
_whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false;
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
if (!_whatsNewMessageShown) {
_showWhatsNewMessage();
} else if (!_supportAppDevelopmentMessageShown) {
@ -52,7 +52,7 @@ class StartupUserMessagesManager {
void _showWhatsNewMessage() {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_whatsNewMessageKey, true);
prefs.setString(_whatsNewMessageKey, whatsNewUrl);
eventBus.fire(ShowPageEvent(path: "/whats-new"));
});
}

View File

@ -53,9 +53,11 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
}
_switchLocationTrackingState(bool state) async {
/*
if (state) {
await LocationManager().updateDeviceLocation();
}
*/
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
setState(() {
_wait = false;

View File

@ -12,7 +12,7 @@ class LookAndFeelSettingsPage extends StatefulWidget {
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
AppTheme _currentTheme;
bool _changed = false;
bool _scrollBadges = false;
@override
void initState() {
@ -26,13 +26,15 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
SharedPreferences.getInstance().then((prefs) {
setState(() {
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme.index];
_scrollBadges = prefs.getBool('scroll-badges') ?? true;
});
});
}
_saveSettings(AppTheme theme) {
_saveTheme(AppTheme theme) {
SharedPreferences.getInstance().then((prefs) {
prefs.setInt('app-theme', theme.index);
prefs.setBool('scroll-badges', _scrollBadges);
setState(() {
_currentTheme = theme;
eventBus.fire(ChangeThemeEvent(_currentTheme));
@ -40,6 +42,12 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
});
}
Future _saveOther() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
ConnectionManager().scrollBadges = _scrollBadges;
await prefs.setBool('scroll-badges', _scrollBadges);
}
Map appThemeName = {
AppTheme.defaultTheme: 'Default',
AppTheme.haTheme: 'Home Assistant theme',
@ -59,15 +67,35 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
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),
)
onChanged: (theme) => _saveTheme(theme),
),
Container(height: Sizes.doubleRowPadding),
Text("Badges display:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton<bool>(
value: _scrollBadges,
iconSize: 30.0,
isExpanded: true,
style: Theme.of(context).textTheme.title,
items: [true, false].map((value) {
return new DropdownMenuItem<bool>(
value: value,
child: Text('${value ? 'Horizontal scroll' : 'In rows'}'),
);
}).toList(),
onChanged: (val) {
setState(() {
_scrollBadges = val;
});
_saveOther();
},
),
]
);
}

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_1.0.0_stable.md");
response = await http.get(whatsNewUrl);
if (response.statusCode == 200) {
setState(() {
data = response.body;

View File

@ -54,7 +54,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
});
}
}).catchError((e) {
Logger.e("Error loading $entityId history: $e");
Logger.e("Error loading $entityId history: $e", skipCrashlytics: true);
if (!_disposed) {
setState(() {
_history = [];

View File

@ -18,8 +18,8 @@ class Logger {
print(data);
}
static void e(String message, {dynamic stacktrace, bool skipCrashlytics: false}) {
_writeToLog(ErrorLevel.ERROR, message, stacktrace, skipCrashlytics);
static void e(dynamic message, {dynamic stacktrace, bool skipCrashlytics: false}) {
_writeToLog(ErrorLevel.ERROR, message.toString(), stacktrace, skipCrashlytics);
}
static void w(String message) {

View File

@ -2,7 +2,7 @@ part of 'main.dart';
class HAView {
List<CardData> cards = [];
List<Entity> badges = [];
CardData badges;
Entity linkedEntity;
String name;
String id;
@ -16,28 +16,16 @@ class HAView {
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);
}
} else {
String eId = '${entity['entity']}';
if (HomeAssistant().entities.isExist(eId)) {
Entity e = HomeAssistant().entities.get(eId);
badges.add(e);
}
}
if (rawData['badges'] != null && !isPanel) {
badges = CardData.parse({
'type': CardType.BADGES,
'badges': rawData['badges']
});
}
(rawData["cards"] ?? []).forEach((rawCardData) {
(rawData['cards'] ?? []).forEach((rawCardData) {
cards.add(CardData.parse(rawCardData));
});
//cards.addAll(_createLovelaceCards(rawData["cards"] ?? [], 1));
}
Widget buildTab() {

View File

@ -18,6 +18,12 @@ class ViewWidget extends StatelessWidget {
);
} else {
Widget cardsContainer;
Widget badgesContainer;
if (this.view.badges != null && this.view.badges is BadgesData) {
badgesContainer = this.view.badges.buildCardWidget();
} else {
badgesContainer = Container(width: 0, height: 0);
}
if (this.view.cards.isNotEmpty) {
cardsContainer = DynamicMultiColumnLayout(
minColumnWidth: Sizes.minViewColumnWidth,
@ -44,13 +50,15 @@ class ViewWidget extends StatelessWidget {
} else {
cardsContainer = Container();
}
return ListView(
shrinkWrap: true,
return SingleChildScrollView(
padding: EdgeInsets.all(0),
children: <Widget>[
_buildBadges(context),
cardsContainer
]
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
badgesContainer,
cardsContainer
],
),
);
}
}
@ -63,18 +71,4 @@ class ViewWidget extends StatelessWidget {
}
}
Widget _buildBadges(BuildContext context) {
if (this.view.badges.isNotEmpty) {
return Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 1.0,
children: this.view.badges.map((badge) =>
badge.buildBadgeWidget(context)).toList(),
);
} else {
return Container(width: 0, height: 0,);
}
}
}

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 1.0.0+1008
version: 1.1.0+1100
environment:
@ -27,12 +27,13 @@ dependencies:
flutter_secure_storage: ^3.3.3
device_info: ^0.4.1+4
flutter_local_notifications: ^1.1.6
geolocator: ^5.3.1
workmanager: ^0.2.2
#geolocator: ^5.3.1
background_locator: ^1.1.3+1
#workmanager: ^0.2.2
battery: ^1.0.0
firebase_crashlytics: ^0.1.3+3
syncfusion_flutter_core: ^18.1.43
syncfusion_flutter_gauges: ^18.1.43
syncfusion_flutter_core: ^18.1.48
syncfusion_flutter_gauges: ^18.1.48
dev_dependencies:

View File

@ -4,6 +4,7 @@ import 'dart:io';
Future<void> main() async {
final config = {
'syncfusion_license_key': Platform.environment['SYNCFUSION_LICENSE_KEY'],
'version_type': Platform.environment['HA_CLIENT_VERSION_TYPE'] ?? ''
};
final filename = 'lib/.secrets.dart';