Compare commits

...

100 Commits

Author SHA1 Message Date
f77e46de37 Version 0.3.12 2018-12-14 17:04:52 +02:00
cda17b1217 Resolves #232 2018-12-14 17:03:18 +02:00
be560769ef Resolves #243 2018-12-14 16:57:11 +02:00
3815800e32 Resolves #253 removing trash characters from state string 2018-12-14 16:48:35 +02:00
a3226311a2 Fix default tap actions 2018-12-14 16:31:41 +02:00
79669243c2 Remove some logging 2018-12-14 16:01:13 +02:00
fdc81f6ea4 Resolves #237 2018-12-14 15:59:47 +02:00
7fe44459e7 Resolves #252, Resolves #249 2018-12-14 14:28:23 +02:00
a8500d44e1 Version 0.3.11 2018-12-13 23:42:34 +02:00
b4d4c5abec Relates to #248 old format support 2018-12-13 23:40:20 +02:00
c19a3f272a Resolves #248 tap_action parsing fix 2018-12-13 23:37:54 +02:00
b264534858 Add files via upload 2018-12-12 10:43:26 +02:00
ab53f77f9e Add files via upload 2018-12-12 10:40:50 +02:00
c73956720c Create empty 2018-12-12 10:39:42 +02:00
051041e794 build number 2018-12-07 23:00:20 +02:00
5c83be9fee Resolves #207 Entity filter card support 2018-12-07 22:04:14 +02:00
4bece42693 build number 2018-11-29 21:46:47 +02:00
4ae107fe4c Resolves #230 Vertical stack card 2018-11-29 21:45:46 +02:00
9523ed2562 Build number 2018-11-25 20:45:22 +02:00
9c403480e2 Resolves #120 Horizontal Stack Cards 2018-11-25 20:44:19 +02:00
20b1b90e39 Resolves #206 Entity button with tap and hold events 2018-11-25 18:09:06 +02:00
5633e30448 WIP #206 Entity button card 2018-11-25 17:33:33 +02:00
4492fb9f0c Build number 2018-11-24 17:30:02 +02:00
36410752e4 Fix app version 2018-11-24 17:29:33 +02:00
0219f7bfbb Version 0.3.10 2018-11-24 17:04:41 +02:00
5f3c77f4b9 Resolves #145 Fan support 2018-11-24 17:00:45 +02:00
a36c7a9ca3 Resolves #186 Switch for group with same domain antities 2018-11-24 11:33:59 +02:00
56ce6dfeeb Improved tap animation for glance card 2018-11-24 11:02:28 +02:00
67c214454f build number 2018-11-24 01:20:18 +02:00
73398378c4 Resolves #227 2018-11-24 00:37:55 +02:00
215871ce9e Resolves #226 2018-11-23 21:59:33 +02:00
fd8ea6befd Show auto groups that is not hidden 2018-11-23 19:30:16 +02:00
809a1a1c8c Resolves #146 Lock support 2018-11-23 19:18:17 +02:00
fc8f2f200f Small screens support 2018-11-23 18:16:38 +02:00
f41c9f9197 Resolves #202 Service call info 2018-11-23 16:38:26 +02:00
cdf55ce68b Resolves #201 New progress indicator in the bottom of the app 2018-11-23 16:30:42 +02:00
12088d9516 Resolves #223, Resolves #197 2018-11-23 16:03:38 +02:00
a0235ee385 Handle entity taps and holds in one place 2018-11-23 15:06:42 +02:00
67fbdb13c6 Proper autogenerateg groups detection 2018-11-23 14:33:03 +02:00
c5960de0be Resolves #193 Source selection support for media_player 2018-11-23 14:18:25 +02:00
da15e880ec Sound mode support for media player 2018-11-23 14:11:34 +02:00
efbe33f4e3 Fix font sizes, long entity states 2018-11-18 16:40:12 +02:00
af84c99a2d build 69 2018-11-18 13:25:00 +02:00
438449cad8 Handling taps on entity name and state for glance card 2018-11-18 13:24:05 +02:00
d9ca55c3b7 Resolves #131 2018-11-18 13:19:00 +02:00
f248268984 New bottom info bar 2018-11-18 12:46:54 +02:00
8ee096595c Resolves #192 Don't fetch data on every app resume 2018-11-18 09:47:22 +02:00
a8e79c289b Version 0.3.9 2018-11-17 23:07:58 +02:00
2cd8533882 Link in about box 2018-11-17 23:02:05 +02:00
0a21d9c690 Fix repository url 2018-11-17 22:55:04 +02:00
e77bb533b1 Fix non-lovelace UI cards creating issue 2018-11-17 22:52:06 +02:00
96f1211395 Resolves #55 2018-11-17 22:40:33 +02:00
1e4cb03470 build number 66 2018-11-16 23:36:23 +02:00
ab67b557ca Resolves #183 Service call support for glance card 2018-11-16 23:35:08 +02:00
82c9bd26d1 WIP #183 tap_action support. State change event fix 2018-11-16 22:32:43 +02:00
1bd04abd37 Resolves #189 2018-11-16 14:30:43 +02:00
c5942d22b3 WIP #183 Custom names and icons 2018-11-15 19:08:47 +02:00
37ad5e81cf WIP #183 Glance card ui improvements 2018-11-15 16:13:54 +02:00
26187e6233 Resolves #155 2018-11-15 15:57:17 +02:00
b8f6fda8d3 Remove assumedState from Entity class 2018-11-15 12:49:46 +02:00
62b4e99810 build number 2018-11-14 19:53:00 +02:00
25bf10a64e WIP #183 2018-11-14 19:52:17 +02:00
874410964d Resolves #178 2018-11-14 18:03:50 +02:00
57c30917b3 WIP #55 main media controls 2018-11-14 15:43:04 +02:00
87f89b63e1 State const 2018-11-14 15:14:46 +02:00
3190b45db3 Resolves #176 History can be requested only once per 30 seconds 2018-11-14 13:38:02 +02:00
f5434e26e5 Resolves #174 2018-11-14 13:14:45 +02:00
86b6ad6bba Resolves #171 2018-11-14 12:35:08 +02:00
8a9641fbed Internal build 63 2018-11-12 20:54:48 +02:00
5142391da2 Resolves #184 2018-11-12 20:47:49 +02:00
01090dc3b1 Some improvements 2018-11-12 20:47:49 +02:00
0a7bbb5a38 Update README.md 2018-11-12 16:53:26 +02:00
c347eee9f0 Update README.md 2018-11-12 16:52:10 +02:00
90f197ba54 Update README.md 2018-11-12 12:41:01 +02:00
e09917c687 Merge pull request #177 from estevez-dev/add-license-1
Create LICENSE
2018-11-12 10:35:29 +02:00
a69da832cb Create LICENSE 2018-11-12 10:35:17 +02:00
c1708fd980 WIP #55 Fix preview 2018-11-11 22:21:29 +02:00
c85a9bbe27 WIP #55 2018-11-11 20:54:54 +02:00
d9790dedbb Cards creating optimization 2018-11-11 19:51:02 +02:00
30e4eaa023 Move history widget under additional controls 2018-11-11 18:49:04 +02:00
54e00c3403 WIP #55 2018-11-11 18:36:49 +02:00
0e3474bbcb version 0.3.8 2018-11-06 21:12:24 +02:00
efd06ca547 Resolves #164
Template cover has supported_features = 11, but it supports setting position.
2018-11-06 14:10:34 +02:00
69fd37d4fe Fix settings saving issue 2018-11-06 14:01:00 +02:00
4a49372410 version code 2018-11-05 20:41:19 +02:00
478f58e2d8 v.0.3.7 hotfixes 2018-11-05 20:37:35 +02:00
a87aff67ac Resolves #170 Saving settings button issue fix 2018-11-05 20:34:56 +02:00
644f5e7fc6 Resolves #166 2018-11-05 20:21:44 +02:00
3cddac3dc6 Resolves #167 2018-11-05 20:15:20 +02:00
ab30c64eab v.0.3.7 2018-11-04 23:20:58 +02:00
6d79487219 Resolves #165 Hide controls for unavailable lights 2018-11-04 23:13:25 +02:00
9f7444eae0 Remove widgetHeigth 2018-11-04 22:57:53 +02:00
788d682f2f Resolves #160 Flexible entity heigth 2018-11-04 22:55:09 +02:00
66f84952f0 Get entity name from entity id if was not set 2018-11-04 22:25:22 +02:00
5d95c3702d Resolves #162 View names display 2018-11-04 22:19:45 +02:00
1f0bd8059b Resolves #164 Allow to open cover if it is not fully opened 2018-11-04 21:55:37 +02:00
a7830df628 Fix covers tilt position issue 2018-11-04 21:40:41 +02:00
790446d592 Resolves #161 Colors for more then 10 states in history 2018-11-04 21:36:15 +02:00
bb17885b4a Resolves #163 Location title 2018-11-04 21:02:12 +02:00
04d8681656 log improve and v.0.3.6 2018-11-04 19:30:10 +02:00
71 changed files with 2574 additions and 1116 deletions

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,3 +1,13 @@
# Android client for Home Assistant
# HA Client
## Native Android client for Home Assistant
### With Lovelace UI support
Home Assistant Android client using Flutter and Dart.
Home Assistant Android client on Dart with Flutter.
Visit [www.keyboardcrumbs.io](http://www.keyboardcrumbs.io/ha-client) for more info.
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
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)

1
docs/empty Normal file
View File

@ -0,0 +1 @@

BIN
docs/ha_access_tokens.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/ha_profile-300x247.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/settings-869x1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -1,9 +1,9 @@
part of 'main.dart';
class EntityViewPage extends StatefulWidget {
EntityViewPage({Key key, @required this.entity, @required this.homeAssistant }) : super(key: key);
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
final Entity entity;
final String entityId;
final HomeAssistant homeAssistant;
@override
@ -12,30 +12,26 @@ class EntityViewPage extends StatefulWidget {
class _EntityViewPageState extends State<EntityViewPage> {
String _title;
StreamSubscription _refreshDataSubscription;
StreamSubscription _stateSubscription;
@override
void initState() {
super.initState();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId == widget.entity.entityId) {
TheLogger.debug("State change event handled by entity page: ${event.entityId}");
if (event.entityId == widget.entityId) {
setState(() {});
}
});
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
setState(() {});
});
_prepareData();
_getHistory();
}
void _prepareData() async {
_title = widget.entity.displayName;
}
void _getHistory() {
/* widget.homeAssistant.getHistory(widget.entity.entityId).then((List history) {
if (history != null) {
}
});*/
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
}
@ -54,7 +50,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
padding: EdgeInsets.all(10.0),
child: HomeAssistantModel(
homeAssistant: widget.homeAssistant,
child: widget.entity.buildEntityPageWidget(context)
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
)
),
);
@ -63,6 +59,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
@override
void dispose(){
if (_stateSubscription != null) _stateSubscription.cancel();
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
super.dispose();
}
}

View File

@ -1,8 +1,6 @@
part of '../main.dart';
class ClimateEntity extends Entity {
@override
double widgetHeight = 38.0;
@override
EntityHistoryConfig historyConfig = EntityHistoryConfig(
@ -86,7 +84,7 @@ class ClimateEntity extends Entity {
String get fanMode => attributes['fan_mode'];
String get swingMode => attributes['swing_mode'];
bool get awayMode => attributes['away_mode'] == "on";
bool get isOff => state == "off";
bool get isOff => state == EntityState.off;
bool get auxHeat => attributes['aux_heat'] == "on";
ClimateEntity(Map rawData) : super(rawData);

View File

@ -0,0 +1,97 @@
part of '../main.dart';
class EntityState {
static const on = 'on';
static const off = 'off';
static const home = 'home';
static const not_home = 'not_home';
static const unknown = 'unknown';
static const open = 'open';
static const opening = 'opening';
static const closed = 'closed';
static const closing = 'closing';
static const playing = 'playing';
static const paused = 'paused';
static const idle = 'idle';
static const standby = 'standby';
static const alarm_disarmed = 'disarmed';
static const alarm_armed_home = 'armed_home';
static const alarm_armed_away = 'armed_away';
static const alarm_armed_night = 'armed_night';
static const alarm_armed_custom_bypass = 'armed_custom_bypass';
static const alarm_pending = 'pending';
static const alarm_arming = 'arming';
static const alarm_disarming = 'disarming';
static const alarm_triggered = 'triggered';
static const locked = 'locked';
static const unlocked = 'unlocked';
static const unavailable = 'unavailable';
static const ok = 'ok';
static const problem = 'problem';
}
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;
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"];
}
}
}
}
}
class CardType {
static const horizontalStack = "horizontal-stack";
static const verticalStack = "vertical-stack";
static const entities = "entities";
static const glance = "glance";
static const mediaControl = "media-control";
static const weatherForecast = "weather-forecast";
static const thermostat = "thermostat";
static const sensor = "sensor";
static const plantStatus = "plant-status";
static const pictureEntity = "picture-entity";
static const pictureElements = "picture-elements";
static const picture = "picture";
static const map = "map";
static const iframe = "iframe";
static const gauge = "gauge";
static const entityButton = "entity-button";
static const conditional = "conditional";
static const alarmPanel = "alarm-panel";
}

View File

@ -1,8 +1,6 @@
part of '../main.dart';
class CoverEntity extends Entity {
@override
double widgetHeight = 38.0;
static const SUPPORT_OPEN = 1;
static const SUPPORT_CLOSE = 2;
@ -42,10 +40,10 @@ class CoverEntity extends Entity {
double get currentPosition => _getDoubleAttributeValue('current_position');
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
bool get canBeOpened => ((state != "opening") && (state != "open"));
bool get canBeClosed => ((state != "closing") && (state != "closed"));
bool get canTiltBeOpened => currentPosition < 100;
bool get canTiltBeClosed => currentPosition > 0;
bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0);
bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed));
bool get canTiltBeOpened => currentTiltPosition < 100;
bool get canTiltBeClosed => currentTiltPosition > 0;
CoverEntity(Map rawData) : super(rawData);
@ -59,4 +57,4 @@ class CoverEntity extends Entity {
return CoverControlWidget();
}
}
}

View File

@ -2,10 +2,6 @@ part of '../main.dart';
class Entity {
static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
};
static List badgeDomains = [
"alarm_control_panel",
"binary_sensor",
@ -16,43 +12,29 @@ class Entity {
"sensor"
];
static const rightWidgetPadding = 14.0;
static const leftWidgetPadding = 8.0;
static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0;
static const stateFontSize = 16.0;
static const nameFontSize = 16.0;
static const smallFontSize = 14.0;
static const largeFontSize = 24.0;
static const inputWidth = 160.0;
static const rowPadding = 10.0;
double widgetHeight = 34.0;
Map attributes;
String domain;
String entityId;
String state;
String assumedState;
DateTime _lastUpdated;
List<Entity> childEntities = [];
List<String> attributesToShow = ["all"];
String deviceClass;
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.simple
);
String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? "_");
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
String get deviceClass => attributes["device_class"] ?? null;
bool get isView =>
(domain == "group") &&
(attributes != null ? attributes["view"] ?? false : false);
bool get isGroup => domain == "group";
bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? "";
bool get isOn => state == "on";
bool get isOn => state == EntityState.on;
String get entityPicture => attributes["entity_picture"];
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? [];
@ -68,8 +50,8 @@ class Entity {
attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"];
assumedState = state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
}
@ -95,14 +77,18 @@ class Entity {
}
}
List<String> getStringListAttributeValue(String attribute) {
if (attributes["$attribute"] != null) {
List<String> result = (attributes["$attribute"] as List).cast<String>();
return result;
} else {
return null;
}
}
Widget buildDefaultWidget(BuildContext context) {
return EntityModel(
entity: this,
child: DefaultEntityContainer(
state: _buildStatePart(context),
height: widgetHeight,
),
handleTap: true,
return DefaultEntityContainer(
state: _buildStatePart(context)
);
}
@ -123,13 +109,14 @@ class Entity {
Widget buildEntityPageWidget(BuildContext context) {
return EntityModel(
entity: this,
entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[
DefaultEntityContainer(state: _buildStatePartForPage(context), height: widgetHeight),
DefaultEntityContainer(state: _buildStatePartForPage(context)),
LastUpdatedWidget(),
Divider(),
buildHistoryWidget(),
_buildAdditionalControlsForPage(context),
Divider(),
buildHistoryWidget(),
EntityAttributesList()
]),
handleTap: false,
@ -144,7 +131,7 @@ class Entity {
Widget buildBadgeWidget(BuildContext context) {
return EntityModel(
entity: this,
entityWrapper: EntityWrapper(entity: this),
child: BadgeWidget(),
handleTap: true,
);

View File

@ -0,0 +1,88 @@
part of '../main.dart';
class EntityWrapper {
String displayName;
String icon;
EntityUIAction uiAction;
Entity entity;
EntityWrapper({
this.entity,
String icon,
String displayName,
this.uiAction
}) {
this.icon = icon ?? entity.icon;
this.displayName = displayName ?? entity.displayName;
if (this.uiAction == null) {
this.uiAction = EntityUIAction();
}
}
void handleTap() {
switch (uiAction.tapAction) {
case EntityUIAction.toggle: {
eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break;
}
case EntityUIAction.callService: {
if (uiAction.tapService != null) {
eventBus.fire(
ServiceCallEvent(uiAction.tapService.split(".")[0],
uiAction.tapService.split(".")[1], null,
uiAction.tapServiceData));
}
break;
}
case EntityUIAction.none: {
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity));
break;
}
default: {
break;
}
}
}
void handleHold() {
switch (uiAction.holdAction) {
case EntityUIAction.toggle: {
eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break;
}
case EntityUIAction.callService: {
if (uiAction.holdService != null) {
eventBus.fire(
ServiceCallEvent(uiAction.holdService.split(".")[0],
uiAction.holdService.split(".")[1], null,
uiAction.holdServiceData));
}
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity));
break;
}
default: {
break;
}
}
}
}

View File

@ -0,0 +1,32 @@
part of '../main.dart';
class FanEntity extends Entity {
static const SUPPORT_SET_SPEED = 1;
static const SUPPORT_OSCILLATE = 2;
static const SUPPORT_DIRECTION = 4;
FanEntity(Map rawData) : super(rawData);
bool get supportSetSpeed => ((attributes["supported_features"] &
FanEntity.SUPPORT_SET_SPEED) ==
FanEntity.SUPPORT_SET_SPEED);
bool get supportOscillate => ((attributes["supported_features"] &
FanEntity.SUPPORT_OSCILLATE) ==
FanEntity.SUPPORT_OSCILLATE);
bool get supportDirection => ((attributes["supported_features"] &
FanEntity.SUPPORT_DIRECTION) ==
FanEntity.SUPPORT_DIRECTION);
List<String> get speedList => getStringListAttributeValue("speed_list");
@override
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return FanControlsWidget();
}
}

View File

@ -0,0 +1,43 @@
part of '../main.dart';
class GroupEntity extends Entity {
GroupEntity(Map rawData) : super(rawData);
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
String mutualDomain;
bool switchable = false;
@override
Widget _buildStatePart(BuildContext context) {
if (switchable) {
return SwitchStateWidget(
domainForService: "homeassistant",
);
} else {
return super._buildStatePart(context);
}
}
@override
void update(Map rawData) {
super.update(rawData);
if (_isOneDomain()) {
mutualDomain = attributes['entity_id'][0].split(".")[0];
switchable = _domainsForSwitchableGroup.contains(mutualDomain);
}
}
bool _isOneDomain() {
bool result = false;
if (attributes['entity_id'] != null && attributes['entity_id'] is List && attributes['entity_id'].isNotEmpty) {
String firstChildDomain = attributes['entity_id'][0].split(".")[0];
result = true;
attributes['entity_id'].forEach((childEntityId){
if (childEntityId.split(".")[0] != firstChildDomain) {
result = false;
}
});
}
return result;
}
}

View File

@ -38,7 +38,7 @@ class LightEntity extends Entity {
double get minMireds => _getDoubleAttributeValue("min_mireds");
Color get color => _getColor();
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
List<String> get effectList => _getEffectList();
List<String> get effectList => getStringListAttributeValue("effect_list");
LightEntity(Map rawData) : super(rawData);
@ -55,15 +55,6 @@ class LightEntity extends Entity {
}
}
List<String> _getEffectList() {
if (attributes["effect_list"] != null) {
List<String> result = (attributes["effect_list"] as List).cast<String>();
return result;
} else {
return null;
}
}
@override
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();

View File

@ -0,0 +1,12 @@
part of '../main.dart';
class LockEntity extends Entity {
LockEntity(Map rawData) : super(rawData);
bool get isLocked => state == "locked";
@override
Widget _buildStatePart(BuildContext context) {
return LockStateWidget();
}
}

View File

@ -47,5 +47,37 @@ class MediaPlayerEntity extends Entity {
bool get supportTurnOff => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_TURN_OFF) ==
MediaPlayerEntity.SUPPORT_TURN_OFF);
bool get supportPlayMedia => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_PLAY_MEDIA) ==
MediaPlayerEntity.SUPPORT_PLAY_MEDIA);
bool get supportVolumeStep => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_VOLUME_STEP) ==
MediaPlayerEntity.SUPPORT_VOLUME_STEP);
bool get supportSelectSource => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_SELECT_SOURCE) ==
MediaPlayerEntity.SUPPORT_SELECT_SOURCE);
bool get supportStop => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_STOP) ==
MediaPlayerEntity.SUPPORT_STOP);
bool get supportClearPlaylist => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) ==
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST);
bool get supportPlay => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_PLAY) ==
MediaPlayerEntity.SUPPORT_PLAY);
bool get supportShuffleSet => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_SHUFFLE_SET) ==
MediaPlayerEntity.SUPPORT_SHUFFLE_SET);
bool get supportSelectSoundMode => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) ==
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE);
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
List<String> get sourceList => getStringListAttributeValue("source_list");
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return MediaPlayerControls();
}
}

View File

@ -8,6 +8,12 @@ class SliderEntity extends Entity {
double get valueStep => _getDoubleAttributeValue("step") ?? 1.0;
@override
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.numericState,
numericState: true
);
/*@override
Widget _buildStatePart(BuildContext context) {
return Expanded(
//width: 200.0,
@ -16,7 +22,9 @@ class SliderEntity extends Entity {
SliderStateWidget(
expanded: true,
),
SimpleEntityState(),
SimpleEntityState(
expanded: false,
),
],
),
);
@ -24,13 +32,13 @@ class SliderEntity extends Entity {
@override
Widget _buildStatePartForPage(BuildContext context) {
return SimpleEntityState();
}
return SimpleEntityState(
expanded: false,
);
}*/
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return SliderStateWidget(
expanded: false,
);
return SliderControlsWidget();
}
}

View File

@ -38,9 +38,15 @@ class EntityCollection {
case 'sun': {
return SunEntity(rawEntityData);
}
case "media_player": {
return MediaPlayerEntity(rawEntityData);
}
case 'sensor': {
return SensorEntity(rawEntityData);
}
case 'lock': {
return LockEntity(rawEntityData);
}
case "automation":
case "input_boolean":
case "switch": {
@ -49,6 +55,9 @@ class EntityCollection {
case "light": {
return LightEntity(rawEntityData);
}
case "group": {
return GroupEntity(rawEntityData);
}
case "script":
case "scene": {
return ButtonEntity(rawEntityData);
@ -71,6 +80,9 @@ class EntityCollection {
case "cover": {
return CoverEntity(rawEntityData);
}
case "fan": {
return FanEntity(rawEntityData);
}
default: {
return Entity(rawEntityData);
}
@ -123,7 +135,7 @@ class EntityCollection {
List<Entity> groups = [];
List<Entity> nonGroupEntities = [];
_allEntities.forEach((id, entity){
if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) {
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
groups.add(entity);
}
if (!entity.isGroup) {

View File

@ -0,0 +1,45 @@
part of '../main.dart';
class ButtonEntityContainer extends StatelessWidget {
ButtonEntityContainer({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FractionallySizedBox(
widthFactor: 0.4,
child: FittedBox(
fit: BoxFit.fitHeight,
child: EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
iconSize: Sizes.iconSize,
)
),
),
_buildName()
],
),
);
}
Widget _buildName() {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
wordsWrap: true,
textAlign: TextAlign.center,
fontSize: Sizes.nameFontSize,
);
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class BadgeWidget extends StatelessWidget {
@override
@ -7,12 +7,12 @@ class BadgeWidget extends StatelessWidget {
double iconSize = 26.0;
Widget badgeIcon;
String onBadgeTextValue;
Color iconColor = Entity.badgeColors[entityModel.entity.domain] ??
Entity.badgeColors["default"];
switch (entityModel.entity.domain) {
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
EntityColor.badgeColors["default"];
switch (entityModel.entityWrapper.entity.domain) {
case "sun":
{
badgeIcon = entityModel.entity.state == "below_horizon"
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
? Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
size: iconSize,
@ -25,10 +25,10 @@ class BadgeWidget extends StatelessWidget {
}
case "sensor":
{
onBadgeTextValue = entityModel.entity.unitOfMeasurement;
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${entityModel.entity.state}",
"${entityModel.entityWrapper.entity.state}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
@ -40,14 +40,14 @@ class BadgeWidget extends StatelessWidget {
case "device_tracker":
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entity, iconSize, Colors.black);
onBadgeTextValue = entityModel.entity.state;
entityModel.entityWrapper, iconSize, Colors.black);
onBadgeTextValue = entityModel.entityWrapper.entity.state;
break;
}
default:
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entity, iconSize, Colors.black);
entityModel.entityWrapper, iconSize, Colors.black);
}
}
Widget onBadgeText;
@ -109,7 +109,7 @@ class BadgeWidget extends StatelessWidget {
Container(
width: 60.0,
child: Text(
"${entityModel.entity.displayName}",
"${entityModel.entityWrapper.displayName}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.0),
softWrap: true,
@ -120,6 +120,6 @@ class BadgeWidget extends StatelessWidget {
],
),
onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entityModel.entity)));
eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity)));
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class EntityAttributesList extends StatelessWidget {
EntityAttributesList({Key key}) : super(key: key);
@ -7,14 +7,14 @@ class EntityAttributesList extends StatelessWidget {
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
List<Widget> attrs = [];
if ((entityModel.entity.attributesToShow == null) ||
(entityModel.entity.attributesToShow.contains("all"))) {
entityModel.entity.attributes.forEach((name, value) {
if ((entityModel.entityWrapper.entity.attributesToShow == null) ||
(entityModel.entityWrapper.entity.attributesToShow.contains("all"))) {
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
attrs.add(_buildSingleAttribute("$name", "$value"));
});
} else {
entityModel.entity.attributesToShow.forEach((String attr) {
String attrValue = entityModel.entity.getAttribute("$attr");
entityModel.entityWrapper.entity.attributesToShow.forEach((String attr) {
String attrValue = entityModel.entityWrapper.entity.getAttribute("$attr");
if (attrValue != null) {
attrs.add(
_buildSingleAttribute("$attr", "$attrValue"));
@ -34,7 +34,7 @@ class EntityAttributesList extends StatelessWidget {
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(
Entity.leftWidgetPadding, Entity.rowPadding, 0.0, 0.0),
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, 0.0),
child: Text(
"$name",
textAlign: TextAlign.left,
@ -44,7 +44,7 @@ class EntityAttributesList extends StatelessWidget {
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(
0.0, Entity.rowPadding, Entity.rightWidgetPadding, 0.0),
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
child: Text(
"$value",
textAlign: TextAlign.right,

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class LastUpdatedWidget extends StatelessWidget {
@override
@ -6,12 +6,12 @@ class LastUpdatedWidget extends StatelessWidget {
final entityModel = EntityModel.of(context);
return Padding(
padding: EdgeInsets.fromLTRB(
Entity.leftWidgetPadding, 0.0, 0.0, 0.0),
Sizes.leftWidgetPadding, 0.0, 0.0, 0.0),
child: Text(
'${entityModel.entity.lastUpdated}',
'${entityModel.entityWrapper.entity.lastUpdated}',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: Entity.smallFontSize, color: Colors.black26),
fontSize: Sizes.smallFontSize, color: Colors.black26),
),
);
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class ModeSelectorWidget extends StatelessWidget {
@ -12,7 +12,7 @@ class ModeSelectorWidget extends StatelessWidget {
ModeSelectorWidget({
Key key,
this.caption,
@required this.caption,
@required this.options,
this.value,
@required this.onChange,
@ -27,7 +27,7 @@ class ModeSelectorWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("$caption", style: TextStyle(
fontSize: captionFontSize ?? Entity.stateFontSize
fontSize: captionFontSize ?? Sizes.stateFontSize
)),
Row(
children: <Widget>[
@ -39,7 +39,7 @@ class ModeSelectorWidget extends StatelessWidget {
iconSize: 30.0,
isExpanded: true,
style: TextStyle(
fontSize: valueFontSize ?? Entity.largeFontSize,
fontSize: valueFontSize ?? Sizes.largeFontSize,
color: Colors.black,
),
hint: Text("Select ${caption.toLowerCase()}"),
@ -55,7 +55,7 @@ class ModeSelectorWidget extends StatelessWidget {
)
],
),
Container(height: bottomPadding ?? Entity.rowPadding,)
Container(height: bottomPadding ?? Sizes.rowPadding,)
],
);
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class ModeSwitchWidget extends StatelessWidget {
@ -6,27 +6,22 @@ class ModeSwitchWidget extends StatelessWidget {
final onChange;
final double captionFontSize;
final bool value;
final bool expanded;
ModeSwitchWidget({
Key key,
@required this.caption,
@required this.onChange,
this.captionFontSize,
this.value
this.value,
this.expanded: true
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: Text(
"$caption",
style: TextStyle(
fontSize: captionFontSize ?? Entity.stateFontSize
),
),
),
_buildCaption(),
Switch(
onChanged: (value) => onChange(value),
value: value ?? false,
@ -35,4 +30,19 @@ class ModeSwitchWidget extends StatelessWidget {
);
}
Widget _buildCaption() {
Widget captionWidget = Text(
"$caption",
style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
),
);
if (expanded) {
return Expanded(
child: captionWidget,
);
}
return captionWidget;
}
}

View File

@ -0,0 +1,54 @@
part of '../../main.dart';
class UniversalSlider extends StatelessWidget {
final onChanged;
final onChangeEnd;
final Widget leading;
final Widget closing;
final String title;
final double min;
final double max;
final double value;
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value}) : super(key: key);
@override
Widget build(BuildContext context) {
List <Widget> row = [];
if (leading != null) {
row.add(leading);
}
row.add(
Flexible(
child: Slider(
value: value,
min: min,
max: max,
onChanged: (value) => onChanged(value),
onChangeEnd: (value) => onChangeEnd(value),
),
)
);
if (closing != null) {
row.add(closing);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Sizes.rowPadding,),
Text(
"$title",
style: TextStyle(fontSize: Sizes.stateFontSize),
),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
Container(height: Sizes.rowPadding,)
],
);
}
}

View File

@ -165,7 +165,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entity;
final ClimateEntity entity = entityModel.entityWrapper.entity;
if (_changedHere) {
_showPending = (_tmpTemperature != entity.temperature);
_changedHere = false;
@ -174,7 +174,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_resetVars(entity);
}
return Padding(
padding: EdgeInsets.fromLTRB(Entity.leftWidgetPadding, Entity.rowPadding, Entity.rightWidgetPadding, 0.0),
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
@ -273,7 +273,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature", style: TextStyle(
fontSize: Entity.stateFontSize
fontSize: Sizes.stateFontSize
)),
TemperatureControlWidget(
value: _tmpTemperature,
@ -324,7 +324,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature range", style: TextStyle(
fontSize: Entity.stateFontSize
fontSize: Sizes.stateFontSize
)),
Row(
children: controls,
@ -342,7 +342,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
result.addAll(<Widget>[
Text(
"$_tmpTargetHumidity%",
style: TextStyle(fontSize: Entity.largeFontSize),
style: TextStyle(fontSize: Sizes.largeFontSize),
),
Expanded(
child: Slider(
@ -366,9 +366,9 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Target humidity", style: TextStyle(
fontSize: Entity.stateFontSize
fontSize: Sizes.stateFontSize
)),
),
Row(
@ -376,7 +376,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
children: result,
),
Container(
height: Entity.rowPadding,
height: Sizes.rowPadding,
)
],
);

View File

@ -38,15 +38,14 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entity;
TheLogger.debug("${entity.state}");
final CoverEntity entity = entityModel.entityWrapper.entity;
if (_changedHere) {
_changedHere = false;
} else {
_resetVars(entity);
}
return Padding(
padding: EdgeInsets.fromLTRB(Entity.leftWidgetPadding, Entity.rowPadding, Entity.rightWidgetPadding, 0.0),
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
@ -64,9 +63,9 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Position", style: TextStyle(
fontSize: Entity.stateFontSize
fontSize: Sizes.stateFontSize
)),
),
Slider(
@ -82,7 +81,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
},
onChangeEnd: (double value) => _setNewPosition(entity, value),
),
Container(height: Entity.rowPadding,)
Container(height: Sizes.rowPadding,)
],
);
} else {
@ -112,15 +111,15 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
},
onChangeEnd: (double value) => _setNewTiltPosition(entity, value),
),
Container(height: Entity.rowPadding,)
Container(height: Sizes.rowPadding,)
]);
}
if (controls.isNotEmpty) {
controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB(
0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Tilt position", style: TextStyle(
fontSize: Entity.stateFontSize
fontSize: Sizes.stateFontSize
)),
));
return Column(
@ -153,31 +152,31 @@ class CoverTiltControlsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entity;
final CoverEntity entity = entityModel.entityWrapper.entity;
List<Widget> buttons = [];
if (entity.supportOpenTilt) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName(
"mdi:arrow-top-right"),
size: Entity.iconSize,
size: Sizes.iconSize,
),
onPressed: entity.canTiltBeOpened ? () => _open(entity) : null));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
width: Sizes.iconSize + 20.0,
));
}
if (entity.supportStopTilt) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
size: Entity.iconSize,
size: Sizes.iconSize,
),
onPressed: () => _stop(entity)));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
width: Sizes.iconSize + 20.0,
));
}
if (entity.supportCloseTilt) {
@ -185,12 +184,12 @@ class CoverTiltControlsWidget extends StatelessWidget {
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName(
"mdi:arrow-bottom-left"),
size: Entity.iconSize,
size: Sizes.iconSize,
),
onPressed: entity.canTiltBeClosed ? () => _close(entity) : null));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
width: Sizes.iconSize + 20.0,
));
}

View File

@ -0,0 +1,123 @@
part of '../../main.dart';
class FanControlsWidget extends StatefulWidget {
@override
_FanControlsWidgetState createState() => _FanControlsWidgetState();
}
class _FanControlsWidgetState extends State<FanControlsWidget> {
bool _tmpOscillate;
bool _tmpDirectionForward;
bool _changedHere = false;
String _tmpSpeed;
void _resetState(FanEntity entity) {
_tmpOscillate = entity.attributes["oscillating"] ?? false;
_tmpDirectionForward = entity.attributes["direction"] == "forward";
_tmpSpeed = entity.attributes["speed"];
}
void _setOscillate(FanEntity entity, bool oscillate) {
setState(() {
_tmpOscillate = oscillate;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
"fan", "oscillate", entity.entityId,
{"oscillating": oscillate}));
});
}
void _setDirection(FanEntity entity, bool forward) {
setState(() {
_tmpDirectionForward = forward;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
"fan", "set_direction", entity.entityId,
{"direction": forward ? "forward" : "reverse"}));
});
}
void _setSpeed(FanEntity entity, String value) {
setState(() {
_tmpSpeed = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
"fan", "set_speed", entity.entityId,
{"speed": value}));
});
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final FanEntity entity = entityModel.entityWrapper.entity;
if (!_changedHere) {
_resetState(entity);
} else {
_changedHere = false;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_buildSpeedControl(entity),
_buildOscillateControl(entity),
_buildDirectionControl(entity)
],
);
}
Widget _buildSpeedControl(FanEntity entity) {
if (entity.supportSetSpeed && entity.speedList != null && entity.speedList.isNotEmpty) {
return ModeSelectorWidget(
onChange: (effect) => _setSpeed(entity, effect),
caption: "Speed",
options: entity.speedList,
value: _tmpSpeed
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildOscillateControl(FanEntity entity) {
if (entity.supportOscillate) {
return ModeSwitchWidget(
onChange: (value) => _setOscillate(entity, value),
caption: "Oscillate",
value: _tmpOscillate,
expanded: false,
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildDirectionControl(FanEntity entity) {
if (entity.supportDirection) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
onPressed: _tmpDirectionForward ?
() => _setDirection(entity, false) :
null,
icon: Icon(Icons.rotate_left),
),
IconButton(
onPressed: !_tmpDirectionForward ?
() => _setDirection(entity, true) :
null,
icon: Icon(Icons.rotate_right),
),
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
}

View File

@ -80,7 +80,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final LightEntity entity = entityModel.entity;
final LightEntity entity = entityModel.entityWrapper.entity;
if (!_changedHere) {
_resetState(entity);
} else {
@ -98,37 +98,20 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}
Widget _buildBrightnessControl(LightEntity entity) {
if ((entity.supportBrightness) && (_tmpBrightness != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Entity.rowPadding,),
Text(
"Brightness",
style: TextStyle(fontSize: Entity.stateFontSize),
),
Container(height: Entity.rowPadding,),
Row(
children: <Widget>[
Icon(Icons.brightness_5),
Expanded(
child: Slider(
value: _tmpBrightness.toDouble(),
min: 0.0,
max: 255.0,
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpBrightness = value.round();
});
},
onChangeEnd: (value) => _setBrightness(entity, value),
),
)
],
),
Container(height: Entity.rowPadding,)
],
if ((entity.supportBrightness) && (_tmpBrightness != null) && (entity.state != EntityState.unavailable)) {
return UniversalSlider(
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpBrightness = value.round();
});
},
min: 0.0,
max: 255.0,
onChangeEnd: (value) => _setBrightness(entity, value),
value: _tmpBrightness.toDouble(),
leading: Icon(Icons.brightness_5),
title: "Brightness",
);
} else {
return Container(width: 0.0, height: 0.0);
@ -137,37 +120,20 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildColorTempControl(LightEntity entity) {
if ((entity.supportColorTemp) && (_tmpColorTemp != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Entity.rowPadding,),
Text(
"Color temperature",
style: TextStyle(fontSize: Entity.stateFontSize),
),
Container(height: Entity.rowPadding,),
Row(
children: <Widget>[
Text("Cold", style: TextStyle(color: Colors.lightBlue),),
Expanded(
child: Slider(
value: _tmpColorTemp.toDouble(),
min: entity.minMireds,
max: entity.maxMireds,
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpColorTemp = value.round();
});
},
onChangeEnd: (value) => _setColorTemp(entity, value),
),
),
Text("Warm", style: TextStyle(color: Colors.amberAccent),),
],
),
Container(height: Entity.rowPadding,)
],
return UniversalSlider(
title: "Color temperature",
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
value: _tmpColorTemp.toDouble(),
onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds,
min: entity.minMireds,
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpColorTemp = value.round();
});
},
closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
);
} else {
return Container(width: 0.0, height: 0.0);
@ -179,7 +145,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(height: Entity.rowPadding,),
Container(height: Sizes.rowPadding,),
RaisedButton(
onPressed: () => _showColorPicker(entity),
color: _tmpColor ?? Colors.black45,
@ -193,7 +159,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
),
),
),
Container(height: 2*Entity.rowPadding,),
Container(height: 2*Sizes.rowPadding,),
],
);
} else {

View File

@ -0,0 +1,466 @@
part of '../../main.dart';
class MediaPlayerWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
//TheLogger.debug("stop: ${entity.supportStop}, seek: ${entity.supportSeek}");
return Column(
children: <Widget>[
Stack(
alignment: AlignmentDirectional.topEnd,
children: <Widget>[
_buildImage(entity),
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: Container(
color: Colors.black45,
child: _buildState(entity),
),
),
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: MediaPlayerProgressWidget()
)
],
),
MediaPlayerPlaybackControls()
]
);
}
Widget _buildState(MediaPlayerEntity entity) {
TextStyle style = TextStyle(
fontSize: 14.0,
color: Colors.white,
fontWeight: FontWeight.normal,
height: 1.2
);
List<Widget> states = [];
states.add(Text("${entity.displayName}", style: style));
String state = entity.state;
if (state == null || state == EntityState.off || state == EntityState.unavailable || state == EntityState.idle) {
states.add(Text("${entity.state}", style: style.apply(fontSizeDelta: 4.0),));
}
if (entity.attributes['media_title'] != null) {
states.add(Text(
"${entity.attributes['media_title']}",
style: style.apply(fontSizeDelta: 6.0, fontWeightDelta: 50),
maxLines: 1,
softWrap: true,
overflow: TextOverflow.ellipsis,
));
}
if (entity.attributes['media_content_type'] == "music") {
states.add(Text("${entity.attributes['media_artist'] ?? entity.attributes['app_name']}", style: style.apply(fontSizeDelta: 4.0),));
} else if (entity.attributes['app_name'] != null) {
states.add(Text("${entity.attributes['app_name']}", style: style.apply(fontSizeDelta: 4.0),));
}
return Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: states,
),
);
}
Widget _buildImage(MediaPlayerEntity entity) {
String state = entity.state;
if (homeAssistantWebHost != null && entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
return Container(
color: Colors.black,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Flexible(
child: Image(
image: CachedNetworkImageProvider("$homeAssistantWebHost${entity.entityPicture}"),
height: 240.0,
//width: 320.0,
fit: BoxFit.contain,
),
)
],
),
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:movie"),
size: 150.0,
color: EntityColor.stateColor("$state"),
)
],
);
/*return Container(
color: Colors.blue,
height: 80.0,
);*/
}
}
}
class MediaPlayerPlaybackControls extends StatelessWidget {
final bool showMenu;
final bool showStop;
const MediaPlayerPlaybackControls({Key key, this.showMenu: true, this.showStop: false}) : super(key: key);
void _setPower(MediaPlayerEntity entity) {
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
if (entity.state == EntityState.off) {
TheLogger.debug("${entity.entityId} turn_on");
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
null));
} else {
TheLogger.debug("${entity.entityId} turn_off");
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_off", entity.entityId,
null));
}
}
}
void _callAction(MediaPlayerEntity entity, String action) {
TheLogger.debug("${entity.entityId} $action");
eventBus.fire(new ServiceCallEvent(
entity.domain, "$action", entity.entityId,
null));
}
@override
Widget build(BuildContext context) {
final MediaPlayerEntity entity = EntityModel.of(context).entityWrapper.entity;
List<Widget> result = [];
if (entity.supportTurnOn || entity.supportTurnOff) {
result.add(
IconButton(
icon: Icon(Icons.power_settings_new),
onPressed: () => _setPower(entity),
iconSize: Sizes.iconSize,
)
);
} else {
result.add(
Container(
width: Sizes.iconSize,
)
);
}
List <Widget> centeredControlsChildren = [];
if (entity.supportPreviousTrack && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
centeredControlsChildren.add(
IconButton(
icon: Icon(Icons.skip_previous),
onPressed: () => _callAction(entity, "media_previous_track"),
iconSize: Sizes.iconSize,
)
);
}
if (entity.supportPlay || entity.supportPause) {
if (entity.state == EntityState.playing) {
centeredControlsChildren.add(
IconButton(
icon: Icon(Icons.pause_circle_filled),
color: Colors.blue,
onPressed: () => _callAction(entity, "media_pause"),
iconSize: Sizes.iconSize*1.8,
)
);
} else if (entity.state == EntityState.paused || entity.state == EntityState.idle) {
centeredControlsChildren.add(
IconButton(
icon: Icon(Icons.play_circle_filled),
color: Colors.blue,
onPressed: () => _callAction(entity, "media_play"),
iconSize: Sizes.iconSize*1.8,
)
);
} else {
centeredControlsChildren.add(
Container(
width: Sizes.iconSize*1.8,
height: Sizes.iconSize*2.0,
)
);
}
}
if (entity.supportNextTrack && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
centeredControlsChildren.add(
IconButton(
icon: Icon(Icons.skip_next),
onPressed: () => _callAction(entity, "media_next_track"),
iconSize: Sizes.iconSize,
)
);
}
if (centeredControlsChildren.isNotEmpty) {
result.add(
Expanded(
child: Row(
mainAxisAlignment: showMenu ? MainAxisAlignment.center : MainAxisAlignment.end,
children: centeredControlsChildren,
)
)
);
} else {
result.add(
Expanded(
child: Container(
height: 10.0,
),
)
);
}
if (showMenu) {
result.add(
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
)
);
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
result.add(
IconButton(
icon: Icon(Icons.stop),
onPressed: () => _callAction(entity, "media_stop")
)
);
}
return Row(
children: result,
mainAxisAlignment: MainAxisAlignment.center,
);
}
}
class MediaPlayerControls extends StatefulWidget {
@override
_MediaPlayerControlsState createState() => _MediaPlayerControlsState();
}
class _MediaPlayerControlsState extends State<MediaPlayerControls> {
double _newVolumeLevel;
bool _changedHere = false;
String _newSoundMode;
String _newSource;
void _setVolume(double value, String entityId) {
setState(() {
_changedHere = true;
_newVolumeLevel = value;
eventBus.fire(ServiceCallEvent("media_player", "volume_set", entityId, {"volume_level": value}));
});
}
void _setVolumeMute(bool isMuted, String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_mute", entityId, {"is_volume_muted": isMuted}));
}
void _setVolumeUp(String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_up", entityId, null));
}
void _setVolumeDown(String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_down", entityId, null));
}
void _setSoundMode(String value, String entityId) {
setState(() {
_newSoundMode = value;
_changedHere = true;
eventBus.fire(ServiceCallEvent("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"}));
});
}
void _setSource(String source, String entityId) {
setState(() {
_newSource = source;
_changedHere = true;
eventBus.fire(ServiceCallEvent("media_player", "select_source", entityId, {"source": "$source"}));
});
}
@override
Widget build(BuildContext context) {
final MediaPlayerEntity entity = EntityModel.of(context).entityWrapper.entity;
List<Widget> children = [
MediaPlayerPlaybackControls(
showMenu: false,
)
];
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
Widget muteWidget;
Widget volumeStepWidget;
if (entity.supportVolumeMute) {
bool isMuted = entity.attributes["is_volume_muted"] ?? false;
muteWidget =
IconButton(
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up),
onPressed: () => _setVolumeMute(!isMuted, entity.entityId)
);
} else {
muteWidget = Container(width: 0.0, height: 0.0,);
}
if (entity.supportVolumeStep) {
volumeStepWidget = Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId)
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:minus")),
onPressed: () => _setVolumeDown(entity.entityId)
)
],
);
} else {
volumeStepWidget = Container(width: 0.0, height: 0.0,);
}
if (entity.supportVolumeSet) {
if (!_changedHere) {
_newVolumeLevel = entity._getDoubleAttributeValue("volume_level");
} else {
_changedHere = false;
}
children.add(
UniversalSlider(
leading: muteWidget,
closing: volumeStepWidget,
title: "Volume",
onChanged: (value) {
setState(() {
_changedHere = true;
_newVolumeLevel = value;
});
},
value: _newVolumeLevel,
onChangeEnd: (value) => _setVolume(value, entity.entityId),
max: 1.0,
min: 0.0,
)
);
} else {
children.add(Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
muteWidget,
volumeStepWidget
],
));
}
if (entity.supportSelectSoundMode && entity.soundModeList != null) {
if (!_changedHere) {
_newSoundMode = entity.attributes["sound_mode"];
} else {
_changedHere = false;
}
children.add(
ModeSelectorWidget(
options: entity.soundModeList,
caption: "Sound mode",
value: _newSoundMode,
onChange: (value) => _setSoundMode(value, entity.entityId)
)
);
}
if (entity.supportSelectSource && entity.sourceList != null) {
if (!_changedHere) {
_newSource = entity.attributes["source"];
} else {
_changedHere = false;
}
children.add(
ModeSelectorWidget(
options: entity.sourceList,
caption: "Source",
value: _newSource,
onChange: (value) => _setSource(value, entity.entityId)
)
);
}
}
return Column(
children: children,
);
}
}
class MediaPlayerProgressWidget extends StatefulWidget {
@override
_MediaPlayerProgressWidgetState createState() => _MediaPlayerProgressWidgetState();
}
class _MediaPlayerProgressWidgetState extends State<MediaPlayerProgressWidget> {
Timer _timer;
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress;
try {
DateTime lastUpdated = DateTime.parse(
entity.attributes["media_position_updated_at"]).toLocal();
Duration duration = Duration(seconds: entity._getIntAttributeValue("media_duration") ?? 1);
Duration position = Duration(seconds: entity._getIntAttributeValue("media_position") ?? 0);
int currentPosition = position.inSeconds;
if (entity.state == EntityState.playing) {
_timer?.cancel();
_timer = Timer(Duration(seconds: 1), () {
setState(() {
});
});
int differenceInSeconds = DateTime
.now()
.difference(lastUpdated)
.inSeconds;
currentPosition = currentPosition + differenceInSeconds;
} else {
_timer?.cancel();
}
progress = currentPosition / duration.inSeconds;
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
);
} catch (e) {
_timer?.cancel();
progress = 0.0;
}
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}

View File

@ -0,0 +1,70 @@
part of '../../main.dart';
class SliderControlsWidget extends StatefulWidget {
SliderControlsWidget({Key key}) : super(key: key);
@override
_SliderControlsWidgetState createState() => _SliderControlsWidgetState();
}
class _SliderControlsWidgetState extends State<SliderControlsWidget> {
int _multiplier = 1;
double _newValue;
bool _changedHere = false;
void setNewState(newValue, domain, entityId) {
setState(() {
_newValue = newValue;
_changedHere = true;
});
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
{"value": "${newValue.toString()}"}));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final SliderEntity entity = entityModel.entityWrapper.entity;
if (entity.valueStep < 1) {
_multiplier = 10;
} else if (entity.valueStep < 0.1) {
_multiplier = 100;
}
if (!_changedHere) {
_newValue = entity.doubleState;
} else {
_changedHere = false;
}
Widget slider = Slider(
min: entity.minValue * _multiplier,
max: entity.maxValue * _multiplier,
value: (_newValue <= entity.maxValue) &&
(_newValue >= entity.minValue)
? _newValue * _multiplier
: entity.minValue * _multiplier,
onChanged: (value) {
setState(() {
_newValue = (value.roundToDouble() / _multiplier);
_changedHere = true;
});
},
onChangeEnd: (value) {
setNewState(value.roundToDouble() / _multiplier, entity.domain, entity.entityId);
},
);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"$_newValue",
style: TextStyle(
fontSize: Sizes.largeFontSize,
color: Colors.blue
),
),
slider
],
);
}
}

View File

@ -3,21 +3,33 @@ part of '../main.dart';
class DefaultEntityContainer extends StatelessWidget {
DefaultEntityContainer({
Key key,
@required this.state,
@required this.height
@required this.state
}) : super(key: key);
final Widget state;
final double height;
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
final EntityModel entityModel = EntityModel.of(context);
return InkWell(
onLongPress: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleHold();
}
},
onTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleTap();
}
},
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
EntityIcon(),
Expanded(
Flexible(
fit: FlexFit.tight,
flex: 3,
child: EntityName(),
),
state

View File

@ -1,22 +1,28 @@
part of '../main.dart';
class EntityColors {
class EntityColor {
static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
};
static const _stateColors = {
"on": Colors.amber,
EntityState.on: Colors.amber,
"auto": Colors.amber,
"idle": Colors.amber,
"playing": Colors.amber,
EntityState.idle: Colors.amber,
EntityState.playing: Colors.amber,
"above_horizon": Colors.amber,
"home": Colors.amber,
"open": Colors.amber,
"off": Color.fromRGBO(68, 115, 158, 1.0),
"closed": Color.fromRGBO(68, 115, 158, 1.0),
EntityState.home: Colors.amber,
EntityState.open: Colors.amber,
EntityState.off: Color.fromRGBO(68, 115, 158, 1.0),
EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0),
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0),
"default": Color.fromRGBO(68, 115, 158, 1.0),
"heat": Colors.redAccent,
"cool": Colors.lightBlue,
"unavailable": Colors.black26,
"unknown": Colors.black26,
EntityState.unavailable: Colors.black26,
EntityState.unknown: Colors.black26,
};
static Color stateColor(String state) {
@ -33,7 +39,8 @@ class EntityColors {
a: c.alpha
);
} else {
return charts.MaterialPalette.getOrderedPalettes(id+1)[id].shadeDefault;
double r = id.toDouble() % 10;
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
}
}
@ -43,10 +50,11 @@ class EntityColors {
return c;
} else {
if (id > -1) {
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(id + 1)[id].shadeDefault;
double r = id.toDouble() % 10;
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
} else {
return _stateColors["on"];
return _stateColors[EntityState.on];
}
}
}

View File

@ -1,22 +1,23 @@
part of '../main.dart';
class EntityIcon extends StatelessWidget {
final EdgeInsetsGeometry padding;
final double iconSize;
const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key);
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return GestureDetector(
child: Padding(
padding: EdgeInsets.fromLTRB(
Entity.leftWidgetPadding, 0.0, 12.0, 0.0),
child: MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entity,
Entity.iconSize,
EntityColors.stateColor(entityModel.entity.state)
),
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: MaterialDesignIcons.createIconWidgetFromEntityData(
entityWrapper,
iconSize,
EntityColor.stateColor(entityWrapper.entity.state)
),
onTap: () => entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entityModel.entity))
: null,
);
}
}

View File

@ -1,23 +1,29 @@
part of '../main.dart';
class EntityName extends StatelessWidget {
final EdgeInsetsGeometry padding;
final TextOverflow textOverflow;
final bool wordsWrap;
final double fontSize;
final TextAlign textAlign;
final int maxLines;
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return GestureDetector(
child: Padding(
padding: EdgeInsets.only(right: 10.0),
child: Text(
"${entityModel.entity.displayName}",
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(fontSize: Entity.nameFontSize),
),
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: Text(
"${entityWrapper.displayName}",
overflow: textOverflow,
softWrap: wordsWrap,
maxLines: maxLines,
style: TextStyle(fontSize: fontSize),
textAlign: textAlign,
),
onTap: () =>
entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entityModel.entity))
: null,
);
}
}

View File

@ -0,0 +1,84 @@
part of '../main.dart';
class GlanceEntityContainer extends StatelessWidget {
final bool showName;
final bool showState;
final bool nameInTheBottom;
final double iconSize;
final double nameFontSize;
final bool wordsWrapInName;
GlanceEntityContainer({
Key key,
@required this.showName,
@required this.showState,
this.nameInTheBottom: false,
this.iconSize: Sizes.iconSize,
this.nameFontSize: Sizes.smallFontSize,
this.wordsWrapInName: false
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
List<Widget> result = [];
if (!nameInTheBottom) {
if (showName) {
result.add(_buildName());
}
} else {
if (showState) {
result.add(_buildState());
}
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
iconSize: iconSize,
)
);
if (!nameInTheBottom) {
if (showState) {
result.add(_buildState());
}
} else {
result.add(_buildName());
}
return Center(
child: InkResponse(
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
child: Column(
mainAxisSize: MainAxisSize.min,
//mainAxisAlignment: MainAxisAlignment.start,
//crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
),
);
}
Widget _buildName() {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: wordsWrapInName,
textAlign: TextAlign.center,
fontSize: nameFontSize,
);
}
Widget _buildState() {
return SimpleEntityState(
textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
padding: EdgeInsets.only(top: Sizes.rowPadding),
);
}
}

View File

@ -69,7 +69,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
listener: (model) => _onSelectionChanged(model),
changedListener: (model) => _onSelectionChanged(model),
)
],
customSeriesRenderers: [
@ -156,7 +156,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
result.add(
new charts.Series<EntityHistoryMoment, DateTime>(
id: "value",
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("_", historyMoment.colorId),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor("_", historyMoment.colorId),
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
if (historyMoment.hiddenDot) {
return 0.0;
@ -179,7 +179,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'state',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),

View File

@ -31,6 +31,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
List _history;
bool _needToUpdateHistory;
DateTime _historyLastUpdated;
@override
void initState() {
@ -39,25 +40,32 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
}
void _loadHistory(HomeAssistant ha, String entityId) {
ha.getHistory(entityId).then((history){
setState(() {
_history = history.isNotEmpty ? history[0] : [];
_needToUpdateHistory = false;
DateTime now = DateTime.now();
if (_historyLastUpdated != null) {
TheLogger.debug("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
}
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now;
ha.getHistory(entityId).then((history){
setState(() {
_history = history.isNotEmpty ? history[0] : [];
_needToUpdateHistory = false;
});
}).catchError((e) {
TheLogger.error("Error loading $entityId history: $e");
setState(() {
_history = [];
_needToUpdateHistory = false;
});
});
}).catchError((e) {
TheLogger.error("Error loading $entityId history: $e");
setState(() {
_history = [];
_needToUpdateHistory = false;
});
});
}
}
@override
Widget build(BuildContext context) {
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
final EntityModel entityModel = EntityModel.of(context);
final Entity entity = entityModel.entity;
final Entity entity = entityModel.entityWrapper.entity;
if (!_needToUpdateHistory) {
_needToUpdateHistory = true;
} else {
@ -83,7 +91,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
}
children.add(Divider());
return Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, Entity.rowPadding),
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, Sizes.rowPadding),
child: Column(
children: children,
),
@ -91,18 +99,15 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
}
Widget _selectChartWidget() {
TheLogger.debug(" selecting history widget (${widget.config.chartType})");
switch (widget.config.chartType) {
case EntityHistoryWidgetType.simple: {
TheLogger.debug(" Simple selected");
return SimpleStateHistoryChartWidget(
rawHistory: _history,
);
}
case EntityHistoryWidgetType.numericState: {
TheLogger.debug(" EntityHistory selected");
return NumericStateHistoryChartWidget(
rawHistory: _history,
config: widget.config,
@ -110,7 +115,6 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
}
case EntityHistoryWidgetType.numericAttributes: {
TheLogger.debug(" NumericAttributes selected");
return CombinedHistoryChartWidget(
rawHistory: _history,
config: widget.config,

View File

@ -55,7 +55,7 @@ class HistoryControlWidget extends StatelessWidget {
textAlign: TextAlign.right,
style: TextStyle(
fontWeight: FontWeight.bold,
color: EntityColors.historyStateColor(selectedStates[i], colorIndexes[i]),
color: EntityColor.historyStateColor(selectedStates[i], colorIndexes[i]),
fontSize: 22.0
),
)

View File

@ -57,7 +57,7 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
listener: (model) => _onSelectionChanged(model),
changedListener: (model) => _onSelectionChanged(model),
)
],
),
@ -108,7 +108,7 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
return [
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor("on", -1),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(EntityState.on, -1),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
data: data,

View File

@ -53,7 +53,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
listener: (model) => _onSelectionChanged(model),
changedListener: (model) => _onSelectionChanged(model),
)
],
customSeriesRenderers: [
@ -107,7 +107,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
@ -115,7 +115,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
@ -123,7 +123,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColors.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,

View File

@ -3,12 +3,12 @@ part of '../main.dart';
class EntityModel extends InheritedWidget {
const EntityModel({
Key key,
@required this.entity,
@required this.entityWrapper,
@required this.handleTap,
@required Widget child,
}) : super(key: key, child: child);
final Entity entity;
final EntityWrapper entityWrapper;
final bool handleTap;
static EntityModel of(BuildContext context) {

View File

@ -9,16 +9,19 @@ class ButtonStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return FlatButton(
onPressed: (() {
_setNewState(entityModel.entity);
}),
child: Text(
"EXECUTE",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Entity.stateFontSize, color: Colors.blue),
),
return SizedBox(
height: 34.0,
child: FlatButton(
onPressed: (() {
_setNewState(entityModel.entityWrapper.entity);
}),
child: Text(
"EXECUTE",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
),
)
);
}
}

View File

@ -4,7 +4,7 @@ class ClimateStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entity;
final ClimateEntity entity = entityModel.entityWrapper.entity;
String targetTemp = "-";
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
targetTemp = "${entity.temperature}";
@ -18,40 +18,35 @@ class ClimateStateWidget extends StatelessWidget {
}
return Padding(
padding: EdgeInsets.fromLTRB(
0.0, 0.0, Entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Text("${entity.state}",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: Entity.stateFontSize,
)),
Text(" $targetTemp",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
))
],
),
entity.attributes["current_temperature"] != null ?
Text("Currently: ${entity.attributes["current_temperature"]}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
color: Colors.black45)
) :
Container(height: 0.0,)
],
),
onTap: () => entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entity))
: null,
0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Text("${entity.state}",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: Sizes.stateFontSize,
)),
Text(" $targetTemp",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
))
],
),
entity.attributes["current_temperature"] != null ?
Text("Currently: ${entity.attributes["current_temperature"]}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
color: Colors.black45)
) :
Container(height: 0.0,)
],
));
}
}

View File

@ -19,42 +19,42 @@ class CoverStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entity;
final CoverEntity entity = entityModel.entityWrapper.entity;
List<Widget> buttons = [];
if (entity.supportOpen) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"),
size: Entity.iconSize,
size: Sizes.iconSize,
),
onPressed: entity.canBeOpened ? () => _open(entity) : null));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
width: Sizes.iconSize + 20.0,
));
}
if (entity.supportStop) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
size: Entity.iconSize,
size: Sizes.iconSize,
),
onPressed: () => _stop(entity)));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
width: Sizes.iconSize + 20.0,
));
}
if (entity.supportClose) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"),
size: Entity.iconSize,
size: Sizes.iconSize,
),
onPressed: entity.canBeClosed ? () => _close(entity) : null));
} else {
buttons.add(Container(
width: Entity.iconSize + 20.0,
width: Sizes.iconSize + 20.0,
));
}

View File

@ -4,14 +4,14 @@ class DateTimeStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final DateTimeEntity entity = entityModel.entity;
final DateTimeEntity entity = entityModel.entityWrapper.entity;
return Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, Entity.rightWidgetPadding, 0.0),
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Text("${entity.formattedState}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
fontSize: Sizes.stateFontSize,
)),
onTap: () => _handleStateTap(context, entity),
));

View File

@ -0,0 +1,32 @@
part of '../../main.dart';
class LockStateWidget extends StatelessWidget {
void _lock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
}
void _unlock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final LockEntity entity = entityModel.entityWrapper.entity;
return SizedBox(
height: 34.0,
child: FlatButton(
onPressed: (() {
entity.isLocked ? _unlock(entity) : _lock(entity);
}),
child: Text(
entity.isLocked ? "UNLOCK" : "LOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
),
)
);
}
}

View File

@ -18,11 +18,12 @@ class _SelectStateWidgetState extends State<SelectStateWidget> {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final SelectEntity entity = entityModel.entity;
final SelectEntity entity = entityModel.entityWrapper.entity;
Widget ctrl;
if (entity.listOptions.isNotEmpty) {
ctrl = DropdownButton<String>(
value: entity.state,
isExpanded: true,
items: entity.listOptions.map((String value) {
return new DropdownMenuItem<String>(
value: value,
@ -36,7 +37,9 @@ class _SelectStateWidgetState extends State<SelectStateWidget> {
} else {
ctrl = Text('---');
}
return Expanded(
return Flexible(
flex: 2,
fit: FlexFit.tight,
//width: Entity.INPUT_WIDTH,
child: ctrl,
);

View File

@ -1,22 +1,43 @@
part of '../../main.dart';
class SimpleEntityState extends StatelessWidget {
final bool expanded;
final TextAlign textAlign;
final EdgeInsetsGeometry padding;
final int maxLines;
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);
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
return Padding(
padding: EdgeInsets.fromLTRB(
0.0, 0.0, Entity.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Text(
"${entityModel.entity.state}${entityModel.entity.unitOfMeasurement}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Entity.stateFontSize,
)),
onTap: () => entityModel.handleTap
? eventBus.fire(new ShowEntityPageEvent(entityModel.entity))
: null,
));
String state = entityModel.entityWrapper.entity.state ?? "";
state = state.replaceAll("\n", "").replaceAll("\t", " ").trim();
while (state.contains(" ")){
state = state.replaceAll(" ", " ");
}
Widget result = Padding(
padding: padding,
child: Text(
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
textAlign: textAlign,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
)
)
);
if (expanded) {
return Flexible(
fit: FlexFit.tight,
flex: 2,
child: result,
);
} else {
return result;
}
}
}
}

View File

@ -1,58 +0,0 @@
part of '../../main.dart';
class SliderStateWidget extends StatefulWidget {
final bool expanded;
SliderStateWidget({Key key, @required this.expanded}) : super(key: key);
@override
_SliderStateWidgetState createState() => _SliderStateWidgetState();
}
class _SliderStateWidgetState extends State<SliderStateWidget> {
int _multiplier = 1;
void setNewState(newValue, domain, entityId) {
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
{"value": "${newValue.toString()}"}));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final SliderEntity entity = entityModel.entity;
if (entity.valueStep < 1) {
_multiplier = 10;
} else if (entity.valueStep < 0.1) {
_multiplier = 100;
}
Widget slider = Slider(
min: entity.minValue * _multiplier,
max: entity.maxValue * _multiplier,
value: (entity.doubleState <= entity.maxValue) &&
(entity.doubleState >= entity.minValue)
? entity.doubleState * _multiplier
: entity.minValue * _multiplier,
onChanged: (value) {
setState(() {
entity.state =
(value.roundToDouble() / _multiplier).toString();
});
eventBus.fire(new StateChangedEvent(entity.entityId,
(value.roundToDouble() / _multiplier).toString(), true));
},
onChangeEnd: (value) {
setNewState(value.roundToDouble() / _multiplier, entity.domain, entity.entityId);
},
);
if (widget.expanded) {
return Expanded(
child: slider,
);
} else {
return slider;
}
}
}

View File

@ -1,12 +1,20 @@
part of '../../main.dart';
class SwitchStateWidget extends StatefulWidget {
final String domainForService;
const SwitchStateWidget({Key key, this.domainForService}) : super(key: key);
@override
_SwitchStateWidgetState createState() => _SwitchStateWidgetState();
}
class _SwitchStateWidgetState extends State<SwitchStateWidget> {
String newState;
bool updatedHere = false;
@override
void initState() {
super.initState();
@ -14,47 +22,68 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
void _setNewState(newValue, Entity entity) {
setState(() {
entity.assumedState = newValue ? 'on' : 'off';
newState = newValue ? EntityState.on : EntityState.off;
updatedHere = true;
});
Timer(Duration(seconds: 2), (){
setState(() {
entity.assumedState = entity.state;
newState = entity.state;
updatedHere = true;
//TheLogger.debug("Timer@!!");
});
});
String domain;
if (widget.domainForService != null) {
domain = widget.domainForService;
} else {
domain = entity.domain;
}
eventBus.fire(new ServiceCallEvent(
entity.domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final entity = entityModel.entity;
if ((entity.attributes["assumed_state"] == null) || (entity.attributes["assumed_state"] == false)) {
return Switch(
value: entity.assumedState == 'on',
onChanged: ((switchState) {
_setNewState(switchState, entity);
}),
final entity = entityModel.entityWrapper.entity;
if (!updatedHere) {
newState = entity.state;
} else {
updatedHere = false;
}
if (entity.state == EntityState.unavailable || entity.state == EntityState.unknown) {
return SimpleEntityState();
} else if ((entity.attributes["assumed_state"] == null) || (entity.attributes["assumed_state"] == false)) {
return SizedBox(
height: 32.0,
child: Switch(
value: newState == EntityState.on,
onChanged: ((switchState) {
_setNewState(switchState, entity);
}),
)
);
} else {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
IconButton(
onPressed: () => _setNewState(false, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")),
color: entity.assumedState == 'on' ? Colors.black : Colors.blue,
iconSize: Entity.iconSize,
),
IconButton(
onPressed: () => _setNewState(true, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")),
color: entity.assumedState == 'on' ? Colors.blue : Colors.black,
iconSize: Entity.iconSize
)
],
return SizedBox(
height: 32.0,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
IconButton(
onPressed: () => _setNewState(false, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")),
color: newState == EntityState.on ? Colors.black : Colors.blue,
iconSize: Sizes.iconSize,
),
IconButton(
onPressed: () => _setNewState(true, entity),
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")),
color: newState == EntityState.on ? Colors.blue : Colors.black,
iconSize: Sizes.iconSize
)
],
),
);
}
}
}

View File

@ -55,7 +55,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final TextEntity entity = entityModel.entity;
final TextEntity entity = entityModel.entityWrapper.entity;
_entityState = entity.state;
_entityDomain = entity.domain;
_entityId = entity.entityId;
@ -66,7 +66,9 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
_tmpValue = entity.state;
}
if (entity.isTextField || entity.isPasswordField) {
return Expanded(
return Flexible(
fit: FlexFit.tight,
flex: 2,
//width: Entity.INPUT_WIDTH,
child: TextField(
focusNode: _focusNode,

View File

@ -4,7 +4,7 @@ class HomeAssistant {
String _webSocketAPIEndpoint;
String _password;
String _authType;
bool _useLovelace;
bool _useLovelace = false;
IOWebSocketChannel _hassioChannel;
SendMessageQueue _messageQueue;
@ -40,7 +40,13 @@ class HomeAssistant {
Duration fetchTimeout = Duration(seconds: 30);
Duration connectTimeout = Duration(seconds: 15);
String get locationName => _instanceConfig["location_name"] ?? "";
String get locationName {
if (_useLovelace) {
return ui?.title ?? "";
} else {
return _instanceConfig["location_name"] ?? "";
}
}
String get userName => _userName ?? locationName;
String get userAvatarText => userName.length > 0 ? userName[0] : "";
//int get viewsCount => entities.views.length ?? 0;
@ -214,6 +220,7 @@ class HomeAssistant {
} else if (data["type"] == "auth_invalid") {
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
TheLogger.debug("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
if (data["id"] == _configMessageId) {
_parseConfig(data);
} else if (data["id"] == _statesMessageId) {
@ -224,12 +231,10 @@ class HomeAssistant {
_parseServices(data);
} else if (data["id"] == _userInfoMessageId) {
_parseUserInfo(data);
} else if (data["id"] == _currentMessageId) {
TheLogger.debug("[Received] => Request id:$_currentMessageId was successful");
}
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
TheLogger.debug("[Received] => ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
TheLogger.debug("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
_handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) {
TheLogger.warning("Unhandled event type: ${data["event"]["event_type"]}");
@ -322,17 +327,40 @@ class HomeAssistant {
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_incrementMessageId();
String message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value){
if ((value is double) || (value is int) || (value is List)) {
message += ', "$name" : $value';
} else {
message += ', "$name" : "$value"';
}
});
String message = "";
if (entityId != null) {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value) {
if ((value is double) || (value is int) || (value is List)) {
message += ', "$name" : $value';
} else {
message += ', "$name" : "$value"';
}
});
}
message += '}}';
} else {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"';
if (additionalParams != null && additionalParams.isNotEmpty) {
message += ', "service_data": {';
bool first = true;
additionalParams.forEach((name, value) {
if (!first) {
message += ', ';
}
if ((value is double) || (value is int) || (value is List)) {
message += '"$name" : $value';
} else {
message += '"$name" : "$value"';
}
first = false;
});
message += '}';
}
message += '}';
}
message += '}}';
return _sendMessageRaw(message, true);
}
@ -340,7 +368,7 @@ class HomeAssistant {
//TheLogger.debug( "New state for ${eventData['entity_id']}");
Map data = Map.from(eventData);
entities.updateState(data);
eventBus.fire(new StateChangedEvent(data["entity_id"], null, false));
eventBus.fire(new StateChangedEvent(data["entity_id"], null));
}
void _parseConfig(Map data) {
@ -357,6 +385,7 @@ class HomeAssistant {
_userName = data["result"]["name"];
} else {
_userName = null;
TheLogger.warning("There was an error getting current user: $data");
}
_userInfoCompleter.complete();
}
@ -369,15 +398,15 @@ class HomeAssistant {
if (response["success"] == true) {
_rawLovelaceData = response["result"];
} else {
TheLogger.error("There was an error getting Lovelace config: $response");
_rawLovelaceData = null;
}
_lovelaceCompleter.complete();
}
void _parseLovelace() {
ui = HomeAssistantUI();
TheLogger.debug("Parsing lovelace config");
TheLogger.debug("--Title: ${_rawLovelaceData["title"]}");
ui.title = _rawLovelaceData["title"];
int viewCounter = 0;
TheLogger.debug("--Views count: ${_rawLovelaceData['views'].length}");
_rawLovelaceData["views"].forEach((rawView){
@ -399,32 +428,63 @@ class HomeAssistant {
List<HACard> _createLovelaceCards(List rawCards) {
List<HACard> result = [];
rawCards.forEach((rawCard){
bool isThereCardOptionsInside = rawCard["card"] != null;
HACard card = HACard(
id: "card",
name: isThereCardOptionsInside ? rawCard["card"]["title"] ?? rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
type: isThereCardOptionsInside ? rawCard["card"]['type'] : rawCard['type'],
columnsCount: isThereCardOptionsInside ? rawCard["card"]['columns'] ?? 4 : rawCard['columns'] ?? 4,
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ?? true : rawCard['show_name'] ?? true,
showState: isThereCardOptionsInside ? rawCard["card"]['show_state'] ?? true : rawCard['show_state'] ?? true,
showEmpty: rawCard['show_empty'] ?? true,
stateFilter: rawCard['state_filter'] ?? []
);
if (rawCard["cards"] != null) {
TheLogger.debug("------card: ${rawCard['type']} has child cards");
result.addAll(_createLovelaceCards(rawCard["cards"]));
} else {
TheLogger.debug("------card: ${rawCard['type']}");
HACard card = HACard(
id: "card",
name: rawCard["title"],
type: rawCard['type']
);
rawCard["entities"]?.forEach((rawEntity) {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(entities.get(rawEntity));
}
} else {
if (entities.isExist(rawEntity["entity"])) {
card.entities.add(entities.get(rawEntity["entity"]));
}
}
});
if (rawCard["entity"] != null) {
card.linkedEntity = entities.get(rawCard["entity"]);
}
result.add(card);
card.childCards = _createLovelaceCards(rawCard["cards"]);
}
rawCard["entities"]?.forEach((rawEntity) {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
}
} else {
if (entities.isExist(rawEntity["entity"])) {
Entity e = entities.get(rawEntity["entity"]);
card.entities.add(
EntityWrapper(
entity: e,
displayName: rawEntity["name"],
icon: rawEntity["icon"],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
}
}
});
if (rawCard["entity"] != null) {
var en = rawCard["entity"];
if (en is String) {
if (entities.isExist(en)) {
Entity e = entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
uiAction: EntityUIAction(rawEntityData: rawCard)
);
}
} else {
if (entities.isExist(en["entity"])) {
Entity e = entities.get(en["entity"]);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: en["icon"],
displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
}
}
}
result.add(card);
});
return result;
}
@ -439,13 +499,14 @@ class HomeAssistant {
}
void _createUI() {
ui = HomeAssistantUI();
if ((_useLovelace) && (_rawLovelaceData != null)) {
TheLogger.debug("Creating Lovelace UI");
_parseLovelace();
} else {
ui = HomeAssistantUI();
TheLogger.debug("Creating group-based UI");
int viewCounter = 0;
if (!entities.hasDefaultView) {
TheLogger.debug( "--Default view");
HAView view = HAView(
count: viewCounter,
id: "group.default_view",
@ -458,7 +519,6 @@ class HomeAssistant {
viewCounter += 1;
}
entities.viewEntities.forEach((viewEntity) {
TheLogger.debug( "--View: ${viewEntity.entityId}");
HAView view = HAView(
count: viewCounter,
id: viewEntity.entityId,
@ -483,7 +543,7 @@ class HomeAssistant {
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
TheLogger.debug( "$url");
TheLogger.debug("[Sending] ==> $url");
http.Response historyResponse;
if (_authType == "access_token") {
historyResponse = await http.get(url, headers: {
@ -498,7 +558,7 @@ class HomeAssistant {
}
var history = json.decode(historyResponse.body);
if (history is List) {
TheLogger.debug( "Got ${history.first.length} history recors");
TheLogger.debug( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];

View File

@ -38,15 +38,7 @@ class _LogViewPageState extends State<LogViewPage> {
onPressed: () {
Clipboard.setData(new ClipboardData(text: _logData));
},
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")),
onPressed: () {
String body = "```\n$_logData```";
String encodedBody = "${Uri.encodeFull(body)}";
HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new?body=$encodedBody");
},
),
)
],
),
body: TextField(

View File

@ -4,7 +4,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:web_socket_channel/io.dart';
import 'package:progress_indicators/progress_indicators.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/widgets.dart';
import 'package:cached_network_image/cached_network_image.dart';
@ -14,8 +13,11 @@ import 'package:date_format/date_format.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_colorpicker/material_picker.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart';
part 'entity_class/const.dart';
part 'entity_class/entity.class.dart';
part 'entity_class/entity_wrapper.class.dart';
part 'entity_class/switch_entity.class.dart';
part 'entity_class/button_entity.class.dart';
part 'entity_class/text_entity.class.dart';
@ -27,15 +29,21 @@ part 'entity_class/select_entity.class.dart';
part 'entity_class/other_entity.class.dart';
part 'entity_class/slider_entity.dart';
part 'entity_class/media_player_entity.class.dart';
part 'entity_widgets/badge.dart';
part 'entity_class/lock_entity.class.dart';
part 'entity_class/group_entity.class.dart';
part 'entity_class/fan_entity.class.dart';
part 'entity_widgets/common/badge.dart';
part 'entity_widgets/model_widgets.dart';
part 'entity_widgets/default_entity_container.dart';
part 'entity_widgets/entity_attributes_list.dart';
part 'entity_widgets/glance_entity_container.dart';
part 'entity_widgets/button_entity_container.dart';
part 'entity_widgets/common/entity_attributes_list.dart';
part 'entity_widgets/entity_icon.dart';
part 'entity_widgets/entity_name.dart';
part 'entity_widgets/last_updated.dart';
part 'entity_widgets/mode_swicth.dart';
part 'entity_widgets/mode_selector.dart';
part 'entity_widgets/common/last_updated.dart';
part 'entity_widgets/common/mode_swicth.dart';
part 'entity_widgets/common/mode_selector.dart';
part 'entity_widgets/common/universal_slider.dart';
part 'entity_widgets/entity_colors.class.dart';
part 'entity_widgets/entity_page_container.dart';
part 'entity_widgets/history_chart/entity_history.dart';
@ -45,7 +53,7 @@ part 'entity_widgets/history_chart/combined_history_chart.dart';
part 'entity_widgets/history_chart/history_control_widget.dart';
part 'entity_widgets/history_chart/entity_history_moment.dart';
part 'entity_widgets/state/switch_state.dart';
part 'entity_widgets/state/slider_state.dart';
part 'entity_widgets/controls/slider_controls.dart';
part 'entity_widgets/state/text_input_state.dart';
part 'entity_widgets/state/select_state.dart';
part 'entity_widgets/state/simple_state.dart';
@ -53,9 +61,12 @@ part 'entity_widgets/state/climate_state.dart';
part 'entity_widgets/state/cover_state.dart';
part 'entity_widgets/state/date_time_state.dart';
part 'entity_widgets/state/button_state.dart';
part 'entity_widgets/state/lock_state.dart';
part 'entity_widgets/controls/climate_controls.dart';
part 'entity_widgets/controls/cover_controls.dart';
part 'entity_widgets/controls/light_controls.dart';
part 'entity_widgets/controls/media_player_widgets.dart';
part 'entity_widgets/controls/fan_controls.dart';
part 'settings.page.dart';
part 'home_assistant.class.dart';
part 'log.page.dart';
@ -66,16 +77,15 @@ part 'entity_collection.class.dart';
part 'ui_class/ui.dart';
part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart';
part 'ui_class/sizes_class.dart';
part 'ui_widgets/view.dart';
part 'ui_widgets/entities_card.dart';
part 'ui_widgets/unsupported_card.dart';
part 'ui_widgets/media_control_card.dart';
part 'ui_widgets/card_widget.dart';
part 'ui_widgets/card_header_widget.dart';
EventBus eventBus = new EventBus();
const String appName = "HA Client";
const appVersion = "0.3.5";
const appVersion = "0.3.12";
String homeAssistantWebHost;
@ -128,7 +138,6 @@ class MainPage extends StatefulWidget {
class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
HomeAssistant _homeAssistant;
EntityCollection _entities;
//Map _instanceConfig;
String _webSocketApiEndpoint;
String _password;
@ -141,7 +150,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
StreamSubscription _showEntityPageSubscription;
StreamSubscription _refreshDataSubscription;
StreamSubscription _showErrorSubscription;
int _isLoading = 1;
bool _settingsLoaded = false;
bool _accountMenuExpanded = false;
bool _useLovelaceUI;
@ -152,6 +160,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_settingsLoaded = false;
WidgetsBinding.instance.addObserver(this);
TheLogger.debug("<!!!> Creating new HomeAssistant instance");
_homeAssistant = HomeAssistant();
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
@ -170,10 +179,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_subscribe();
_refreshData();
}, onError: (_) {
setState(() {
_isLoading = 2;
});
_showErrorSnackBar(message: _, errorCode: 5);
_showErrorBottomBar(message: _, errorCode: 5);
});
}
@ -205,15 +211,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_subscribe() {
if (_stateSubscription == null) {
//TODO Move to homeAssistant or remove
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
setState(() {
if (event.localChange) {
_entities
.get(event.entityId)
.state = event.newState;
}
});
setState(() {});
});
}
if (_serviceCallSubscription == null) {
@ -227,7 +226,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
if (_showEntityPageSubscription == null) {
_showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity);
_showEntityPage(event.entity.entityId);
});
}
@ -239,25 +238,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorSnackBar(message: event.text, errorCode: event.errorCode);
_showErrorBottomBar(message: event.text, errorCode: event.errorCode);
});
}
}
_refreshData() async {
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _authType, _useLovelaceUI);
setState(() {
_hideErrorSnackBar();
_isLoading = 1;
});
_hideBottomBar();
_showInfoBottomBar(progress: true,);
await _homeAssistant.fetch().then((result) {
setState(() {
//_instanceConfig = _homeAssistant.instanceConfig;
_entities = _homeAssistant.entities;
//_uiViewsCount = _homeAssistant.viewsCount;
//TheLogger.debug("_uiViewsCount=$_uiViewsCount");
_isLoading = 0;
});
_hideBottomBar();
}).catchError((e) {
_setErrorState(e);
});
@ -265,18 +256,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}
_setErrorState(e) {
setState(() {
_isLoading = 2;
});
if (e is Error) {
TheLogger.error(e.toString());
TheLogger.error("${e.stackTrace}");
_showErrorSnackBar(
_showErrorBottomBar(
message: "There was some error",
errorCode: 13
);
} else {
_showErrorSnackBar(
_showErrorBottomBar(
message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error",
errorCode: e["errorCode"] != null ? e["errorCode"] : 99
);
@ -284,14 +272,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}
void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_showInfoBottomBar(
message: "Calling $domain.$service",
duration: Duration(seconds: 3)
);
_homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e));
}
void _showEntityPage(Entity entity) {
void _showEntityPage(String entityId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EntityViewPage(entity: entity, homeAssistant: _homeAssistant),
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: _homeAssistant),
)
);
}
@ -301,61 +293,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
if (_homeAssistant.ui.views.isNotEmpty) {
_homeAssistant.ui.views.forEach((HAView view) {
if (view.linkedEntity == null) {
result.add(
Tab(
icon:
Icon(
MaterialDesignIcons.createIconDataFromIconName(
view.iconName ?? "mdi:home-assistant"),
size: 24.0,
)
)
);
} else {
result.add(
Tab(
icon: MaterialDesignIcons.createIconWidgetFromEntityData(
view.linkedEntity, 24.0, null) ??
Icon(
MaterialDesignIcons.createIconDataFromIconName(
"mdi:home-assistant"),
size: 24.0,
)
)
);
}
result.add(view.buildTab());
});
}
return result;
}
Widget _buildAppTitle() {
Row titleRow = Row(
children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")],
);
if (_isLoading == 1) {
titleRow.children.add(Padding(
child: JumpingDotsProgressIndicator(
fontSize: 26.0,
color: Colors.white,
),
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 30.0),
));
} else if (_isLoading == 2) {
titleRow.children.add(Padding(
child: Icon(
Icons.error_outline,
size: 20.0,
color: Colors.red,
),
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 0.0),
));
}
return titleRow;
}
Drawer _buildAppDrawer() {
List<Widget> menuItems = [];
menuItems.add(
@ -404,14 +348,29 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
title: Text("Report an issue"),
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new");
HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
},
),
Divider(),
new AboutListTile(
aboutBoxChildren: <Widget>[
GestureDetector(
onTap: () {
Navigator.of(context).pop();
HAUtils.launchURL("http://www.keyboardcrumbs.io/");
},
child: Text(
"www.keyboardcrumbs.io",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
)
],
applicationName: appName,
applicationVersion: appVersion,
applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io",
applicationLegalese: "Keyboard Crumbs",
)
]);
}
@ -422,21 +381,51 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
);
}
void _hideErrorSnackBar() {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
void _hideBottomBar() {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
setState(() {
_showBottomBar = false;
});
}
void _showErrorSnackBar({Key key, @required String message, @required int errorCode}) {
SnackBarAction action;
Widget _bottomBarAction;
bool _showBottomBar = false;
String _bottomBarText;
bool _bottomBarProgress;
Color _bottomBarColor;
Timer _bottomBarTimer;
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
_bottomBarTimer?.cancel();
_bottomBarAction = Container(height: 0.0, width: 0.0,);
_bottomBarColor = Colors.grey.shade50;
setState(() {
_bottomBarText = message;
_bottomBarProgress = progress;
_showBottomBar = true;
});
if (duration != null) {
_bottomBarTimer = Timer(duration, () {
_hideBottomBar();
});
}
}
void _showErrorBottomBar({Key key, @required String message, @required int errorCode}) {
TextStyle textStyle = TextStyle(
color: Colors.blue,
fontSize: Sizes.nameFontSize
);
_bottomBarColor = Colors.red.shade100;
switch (errorCode) {
case 9:
case 11:
case 7:
case 1: {
action = SnackBarAction(
label: "Retry",
_bottomBarAction = FlatButton(
child: Text("Retry", style: textStyle),
onPressed: () {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
@ -445,10 +434,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
case 5: {
message = "Check connection settings";
action = SnackBarAction(
label: "Open",
_bottomBarAction = FlatButton(
child: Text("Open", style: textStyle),
onPressed: () {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
Navigator.pushNamed(context, '/connection-settings');
},
);
@ -456,10 +445,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}
case 6: {
action = SnackBarAction(
label: "Settings",
_bottomBarAction = FlatButton(
child: Text("Settings", style: textStyle),
onPressed: () {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
Navigator.pushNamed(context, '/connection-settings');
},
);
@ -467,10 +456,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}
case 10: {
action = SnackBarAction(
label: "Refresh",
_bottomBarAction = FlatButton(
child: Text("Refresh", style: textStyle),
onPressed: () {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
@ -478,10 +467,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}
case 8: {
action = SnackBarAction(
label: "Reconnect",
_bottomBarAction = FlatButton(
child: Text("Reconnect", style: textStyle),
onPressed: () {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
@ -489,76 +478,146 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}
default: {
action = SnackBarAction(
label: "Reload",
_bottomBarAction = FlatButton(
child: Text("Reload", style: textStyle),
onPressed: () {
_scaffoldKey?.currentState?.hideCurrentSnackBar();
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
_refreshData();
},
);
break;
}
}
_scaffoldKey.currentState.hideCurrentSnackBar();
setState(() {
_bottomBarProgress = false;
_bottomBarText = "$message (code: $errorCode)";
_showBottomBar = true;
});
/*_scaffoldKey.currentState.hideCurrentSnackBar();
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text("$message (code: $errorCode)"),
action: action,
duration: Duration(hours: 1),
)
);
);*/
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Scaffold _buildScaffold(bool empty) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: _buildAppTitle(),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
setState(() {
_accountMenuExpanded = false;
});
},
),
primary: true,
bottom: empty ? null : TabBar(
tabs: buildUIViewTabs(),
isScrollable: true,
),
),
drawer: _buildAppDrawer(),
body: empty ?
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
size: 100.0,
color: _isLoading == 2 ? Colors.redAccent : Colors.blue,
),
]
Widget _buildScaffoldBody(bool empty) {
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
floating: true,
pinned: true,
primary: true,
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
setState(() {
_accountMenuExpanded = false;
});
},
),
bottom: empty ? null : TabBar(
tabs: buildUIViewTabs(),
isScrollable: true,
),
),
)
:
_homeAssistant.buildViews(context, _useLovelaceUI)
];
},
body: empty ?
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"),
size: 100.0,
color: Colors.blue,
),
]
),
)
:
_homeAssistant.buildViews(context, _useLovelaceUI),
);
}
@override
Widget build(BuildContext context) {
Widget bottomBar;
if (_showBottomBar) {
List<Widget> bottomBarChildren = [];
if (_bottomBarText != null) {
bottomBarChildren.add(
Padding(
padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
Sizes.rowPadding),
child: Text(
"$_bottomBarText",
textAlign: TextAlign.left,
softWrap: true,
),
)
);
}
if (_bottomBarProgress) {
bottomBarChildren.add(
CollectionScaleTransition(
children: <Widget>[
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
],
),
);
}
if (bottomBarChildren.isNotEmpty) {
bottomBar = Container(
color: _bottomBarColor,
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: bottomBarChildren,
),
),
_bottomBarAction
],
),
);
}
}
// This method is rerun every time setState is called.
if (_homeAssistant.ui == null || _homeAssistant.ui.views == null) {
return _buildScaffold(true);
return Scaffold(
key: _scaffoldKey,
primary: false,
drawer: _buildAppDrawer(),
bottomNavigationBar: bottomBar,
body: _buildScaffoldBody(true)
);
} else {
return DefaultTabController(
length: _homeAssistant.ui.views.length,
child: _buildScaffold(false)
return Scaffold(
key: _scaffoldKey,
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
body: DefaultTabController(
length: _homeAssistant.ui?.views?.length ?? 0,
child: _buildScaffoldBody(false),
),
);
}
}

View File

@ -22,6 +22,9 @@ class MaterialDesignIcons {
"cover.closed": "mdi:window-closed",
"cover.closing": "mdi:window-open",
"cover.opening": "mdi:window-open",
"lock.locked": "mdi:lock",
"lock.unlocked": "mdi:lock-open",
"fan": "mdi:fan"
};
static Map _defaultIconsByDeviceClass = {
@ -38,8 +41,8 @@ class MaterialDesignIcons {
"binary_sensor.heat.off": "mdi:thermometer",
"binary_sensor.light.on": "mdi:brightness-7",
"binary_sensor.light.off": "mdi:brightness-5",
//"binary_sensor.lock.on": "mdi:",
//"binary_sensor.lock.off": "mdi:",
"binary_sensor.lock.on": "mdi:lock-open",
"binary_sensor.lock.off": "mdi:lock",
"binary_sensor.moisture.on": "mdi:water",
"binary_sensor.moisture.off": "mdi:water-off",
"binary_sensor.motion.on": "mdi:run",
@ -56,8 +59,8 @@ class MaterialDesignIcons {
"binary_sensor.power.off": "mdi:verified",
//"binary_sensor.presence.on": "mdi:",
//"binary_sensor.presence.off": "mdi:",
//"binary_sensor.problem.on": "mdi:",
//"binary_sensor.problem.off": "mdi:",
"binary_sensor.problem.on": "mdi:alert-outline",
"binary_sensor.problem.off": "mdi:check-outline",
"binary_sensor.safety.on": "mdi:alert",
"binary_sensor.safety.off": "mdi:verified",
"binary_sensor.smoke.on": "mdi:alert",
@ -66,13 +69,13 @@ class MaterialDesignIcons {
"binary_sensor.sound.off": "mdi:music-note-off",
"binary_sensor.vibration.on": "mdi:vibrate",
"binary_sensor.vibration.off": "mdi:mdi-crop-portrait",
//"binary_sensor.window.on": "mdi:",
//"binary_sensor.window.off": "mdi:",
"binary_sensor.window.on": "mdi:window-open",
"binary_sensor.window.off": "mdi:window-closed",
"sensor.battery": "mdi:battery-80",
"sensor.humidity": "mdi:water-percent",
//"sensor.illuminance": "mdi:",
"sensor.temperature": "mdi:thermometer",
//"cover.window": "mdi:",
"cover.window": "mdi:mdi:window-closed",
"cover.garage.closed": "mdi:garage",
"cover.garage.open": "mdi:garage-open",
"cover.garage.opening": "mdi:garage-open",
@ -2880,17 +2883,17 @@ class MaterialDesignIcons {
"mdi:blank": 0xf68c
};
static Widget createIconWidgetFromEntityData(Entity data, double size, Color color) {
static Widget createIconWidgetFromEntityData(EntityWrapper data, double size, Color color) {
if (data == null) {
return null;
}
if (data.entityPicture != null) {
if (data.entity.entityPicture != null) {
if (homeAssistantWebHost != null) {
return CircleAvatar(
radius: size/2,
backgroundColor: Colors.white,
backgroundImage: CachedNetworkImageProvider(
"$homeAssistantWebHost${data.entityPicture}",
"$homeAssistantWebHost${data.entity.entityPicture}",
),
);
} else {
@ -2902,8 +2905,8 @@ class MaterialDesignIcons {
if (iconName.length > 0) {
iconCode = getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entityId,
data.deviceClass, data.state); //
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
return Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),

View File

@ -22,20 +22,10 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _newAuthType = "access_token";
bool _useLovelace = false;
bool _newUseLovelace = false;
bool _edited = false;
FocusNode _domainFocusNode;
FocusNode _portFocusNode;
FocusNode _passwordFocusNode;
@override
void initState() {
super.initState();
_domainFocusNode = FocusNode();
_portFocusNode = FocusNode();
_passwordFocusNode = FocusNode();
_domainFocusNode.addListener(_checkConfigChanged);
_portFocusNode.addListener(_checkConfigChanged);
_passwordFocusNode.addListener(_checkConfigChanged);
_loadSettings();
}
@ -56,15 +46,14 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
});
}
void _checkConfigChanged() {
setState(() {
_edited = ((_newHassioPassword != _hassioPassword) ||
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newAuthType != _authType) ||
(_newUseLovelace != _useLovelace));
});
bool _checkConfigChanged() {
return ((_newHassioPassword != _hassioPassword) ||
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newAuthType != _authType) ||
(_newUseLovelace != _useLovelace));
}
_saveSettings() async {
@ -92,16 +81,23 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: _edited ? (){
_saveSettings().then((r){
onPressed: (){
if (_checkConfigChanged()) {
TheLogger.debug("Settings changed. Saving...");
_saveSettings().then((r) {
Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true));
});
} : null
} else {
TheLogger.debug("Settings was not changed");
Navigator.pop(context);
}
}
)
],
),
body: ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text(
@ -117,8 +113,9 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
Switch(
value: (_newSocketProtocol == "wss"),
onChanged: (value) {
_newSocketProtocol = value ? "wss" : "ws";
_checkConfigChanged();
setState(() {
_newSocketProtocol = value ? "wss" : "ws";
});
},
)
],
@ -136,9 +133,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
),
onChanged: (value) {
_newHassioDomain = value;
},
focusNode: _domainFocusNode,
onEditingComplete: _checkConfigChanged,
}
),
new TextField(
decoration: InputDecoration(
@ -153,27 +148,39 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
),
onChanged: (value) {
_newHassioPort = value;
//_saveSettings();
},
focusNode: _portFocusNode,
onEditingComplete: _checkConfigChanged,
}
),
new Text(
"Try ports 80 and 443 if default is not working and you don't know why.",
style: TextStyle(color: Colors.grey),
),
new Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("Login with access token (HA >= 0.78.0)"),
Flexible(
child: Text(
"Login with access token (HA >= 0.78.0)",
softWrap: true,
maxLines: 2,
),
),
Switch(
value: (_newAuthType == "access_token"),
onChanged: (value) {
_newAuthType = value ? "access_token" : "api_password";
_checkConfigChanged();
//_saveSettings();
setState(() {
_newAuthType = value ? "access_token" : "api_password";
});
},
)
],
),
new Text(
"You should use access token for HA >= 0.84.1. Legacy password will not work there.",
style: TextStyle(color: Colors.grey),
),
new TextField(
decoration: InputDecoration(
labelText: _authType == "access_token" ? "Access token" : "API password"
labelText: _newAuthType == "access_token" ? "Access token" : "API password"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(
@ -184,10 +191,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
),
onChanged: (value) {
_newHassioPassword = value;
//_saveSettings();
},
focusNode: _passwordFocusNode,
onEditingComplete: _checkConfigChanged,
}
),
Padding(
padding: EdgeInsets.only(top: 20.0),
@ -205,8 +209,9 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
Switch(
value: _newUseLovelace,
onChanged: (value) {
_newUseLovelace = value;
_checkConfigChanged();
setState(() {
_newUseLovelace = value;
});
},
)
],
@ -218,12 +223,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
@override
void dispose() {
_domainFocusNode.removeListener(_checkConfigChanged);
_portFocusNode.removeListener(_checkConfigChanged);
_passwordFocusNode.removeListener(_checkConfigChanged);
_domainFocusNode.dispose();
_portFocusNode.dispose();
_passwordFocusNode.dispose();
super.dispose();
}
}

View File

@ -1,60 +1,46 @@
part of '../main.dart';
class HACard {
List<Entity> entities = [];
Entity linkedEntity;
List<EntityWrapper> entities = [];
List<HACard> childCards = [];
EntityWrapper linkedEntityWrapper;
String name;
String id;
String type;
bool showName;
bool showState;
bool showEmpty;
int columnsCount;
List stateFilter;
HACard({
this.name,
this.id,
this.linkedEntity,
this.linkedEntityWrapper,
this.columnsCount: 4,
this.showName: true,
this.showState: true,
this.stateFilter: const [],
this.showEmpty: true,
@required this.type
});
Widget build(BuildContext context) {
switch (type) {
case "entities": {
return EntitiesCardWidget(
card: this,
);
}
case "weather-forecast":
case "thermostat":
case "sensor":
case "plant-status":
case "picture-entity":
case "picture-elements":
case "picture":
case "map":
case "iframe":
case "gauge":
case "entity-button":
case "conditional":
case "alarm-panel":
case "media-control": {
return UnsupportedCardWidget(
card: this,
);
}
default: {
if ((linkedEntity == null) && (entities.isNotEmpty)) {
return EntitiesCardWidget(
card: this,
);
} else {
return UnsupportedCardWidget(
card: this,
);
}
}
List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) {
if (entityWrapper.entity.isHidden) {
return false;
}
if (stateFilter.isNotEmpty) {
return stateFilter.contains(entityWrapper.entity.state);
}
return true;
}).toList();
}
Widget build(BuildContext context) {
return CardWidget(
card: this,
);
}
}

View File

@ -0,0 +1,16 @@
part of '../main.dart';
class Sizes {
static const rightWidgetPadding = 14.0;
static const leftWidgetPadding = 8.0;
static const buttonPadding = 4.0;
static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0;
static const largeIconSize = 46.0;
static const stateFontSize = 15.0;
static const nameFontSize = 15.0;
static const smallFontSize = 14.0;
static const largeFontSize = 24.0;
static const inputWidth = 160.0;
static const rowPadding = 10.0;
}

View File

@ -2,6 +2,7 @@ part of '../main.dart';
class HomeAssistantUI {
List<HAView> views;
String title;
HomeAssistantUI() {
views = [];
@ -9,7 +10,7 @@ class HomeAssistantUI {
Widget build(BuildContext context) {
return TabBarView(
children: _buildViews(context)
children: _buildViews(context)
);
}

View File

@ -23,41 +23,89 @@ class HAView {
void _fillView(List<Entity> childEntities) {
List<HACard> autoGeneratedCards = [];
childEntities.forEach((entity) {
if (entity.isBadge) {
badges.add(entity);
TheLogger.debug("----Badge: ${entity.entityId}");
} else {
if (!entity.isGroup) {
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) {
HACard card = HACard(
id: groupIdToAdd,
name: entity.domain,
type: "entities"
);
TheLogger.debug("----Creating card: $groupIdToAdd");
card.entities.add(entity);
autoGeneratedCards.add(card);
} else {
autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(entity);
}
} else {
TheLogger.debug("----Card: ${entity.entityId}");
badges.addAll(childEntities.where((entity){ return entity.isBadge;}));
childEntities.where((entity){ return entity.domain == "media_player";}).forEach((e){
HACard card = HACard(
name: e.displayName,
id: e.entityId,
linkedEntityWrapper: EntityWrapper(entity: e),
type: CardType.mediaControl
);
cards.add(card);
});
childEntities.where((e){return (!e.isBadge && e.domain != "media_player");}).forEach((entity) {
if (!entity.isGroup) {
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) {
HACard card = HACard(
id: groupIdToAdd,
name: entity.domain,
type: CardType.entities
);
card.entities.add(EntityWrapper(entity: entity));
autoGeneratedCards.add(card);
} else {
autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(EntityWrapper(entity: entity));
}
} else {
HACard card = HACard(
name: entity.displayName,
id: entity.entityId,
linkedEntityWrapper: EntityWrapper(entity: entity),
type: CardType.entities
);
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
HACard mediaCard = HACard(
name: entity.displayName,
id: entity.entityId,
linkedEntity: entity,
type: "entities"
linkedEntityWrapper: EntityWrapper(entity: entity),
type: CardType.mediaControl
);
card.entities.addAll(entity.childEntities);
cards.add(card);
}
cards.add(mediaCard);
});
cards.add(card);
}
});
cards.addAll(autoGeneratedCards);
}
Widget buildTab() {
if (linkedEntity == null) {
if (iconName != null) {
return
Tab(
icon:
Icon(
MaterialDesignIcons.createIconDataFromIconName(
iconName ?? "mdi:home-assistant"),
size: 24.0,
)
);
} else {
return
Tab(
text: name.toUpperCase(),
);
}
} else {
if (linkedEntity.icon != null && linkedEntity.icon.length > 0) {
return Tab(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName(
linkedEntity.icon),
size: 24.0,
)
);
} else {
return Tab(
text: linkedEntity.displayName.toUpperCase(),
);
}
}
}
Widget build(BuildContext context) {
return ViewWidget(
view: this,

View File

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

View File

@ -0,0 +1,215 @@
part of '../main.dart';
class CardWidget extends StatelessWidget {
final HACard card;
const CardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if ((card.linkedEntityWrapper!= null) && (card.linkedEntityWrapper.entity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
switch (card.type) {
case CardType.entities: {
return _buildEntitiesCard(context);
}
case CardType.glance: {
return _buildGlanceCard(context);
}
case CardType.mediaControl: {
return _buildMediaControlsCard(context);
}
case CardType.entityButton: {
return _buildEntityButtonCard(context);
}
case CardType.horizontalStack: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
children.add(
Flexible(
fit: FlexFit.tight,
child: card.build(context),
)
);
});
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
case CardType.verticalStack: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
children.add(
card.build(context)
);
});
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
default: {
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
return _buildEntitiesCard(context);
} else {
return _buildUnsupportedCard(context);
}
}
}
}
Widget _buildEntitiesCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name));
entitiesToShow.forEach((EntityWrapper entity) {
if (!entity.entity.isHidden) {
body.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
child: entity.entity.buildDefaultWidget(context)
),
));
}
});
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
);
}
Widget _buildGlanceCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> rows = [];
rows.add(CardHeaderWidget(name: card.name));
List<Widget> result = [];
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
entitiesToShow.forEach((EntityWrapper entity) {
result.add(
FractionallySizedBox(
widthFactor: 1/columnsCount,
child: EntityModel(
entityWrapper: entity,
child: GlanceEntityContainer(
showName: card.showName,
showState: card.showState,
),
handleTap: true
),
)
);
});
rows.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
child: Wrap(
//alignment: WrapAlignment.spaceAround,
runSpacing: Sizes.rowPadding*2,
children: result,
),
)
);
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
);
}
Widget _buildMediaControlsCard(BuildContext context) {
if (card.linkedEntityWrapper == null || card.linkedEntityWrapper.entity == null) {
return Container(width: 0, height: 0,);
} else {
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: MediaPlayerWidget()
)
);
}
}
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.toUpperCase();
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: ButtonEntityContainer(),
handleTap: true
)
);
}
}
Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name ?? ""));
List<Widget> result = [];
if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: true,
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
),
)
]);
} else {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
),
]);
}
body.addAll(result);
return Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: body
)
);
}
}

View File

@ -1,39 +0,0 @@
part of '../main.dart';
class EntitiesCardWidget extends StatelessWidget {
final HACard card;
const EntitiesCardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if ((card.linkedEntity!= null) && (card.linkedEntity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name));
body.addAll(_buildCardBody(context));
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
);
}
List<Widget> _buildCardBody(BuildContext context) {
List<Widget> result = [];
card.entities.forEach((Entity entity) {
if (!entity.isHidden) {
result.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
child: entity.buildDefaultWidget(context),
));
}
});
return result;
}
}

View File

@ -1,27 +0,0 @@
part of '../main.dart';
class MediaControlCardWidget extends StatelessWidget {
final HACard card;
const MediaControlCardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if ((card.linkedEntity!= null) && (card.linkedEntity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> body = [];
return Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: body
)
);
}
}

View File

@ -1,49 +0,0 @@
part of '../main.dart';
class UnsupportedCardWidget extends StatelessWidget {
final HACard card;
const UnsupportedCardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if ((card.linkedEntity!= null) && (card.linkedEntity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name ?? ""));
body.addAll(_buildCardBody(context));
return Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: body
)
);
}
List<Widget> _buildCardBody(BuildContext context) {
List<Widget> result = [];
if (card.linkedEntity != null) {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0.0, Entity.rowPadding, 0.0, Entity.rowPadding),
child: card.linkedEntity.buildDefaultWidget(context),
)
]);
} else {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(Entity.leftWidgetPadding, Entity.rowPadding, Entity.rightWidgetPadding, Entity.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
),
]);
}
return result;
}
}

View File

@ -35,6 +35,7 @@ class ViewWidgetState extends State<ViewWidget> {
return RefreshIndicator(
color: Colors.amber,
child: ListView(
padding: EdgeInsets.all(0.0),
physics: const AlwaysScrollableScrollPhysics(),
children: _buildChildren(context),
),

View File

@ -58,9 +58,8 @@ class HAUtils {
class StateChangedEvent {
String entityId;
String newState;
bool localChange;
StateChangedEvent(this.entityId, this.newState, this.localChange);
StateChangedEvent(this.entityId, this.newState);
}
class SettingsChangedEvent {

View File

@ -1,13 +1,6 @@
# Generated by pub
# See https://www.dartlang.org/tools/pub/glossary#lockfile
packages:
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.32.4"
archive:
dependency: transitive
description:
@ -21,7 +14,7 @@ packages:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0"
version: "1.5.1"
async:
dependency: transitive
description:
@ -42,7 +35,7 @@ packages:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
version: "0.5.1"
charcode:
dependency: transitive
description:
@ -56,14 +49,14 @@ packages:
name: charts_common
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
version: "0.5.0"
charts_flutter:
dependency: "direct main"
description:
name: charts_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
version: "0.5.0"
collection:
dependency: transitive
description:
@ -85,19 +78,12 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.5"
dart_config:
dependency: transitive
description:
path: "."
ref: HEAD
resolved-ref: c5727795659e886a7db8b39a14e2c8987280fe1f
resolved-ref: e26916e095244a7e5ea61315b030d298d127ed26
url: "https://github.com/MarkOSullivan94/dart_config.git"
source: git
version: "0.5.0"
@ -126,7 +112,7 @@ packages:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
version: "0.2.0+1"
flutter_colorpicker:
dependency: "direct main"
description:
@ -140,47 +126,19 @@ packages:
name: flutter_launcher_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1"
version: "0.7.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
front_end:
dependency: transitive
description:
name: front_end
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.7"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.3+3"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.3+17"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "0.12.0"
http_parser:
dependency: transitive
description:
@ -194,7 +152,7 @@ packages:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.0.5"
intl:
dependency: transitive
description:
@ -202,34 +160,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.7"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+1"
json_rpc_2:
dependency: transitive
description:
name: json_rpc_2
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
kernel:
dependency: transitive
description:
name: kernel
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
logging:
dependency: transitive
description:
@ -251,41 +181,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.6+2"
multi_server_socket:
dependency: transitive
description:
name: multi_server_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
package_resolver:
dependency: transitive
description:
name: package_resolver
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
path:
dependency: transitive
description:
@ -307,20 +202,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
plugin:
dependency: transitive
description:
name: plugin
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+3"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.6"
progress_indicators:
dependency: "direct main"
description:
@ -328,20 +209,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.2"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0+1"
version: "2.0.1"
shared_preferences:
dependency: "direct main"
description:
@ -349,53 +223,11 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.3+3"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.8"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2+4"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.5"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.7"
source_span:
dependency: transitive
description:
@ -438,13 +270,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
test:
test_api:
dependency: transitive
description:
name: test
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "0.2.1"
typed_data:
dependency: transitive
description:
@ -458,14 +290,7 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.3"
utf:
dependency: transitive
description:
name: utf
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.0+5"
version: "4.0.2"
uuid:
dependency: transitive
description:
@ -480,22 +305,8 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
vm_service_client:
dependency: transitive
description:
name: vm_service_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.6"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+10"
web_socket_channel:
dependency: transitive
dependency: "direct main"
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
@ -516,5 +327,5 @@ packages:
source: hosted
version: "2.1.15"
sdks:
dart: ">=2.0.0 <=2.1.0-dev.5.0.flutter-a2eb050044"
flutter: ">=0.1.4 <2.0.0"
dart: ">=2.0.0 <3.0.0"
flutter: ">=0.5.6 <2.0.0"

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.3.5+55
version: 0.3.12+79
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -9,14 +9,15 @@ environment:
dependencies:
flutter:
sdk: flutter
web_socket_channel: any
shared_preferences: any
progress_indicators: ^0.1.2
event_bus: ^1.0.1
cached_network_image: ^0.4.1
url_launcher: ^3.0.3
date_format: ^1.0.5
flutter_colorpicker: ^0.1.0
charts_flutter: ^0.4.0
progress_indicators: any
event_bus: any
cached_network_image: any
url_launcher: any
date_format: any
flutter_colorpicker: any
charts_flutter: any
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
@ -25,7 +26,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: ^0.6.1
flutter_launcher_icons: any
flutter_icons:
android: true