Compare commits

...

123 Commits

Author SHA1 Message Date
db77cc43aa Version 0.5.0 2019-03-13 22:42:03 +02:00
b2269cc96d Resolves #293 Fix updater icon 2019-03-13 22:40:54 +02:00
8b28bb2e9e Resolves #314 card icon priority 2019-03-13 22:12:01 +02:00
fb456878bc Resolves #258 Timer support 2019-03-13 21:33:58 +02:00
8b961ebd69 Resolves #83 Calendar support 2019-03-13 20:07:44 +02:00
9bd3a41cf5 Resolves #140 Scenes 2019-03-13 18:06:43 +02:00
491ae55a2a Resolves #299, Resolves #234 Fix entity picture url issue 2019-03-13 17:48:49 +02:00
e1d2981782 Add 'Open Web UI' menu link 2019-03-13 17:25:08 +02:00
74572168ae Resolves #116 Add Iframe panel support 2019-03-13 17:23:23 +02:00
92d0b5c055 Migrate to AndroidX 2019-03-13 17:05:15 +02:00
3504d3276c Resolves #11 Add Panels fetching 2019-03-13 16:39:23 +02:00
736b38b64c Some UI improvements for #245 2019-03-13 14:08:54 +02:00
cb118b599a Resolves #245 Add special row elements support for entities card 2019-03-13 00:56:57 +02:00
a08a056cff Resolves #254 Missed entities 2019-03-12 23:35:33 +02:00
0ef2ebfe31 Fix 'Paste color' button background when saved color is null 2019-03-10 23:49:05 +02:00
4f4ac3b574 Resolves #310 Add assumed state for locks 2019-03-10 23:41:14 +02:00
7064cb0e30 Resolves #272 Add 'Copy color' and 'Past color' 2019-03-10 23:28:23 +02:00
91a99e17e0 Resolves #320 Fix eEntity_picture size 2019-03-10 22:50:39 +02:00
2e9b7d20b9 Fix broken icons 2019-03-10 19:28:11 +02:00
b8aa808de4 Update Material Design Icons to 3.5.95 2019-03-09 13:26:45 +00:00
2cfa92a42b Reverts #308 2019-03-06 16:50:30 +00:00
146efef72d Gradle config for Chrome OS build 2019-03-06 16:42:05 +00:00
8c9804e16f WIP #308 2019-03-02 20:13:24 +02:00
a4736bfb5a Message handling improvements 2019-03-02 18:00:25 +02:00
15c54df629 Update README.md 2019-02-26 11:31:39 +02:00
32ffef21e9 Update README.md 2019-02-26 11:31:08 +02:00
848d3cb510 Update README.md 2019-02-26 10:45:25 +02:00
8a4caeebba Update README.md 2019-02-26 10:43:47 +02:00
aa923f0fba Update README.md 2019-02-26 10:39:09 +02:00
4d8f50ddd5 Update README.md 2019-02-26 10:33:34 +02:00
fe06b21a6c Update README.md 2019-02-26 10:30:08 +02:00
efed7fb1b5 Update README.md 2019-02-26 10:23:03 +02:00
df2cbb7d13 Resolves #313 Fix missed mute button for media_player 2019-02-22 15:39:53 +02:00
03edaa9ca2 Resolves #168 Fix error when entity view closed before history loaded 2019-02-22 15:33:10 +02:00
1a7457abf9 Resolves #311 Rebuild tabs only if views count changes 2019-02-22 15:28:11 +02:00
00889b13e0 Resolves #312 Add white value control for light 2019-02-22 15:15:27 +02:00
0615073ec4 Get color from rgb_color if there is no hsv_color attribute 2019-02-22 14:20:01 +02:00
eb7d17d147 WIP #308 Move entity icon generation into EntityIcon widget 2019-02-21 16:32:55 +02:00
24f80feeee Resolves #187 Fix crash on view count changes 2019-02-21 15:35:58 +02:00
4b6dda5a9c version 0.4.4 2019-02-20 18:54:54 +02:00
4099fa0c83 WIP #302 fix SVG size 2019-02-20 18:50:58 +02:00
76057e8797 WIP #302 simple SVG support 2019-02-20 17:55:56 +02:00
538d3603dc Resolves #306 Improve camera connection 2019-02-20 16:39:57 +02:00
bc0e72ca52 version 0.4.3 2019-02-20 13:58:30 +02:00
f25a47beb2 Add camera stream reconnect on closing 2019-02-20 13:57:25 +02:00
cc3c6b0087 Resolves #307 Support different frame bounderies for MJPEG stream 2019-02-20 12:06:03 +02:00
6cf80c0bfd version 0.4.2 2019-02-19 19:22:40 +02:00
8ce9bdb7a5 Resolves #303, Resolves #304 Fix wrong camera stream url 2019-02-19 19:21:52 +02:00
31e50150b1 Resolves #263 Fix error when supported_features is null 2019-02-17 13:52:24 +02:00
e359150d97 Version 0.4.0 2019-02-16 22:05:43 +02:00
93680c981c Resolves #283 Add possibility to restrat HA 2019-02-16 22:04:49 +02:00
e06b66c523 Resolves #259 target_temp_step support for climate 2019-02-16 20:44:41 +02:00
3dea844e1e Resolves #290 Hide pin inputs if code_format is null 2019-02-16 20:33:56 +02:00
62b1af30e0 Resolves #291 some padding issues 2019-02-16 19:59:39 +02:00
e006c4e403 build number 2019-02-10 22:36:30 +02:00
983573388e Remove pull-to-refresh. Add new menu in header. 2019-02-10 22:33:46 +02:00
bdd1dc7e17 Hide light additional controls if state=unavailable 2019-02-10 19:06:07 +02:00
9c1970ee14 build number 2019-02-10 18:26:48 +02:00
d0e0bf3571 Current color for color picker fix 2019-02-10 18:24:54 +02:00
b399357517 Back to old version format 2019-02-10 17:23:57 +02:00
0290cd3a32 Resolves #273 New color picker 2019-02-10 17:15:52 +02:00
d8a1d03179 v.3.14.87 2019-02-09 02:21:20 +02:00
216fad3cb9 Fix entity page padding 2019-02-09 02:21:20 +02:00
fead6ea348 Resolves #143 Camera support 2019-02-09 02:21:20 +02:00
8814687be6 WIP #143 Initial not optimized MJPEG streaming 2019-02-09 02:21:20 +02:00
71c0e2caa0 Change version numeration 2019-02-09 02:21:20 +02:00
1531c41542 Create CODE_OF_CONDUCT.md 2019-02-01 12:55:25 +02:00
bc90d013e8 Build 86 2019-02-01 11:52:09 +02:00
2adfaca0c4 Resolves #286 2019-02-01 11:51:35 +02:00
6cc1a37d9d Resolves #285 2019-02-01 11:49:27 +02:00
4bb616b327 Build number 2019-01-31 21:14:20 +02:00
38219618ba version 0.3.14 2019-01-31 21:05:11 +02:00
6774b53758 Disable unfinished camera support 2019-01-31 20:57:33 +02:00
29a94c882f WIP MJPEG stream handling 2019-01-31 01:04:13 +02:00
5897fa3a99 WIP #143 2019-01-30 00:25:41 +02:00
7af92c2dc9 WIP #143 Camera support 2019-01-29 22:03:08 +02:00
1094177a42 Resolves #282 Trigger button for alarm 2019-01-29 18:51:28 +02:00
5e814e8109 Resolves #204 Alarm panel card support 2019-01-29 15:00:15 +02:00
24c7675fa4 Resolves #142 Alarm control panel support 2019-01-29 11:54:26 +02:00
dc3ca38c78 WIP #142 Alarm control panel 2019-01-28 16:48:49 +02:00
96b528e055 Update README.md 2019-01-28 15:05:19 +02:00
3858036631 Resolves #139 Trigger for automations 2019-01-25 23:48:31 +02:00
19d42ceeb3 Fix names null 2019-01-25 23:30:23 +02:00
a2836a3603 Resolves #257 2019-01-25 23:08:12 +02:00
2a45758a6d Resolves #268 Badges for Lovelace UI 2019-01-25 22:55:41 +02:00
dc1bf4d878 WIP #266 fix icons 2019-01-25 22:45:54 +02:00
e82ba60c4e WIP #266 Card parsing proper error handling and toString for some fields 2019-01-25 22:41:26 +02:00
09199d30e8 Resolves #274 Use Lovelace UI by default 2019-01-25 22:29:16 +02:00
724d32dbe2 Resolves #277 Remove legacy password support 2019-01-25 22:27:13 +02:00
949c8ee44e Resolves #264 Throttle for sending thermostat temperature 2019-01-25 22:19:11 +02:00
1a446d34c7 Resolves #262 Cler 2019-01-25 22:19:11 +02:00
22a5847285 Resolves #270 Current light effect for lights 2019-01-24 12:26:38 +02:00
1c8f770f10 Resolves #121 Markdown card support 2019-01-23 23:34:45 +02:00
be5ea55f6b Fix light color controls appearence issue 2018-12-29 17:44:11 +02:00
c65ade9827 Fix icons for entity button 2018-12-25 11:48:37 +02:00
d3c1422b9e Version 0.3.13 2018-12-15 14:37:55 +02:00
b6ac9f985f Entity state by device class 2018-12-15 14:37:00 +02:00
a59de4b6dc Resolves #255 Refresh UI for newly appeared entity 2018-12-15 14:09:37 +02:00
f507d5df0c Resolves #242 2018-12-14 19:37:49 +02:00
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
80 changed files with 3560 additions and 1049 deletions

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at vyalov.egor@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

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

View File

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

View File

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

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

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

Binary file not shown.

View File

@ -19,8 +19,8 @@ class _EntityViewPageState extends State<EntityViewPage> {
void initState() { void initState() {
super.initState(); super.initState();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
TheLogger.debug("State change event handled by entity page: ${event.entityId}");
if (event.entityId == widget.entityId) { if (event.entityId == widget.entityId) {
Logger.d("State change event handled by entity page: ${event.entityId}");
setState(() {}); setState(() {});
} }
}); });
@ -46,12 +46,9 @@ class _EntityViewPageState extends State<EntityViewPage> {
// the App.build method, and use it to set our appbar title. // the App.build method, and use it to set our appbar title.
title: new Text(_title), title: new Text(_title),
), ),
body: Padding( body: HomeAssistantModel(
padding: EdgeInsets.all(10.0), homeAssistant: widget.homeAssistant,
child: HomeAssistantModel( child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
homeAssistant: widget.homeAssistant,
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
)
), ),
); );
} }

View File

@ -0,0 +1,12 @@
part of '../main.dart';
class AlarmControlPanelEntity extends Entity {
AlarmControlPanelEntity(Map rawData) : super(rawData);
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return AlarmControlPanelControlsWidget(
extended: false,
);
}
}

View File

@ -0,0 +1,26 @@
part of '../main.dart';
class AutomationEntity extends Entity {
AutomationEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
FlatServiceButton(
serviceDomain: domain,
entityId: entityId,
text: "TRIGGER",
serviceName: "trigger",
)
],
);
}
}

View File

@ -5,6 +5,11 @@ class ButtonEntity extends Entity {
@override @override
Widget _buildStatePart(BuildContext context) { Widget _buildStatePart(BuildContext context) {
return ButtonStateWidget(); return FlatServiceButton(
entityId: entityId,
serviceDomain: domain,
serviceName: 'turn_on',
text: domain == "scene" ? "ACTIVATE" : "EXECUTE",
);
} }
} }

View File

@ -0,0 +1,17 @@
part of '../main.dart';
class CameraEntity extends Entity {
static const SUPPORT_ON_OFF = 1;
CameraEntity(Map rawData) : super(rawData);
bool get supportOnOff => ((supportedFeatures &
CameraEntity.SUPPORT_ON_OFF) ==
CameraEntity.SUPPORT_ON_OFF);
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return CameraStreamView();
}
}

View File

@ -23,44 +23,44 @@ class ClimateEntity extends Entity {
static const SUPPORT_AUX_HEAT = 2048; static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096; static const SUPPORT_ON_OFF = 4096;
bool get supportTargetTemperature => ((attributes["supported_features"] & bool get supportTargetTemperature => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) == ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE); ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] & bool get supportTargetTemperatureHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) == ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH); ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
bool get supportTargetTemperatureLow => ((attributes["supported_features"] & bool get supportTargetTemperatureLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) == ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW); ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
bool get supportTargetHumidity => ((attributes["supported_features"] & bool get supportTargetHumidity => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY) == ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY); ClimateEntity.SUPPORT_TARGET_HUMIDITY);
bool get supportTargetHumidityHigh => ((attributes["supported_features"] & bool get supportTargetHumidityHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) == ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH); ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
bool get supportTargetHumidityLow => ((attributes["supported_features"] & bool get supportTargetHumidityLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) == ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW); ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
bool get supportFanMode => bool get supportFanMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) == ((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
ClimateEntity.SUPPORT_FAN_MODE); ClimateEntity.SUPPORT_FAN_MODE);
bool get supportOperationMode => ((attributes["supported_features"] & bool get supportOperationMode => ((supportedFeatures &
ClimateEntity.SUPPORT_OPERATION_MODE) == ClimateEntity.SUPPORT_OPERATION_MODE) ==
ClimateEntity.SUPPORT_OPERATION_MODE); ClimateEntity.SUPPORT_OPERATION_MODE);
bool get supportHoldMode => bool get supportHoldMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) == ((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) ==
ClimateEntity.SUPPORT_HOLD_MODE); ClimateEntity.SUPPORT_HOLD_MODE);
bool get supportSwingMode => bool get supportSwingMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) == ((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE); ClimateEntity.SUPPORT_SWING_MODE);
bool get supportAwayMode => bool get supportAwayMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) == ((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) ==
ClimateEntity.SUPPORT_AWAY_MODE); ClimateEntity.SUPPORT_AWAY_MODE);
bool get supportAuxHeat => bool get supportAuxHeat =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) == ((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
ClimateEntity.SUPPORT_AUX_HEAT); ClimateEntity.SUPPORT_AUX_HEAT);
bool get supportOnOff => bool get supportOnOff =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) == ((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) ==
ClimateEntity.SUPPORT_ON_OFF); ClimateEntity.SUPPORT_ON_OFF);
List<String> get operationList => attributes["operation_list"] != null List<String> get operationList => attributes["operation_list"] != null
@ -80,6 +80,7 @@ class ClimateEntity extends Entity {
double get targetHumidity => _getDoubleAttributeValue('humidity'); double get targetHumidity => _getDoubleAttributeValue('humidity');
double get maxHumidity => _getDoubleAttributeValue('max_humidity'); double get maxHumidity => _getDoubleAttributeValue('max_humidity');
double get minHumidity => _getDoubleAttributeValue('min_humidity'); double get minHumidity => _getDoubleAttributeValue('min_humidity');
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
String get operationMode => attributes['operation_mode']; String get operationMode => attributes['operation_mode'];
String get fanMode => attributes['fan_mode']; String get fanMode => attributes['fan_mode'];
String get swingMode => attributes['swing_mode']; String get swingMode => attributes['swing_mode'];

View File

@ -28,16 +28,57 @@ class EntityState {
static const unavailable = 'unavailable'; static const unavailable = 'unavailable';
static const ok = 'ok'; static const ok = 'ok';
static const problem = 'problem'; static const problem = 'problem';
static const active = 'active';
} }
class EntityTapAction { class EntityUIAction {
static const moreInfo = 'more-info'; static const moreInfo = 'more-info';
static const toggle = 'toggle'; static const toggle = 'toggle';
static const callService = 'call-service'; static const callService = 'call-service';
static const navigate = 'navigate';
static const none = 'none'; 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 { class CardType {
static const horizontalStack = "horizontal-stack";
static const verticalStack = "vertical-stack";
static const entities = "entities"; static const entities = "entities";
static const glance = "glance"; static const glance = "glance";
static const mediaControl = "media-control"; static const mediaControl = "media-control";
@ -54,4 +95,5 @@ class CardType {
static const entityButton = "entity-button"; static const entityButton = "entity-button";
static const conditional = "conditional"; static const conditional = "conditional";
static const alarmPanel = "alarm-panel"; static const alarmPanel = "alarm-panel";
static const markdown = "markdown";
} }

View File

@ -11,29 +11,29 @@ class CoverEntity extends Entity {
static const SUPPORT_STOP_TILT = 64; static const SUPPORT_STOP_TILT = 64;
static const SUPPORT_SET_TILT_POSITION = 128; static const SUPPORT_SET_TILT_POSITION = 128;
bool get supportOpen => ((attributes["supported_features"] & bool get supportOpen => ((supportedFeatures &
CoverEntity.SUPPORT_OPEN) == CoverEntity.SUPPORT_OPEN) ==
CoverEntity.SUPPORT_OPEN); CoverEntity.SUPPORT_OPEN);
bool get supportClose => ((attributes["supported_features"] & bool get supportClose => ((supportedFeatures &
CoverEntity.SUPPORT_CLOSE) == CoverEntity.SUPPORT_CLOSE) ==
CoverEntity.SUPPORT_CLOSE); CoverEntity.SUPPORT_CLOSE);
bool get supportSetPosition => ((attributes["supported_features"] & bool get supportSetPosition => ((supportedFeatures &
CoverEntity.SUPPORT_SET_POSITION) == CoverEntity.SUPPORT_SET_POSITION) ==
CoverEntity.SUPPORT_SET_POSITION); CoverEntity.SUPPORT_SET_POSITION);
bool get supportStop => ((attributes["supported_features"] & bool get supportStop => ((supportedFeatures &
CoverEntity.SUPPORT_STOP) == CoverEntity.SUPPORT_STOP) ==
CoverEntity.SUPPORT_STOP); CoverEntity.SUPPORT_STOP);
bool get supportOpenTilt => ((attributes["supported_features"] & bool get supportOpenTilt => ((supportedFeatures &
CoverEntity.SUPPORT_OPEN_TILT) == CoverEntity.SUPPORT_OPEN_TILT) ==
CoverEntity.SUPPORT_OPEN_TILT); CoverEntity.SUPPORT_OPEN_TILT);
bool get supportCloseTilt => ((attributes["supported_features"] & bool get supportCloseTilt => ((supportedFeatures &
CoverEntity.SUPPORT_CLOSE_TILT) == CoverEntity.SUPPORT_CLOSE_TILT) ==
CoverEntity.SUPPORT_CLOSE_TILT); CoverEntity.SUPPORT_CLOSE_TILT);
bool get supportStopTilt => ((attributes["supported_features"] & bool get supportStopTilt => ((supportedFeatures &
CoverEntity.SUPPORT_STOP_TILT) == CoverEntity.SUPPORT_STOP_TILT) ==
CoverEntity.SUPPORT_STOP_TILT); CoverEntity.SUPPORT_STOP_TILT);
bool get supportSetTiltPosition => ((attributes["supported_features"] & bool get supportSetTiltPosition => ((supportedFeatures &
CoverEntity.SUPPORT_SET_TILT_POSITION) == CoverEntity.SUPPORT_SET_TILT_POSITION) ==
CoverEntity.SUPPORT_SET_TILT_POSITION); CoverEntity.SUPPORT_SET_TILT_POSITION);

View File

@ -1,5 +1,14 @@
part of '../main.dart'; part of '../main.dart';
class StatelessEntityType {
static const NONE = 0;
static const MISSED = 1;
static const DIVIDER = 2;
static const SECTION = 3;
static const CALL_SERVICE = 4;
static const WEBLINK = 5;
}
class Entity { class Entity {
static List badgeDomains = [ static List badgeDomains = [
@ -12,14 +21,65 @@ class Entity {
"sensor" "sensor"
]; ];
static Map StateByDeviceClass = {
"battery.on": "Low",
"battery.off": "Normal",
"cold.on": "Cold",
"cold.off": "Normal",
"connectivity.on": "Connected",
"connectivity.off": "Diconnected",
"door.on": "Open",
"door.off": "Closed",
"garage_door.on": "Open",
"garage_door.off": "Closed",
"gas.on": "Detected",
"gas.off": "Clear",
"heat.on": "Hot",
"heat.off": "Normal",
"light.on": "Detected",
"lignt.off": "No light",
"lock.on": "Unlocked",
"lock.off": "Locked",
"moisture.on": "Wet",
"moisture.off": "Dry",
"motion.on": "Detected",
"motion.off": "Clear",
"moving.on": "Moving",
"moving.off": "Stopped",
"occupancy.on": "Occupied",
"occupancy.off": "Clear",
"opening.on": "Open",
"opening.off": "Closed",
"plug.on": "Plugged in",
"plug.off": "Unplugged",
"power.on": "Powered",
"power.off": "No power",
"presence.on": "Home",
"presence.off": "Away",
"problem.on": "Problem",
"problem.off": "OK",
"safety.on": "Unsafe",
"safety.off": "Safe",
"smoke.on": "Detected",
"smoke.off": "Clear",
"sound.on": "Detected",
"sound.off": "Clear",
"vibration.on": "Detected",
"vibration.off": "Clear",
"window.on": "Open",
"window.off": "Closed"
};
Map attributes; Map attributes;
String domain; String domain;
String entityId; String entityId;
String state; String state;
String displayState;
DateTime _lastUpdated; DateTime _lastUpdated;
int statelessType = 0;
List<Entity> childEntities = []; List<Entity> childEntities = [];
List<String> attributesToShow = ["all"]; String deviceClass;
EntityHistoryConfig historyConfig = EntityHistoryConfig( EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.simple chartType: EntityHistoryWidgetType.simple
); );
@ -27,7 +87,6 @@ class Entity {
String get displayName => String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " ")); attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
String get deviceClass => attributes["device_class"] ?? null;
bool get isView => bool get isView =>
(domain == "group") && (domain == "group") &&
(attributes != null ? attributes["view"] ?? false : false); (attributes != null ? attributes["view"] ?? false : false);
@ -35,22 +94,67 @@ class Entity {
bool get isBadge => Entity.badgeDomains.contains(domain); bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? ""; String get icon => attributes["icon"] ?? "";
bool get isOn => state == EntityState.on; bool get isOn => state == EntityState.on;
String get entityPicture => attributes["entity_picture"]; String get entityPicture => _getEntityPictureUrl();
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? ""; String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? []; List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted(); String get lastUpdated => _getLastUpdatedFormatted();
bool get isHidden => attributes["hidden"] ?? false; bool get isHidden => attributes["hidden"] ?? false;
double get doubleState => double.tryParse(state) ?? 0.0; double get doubleState => double.tryParse(state) ?? 0.0;
int get supportedFeatures => attributes["supported_features"] ?? 0;
String _getEntityPictureUrl() {
String result = attributes["entity_picture"];
if (result == null) return result;
if (!result.startsWith("http")) {
if (result.startsWith("/")) {
result = "$homeAssistantWebHost$result";
} else {
result = "$homeAssistantWebHost/$result";
}
}
return result;
}
Entity(Map rawData) { Entity(Map rawData) {
update(rawData); update(rawData);
} }
Entity.missed(String entityId) {
statelessType = StatelessEntityType.MISSED;
attributes = {"hidden": false};
this.entityId = entityId;
}
Entity.divider() {
statelessType = StatelessEntityType.DIVIDER;
attributes = {"hidden": false};
}
Entity.section(String label) {
statelessType = StatelessEntityType.SECTION;
attributes = {"hidden": false, "friendly_name": "$label"};
}
Entity.callService({String icon, String name, String service, String actionName}) {
statelessType = StatelessEntityType.CALL_SERVICE;
entityId = service;
displayState = actionName?.toUpperCase() ?? "RUN";
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
}
Entity.weblink({String url, String name, String icon}) {
statelessType = StatelessEntityType.WEBLINK;
entityId = "custom.custom"; //TODO wtf??
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
}
void update(Map rawData) { void update(Map rawData) {
attributes = rawData["attributes"] ?? {}; attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0]; domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"]; entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"]; state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
_lastUpdated = DateTime.tryParse(rawData["last_updated"]); _lastUpdated = DateTime.tryParse(rawData["last_updated"]);
} }
@ -91,13 +195,6 @@ class Entity {
); );
} }
Widget buildGlanceWidget(BuildContext context, bool showName, bool showState) {
return GlanceEntityContainer(
showName: showName,
showState: showState,
);
}
Widget _buildStatePart(BuildContext context) { Widget _buildStatePart(BuildContext context) {
return SimpleEntityState(); return SimpleEntityState();
} }
@ -117,7 +214,10 @@ class Entity {
return EntityModel( return EntityModel(
entityWrapper: EntityWrapper(entity: this), entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[ child: EntityPageContainer(children: <Widget>[
DefaultEntityContainer(state: _buildStatePartForPage(context)), Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding),
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
),
LastUpdatedWidget(), LastUpdatedWidget(),
Divider(), Divider(),
_buildAdditionalControlsForPage(context), _buildAdditionalControlsForPage(context),

View File

@ -4,12 +4,8 @@ class EntityWrapper {
String displayName; String displayName;
String icon; String icon;
String tapAction; String entityPicture;
String holdAction; EntityUIAction uiAction;
String tapActionService;
Map<String, dynamic> tapActionServiceData;
String holdActionService;
Map<String, dynamic> holdActionServiceData;
Entity entity; Entity entity;
@ -17,63 +13,98 @@ class EntityWrapper {
this.entity, this.entity,
String icon, String icon,
String displayName, String displayName,
this.tapAction: EntityTapAction.moreInfo, this.uiAction
this.holdAction: EntityTapAction.none,
this.tapActionService,
this.tapActionServiceData,
this.holdActionService,
this.holdActionServiceData
}) { }) {
this.icon = icon ?? entity.icon; if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
this.displayName = displayName ?? entity.displayName; this.icon = icon ?? entity.icon;
if (icon == null) {
entityPicture = entity.entityPicture;
}
this.displayName = displayName ?? entity.displayName;
if (uiAction == null) {
uiAction = EntityUIAction();
}
}
} }
void handleTap() { void handleTap() {
switch (tapAction) { switch (uiAction.tapAction) {
case EntityTapAction.toggle: { case EntityUIAction.toggle: {
eventBus.fire( eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null)); ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break; break;
} }
case EntityTapAction.callService: { case EntityUIAction.callService: {
eventBus.fire( if (uiAction.tapService != null) {
ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData)); eventBus.fire(
ServiceCallEvent(uiAction.tapService.split(".")[0],
uiAction.tapService.split(".")[1], null,
uiAction.tapServiceData));
}
break; break;
} }
case EntityTapAction.none: { case EntityUIAction.none: {
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.tapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.tapService);
}
break; break;
} }
default: { default: {
eventBus.fire(
new ShowEntityPageEvent(entity));
break; break;
} }
} }
} }
void handleHold() { void handleHold() {
switch (holdAction) { switch (uiAction.holdAction) {
case EntityTapAction.toggle: { case EntityUIAction.toggle: {
eventBus.fire( eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null)); ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break; break;
} }
case EntityTapAction.callService: { case EntityUIAction.callService: {
eventBus.fire( if (uiAction.holdService != null) {
ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData)); eventBus.fire(
ServiceCallEvent(uiAction.holdService.split(".")[0],
uiAction.holdService.split(".")[1], null,
uiAction.holdServiceData));
}
break; break;
} }
case EntityTapAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entity)); new ShowEntityPageEvent(entity));
break; break;
} }
case EntityUIAction.navigate: {
if (uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
HAUtils.launchURL(uiAction.holdService);
}
break;
}
default: { default: {
break; break;
} }

View File

@ -8,13 +8,13 @@ class FanEntity extends Entity {
FanEntity(Map rawData) : super(rawData); FanEntity(Map rawData) : super(rawData);
bool get supportSetSpeed => ((attributes["supported_features"] & bool get supportSetSpeed => ((supportedFeatures &
FanEntity.SUPPORT_SET_SPEED) == FanEntity.SUPPORT_SET_SPEED) ==
FanEntity.SUPPORT_SET_SPEED); FanEntity.SUPPORT_SET_SPEED);
bool get supportOscillate => ((attributes["supported_features"] & bool get supportOscillate => ((supportedFeatures &
FanEntity.SUPPORT_OSCILLATE) == FanEntity.SUPPORT_OSCILLATE) ==
FanEntity.SUPPORT_OSCILLATE); FanEntity.SUPPORT_OSCILLATE);
bool get supportDirection => ((attributes["supported_features"] & bool get supportDirection => ((supportedFeatures &
FanEntity.SUPPORT_DIRECTION) == FanEntity.SUPPORT_DIRECTION) ==
FanEntity.SUPPORT_DIRECTION); FanEntity.SUPPORT_DIRECTION);

View File

@ -10,43 +10,50 @@ class LightEntity extends Entity {
static const SUPPORT_TRANSITION = 32; static const SUPPORT_TRANSITION = 32;
static const SUPPORT_WHITE_VALUE = 128; static const SUPPORT_WHITE_VALUE = 128;
bool get supportBrightness => ((attributes["supported_features"] & bool get supportBrightness => ((supportedFeatures &
LightEntity.SUPPORT_BRIGHTNESS) == LightEntity.SUPPORT_BRIGHTNESS) ==
LightEntity.SUPPORT_BRIGHTNESS); LightEntity.SUPPORT_BRIGHTNESS);
bool get supportColorTemp => ((attributes["supported_features"] & bool get supportColorTemp => ((supportedFeatures &
LightEntity.SUPPORT_COLOR_TEMP) == LightEntity.SUPPORT_COLOR_TEMP) ==
LightEntity.SUPPORT_COLOR_TEMP); LightEntity.SUPPORT_COLOR_TEMP);
bool get supportEffect => ((attributes["supported_features"] & bool get supportEffect => ((supportedFeatures &
LightEntity.SUPPORT_EFFECT) == LightEntity.SUPPORT_EFFECT) ==
LightEntity.SUPPORT_EFFECT); LightEntity.SUPPORT_EFFECT);
bool get supportFlash => ((attributes["supported_features"] & bool get supportFlash => ((supportedFeatures &
LightEntity.SUPPORT_FLASH) == LightEntity.SUPPORT_FLASH) ==
LightEntity.SUPPORT_FLASH); LightEntity.SUPPORT_FLASH);
bool get supportColor => ((attributes["supported_features"] & bool get supportColor => ((supportedFeatures &
LightEntity.SUPPORT_COLOR) == LightEntity.SUPPORT_COLOR) ==
LightEntity.SUPPORT_COLOR); LightEntity.SUPPORT_COLOR);
bool get supportTransition => ((attributes["supported_features"] & bool get supportTransition => ((supportedFeatures &
LightEntity.SUPPORT_TRANSITION) == LightEntity.SUPPORT_TRANSITION) ==
LightEntity.SUPPORT_TRANSITION); LightEntity.SUPPORT_TRANSITION);
bool get supportWhiteValue => ((attributes["supported_features"] & bool get supportWhiteValue => ((supportedFeatures &
LightEntity.SUPPORT_WHITE_VALUE) == LightEntity.SUPPORT_WHITE_VALUE) ==
LightEntity.SUPPORT_WHITE_VALUE); LightEntity.SUPPORT_WHITE_VALUE);
int get brightness => _getIntAttributeValue("brightness"); int get brightness => _getIntAttributeValue("brightness");
int get whiteValue => _getIntAttributeValue("white_value");
String get effect => attributes["effect"];
int get colorTemp => _getIntAttributeValue("color_temp"); int get colorTemp => _getIntAttributeValue("color_temp");
double get maxMireds => _getDoubleAttributeValue("max_mireds"); double get maxMireds => _getDoubleAttributeValue("max_mireds");
double get minMireds => _getDoubleAttributeValue("min_mireds"); double get minMireds => _getDoubleAttributeValue("min_mireds");
Color get color => _getColor(); HSVColor get color => _getColor();
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0)); bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
List<String> get effectList => getStringListAttributeValue("effect_list"); List<String> get effectList => getStringListAttributeValue("effect_list");
LightEntity(Map rawData) : super(rawData); LightEntity(Map rawData) : super(rawData);
Color _getColor() { HSVColor _getColor() {
List hs = attributes["hs_color"];
List rgb = attributes["rgb_color"]; List rgb = attributes["rgb_color"];
try { try {
if ((rgb != null) && (rgb.length > 0)) { if (hs != null && hs.isNotEmpty) {
return Color.fromARGB(255, rgb[0], rgb[1], rgb[2]); double sat = hs[1]/100;
String ssat = sat.toStringAsFixed(2);
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
} else if (rgb != null && rgb.isNotEmpty) {
return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2]));
} else { } else {
return null; return null;
} }
@ -62,7 +69,7 @@ class LightEntity extends Entity {
@override @override
Widget _buildAdditionalControlsForPage(BuildContext context) { Widget _buildAdditionalControlsForPage(BuildContext context) {
if (!isAdditionalControls) { if (!isAdditionalControls || state == EntityState.unavailable) {
return Container(height: 0.0, width: 0.0); return Container(height: 0.0, width: 0.0);
} else { } else {
return LightControlsWidget(); return LightControlsWidget();

View File

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

View File

@ -22,53 +22,53 @@ class MediaPlayerEntity extends Entity {
MediaPlayerEntity(Map rawData) : super(rawData); MediaPlayerEntity(Map rawData) : super(rawData);
bool get supportPause => ((attributes["supported_features"] & bool get supportPause => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_PAUSE) == MediaPlayerEntity.SUPPORT_PAUSE) ==
MediaPlayerEntity.SUPPORT_PAUSE); MediaPlayerEntity.SUPPORT_PAUSE);
bool get supportSeek => ((attributes["supported_features"] & bool get supportSeek => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_SEEK) == MediaPlayerEntity.SUPPORT_SEEK) ==
MediaPlayerEntity.SUPPORT_SEEK); MediaPlayerEntity.SUPPORT_SEEK);
bool get supportVolumeSet => ((attributes["supported_features"] & bool get supportVolumeSet => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_VOLUME_SET) == MediaPlayerEntity.SUPPORT_VOLUME_SET) ==
MediaPlayerEntity.SUPPORT_VOLUME_SET); MediaPlayerEntity.SUPPORT_VOLUME_SET);
bool get supportVolumeMute => ((attributes["supported_features"] & bool get supportVolumeMute => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_VOLUME_MUTE) == MediaPlayerEntity.SUPPORT_VOLUME_MUTE) ==
MediaPlayerEntity.SUPPORT_VOLUME_MUTE); MediaPlayerEntity.SUPPORT_VOLUME_MUTE);
bool get supportPreviousTrack => ((attributes["supported_features"] & bool get supportPreviousTrack => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) == MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) ==
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK); MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK);
bool get supportNextTrack => ((attributes["supported_features"] & bool get supportNextTrack => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_NEXT_TRACK) == MediaPlayerEntity.SUPPORT_NEXT_TRACK) ==
MediaPlayerEntity.SUPPORT_NEXT_TRACK); MediaPlayerEntity.SUPPORT_NEXT_TRACK);
bool get supportTurnOn => ((attributes["supported_features"] & bool get supportTurnOn => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_TURN_ON) == MediaPlayerEntity.SUPPORT_TURN_ON) ==
MediaPlayerEntity.SUPPORT_TURN_ON); MediaPlayerEntity.SUPPORT_TURN_ON);
bool get supportTurnOff => ((attributes["supported_features"] & bool get supportTurnOff => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_TURN_OFF) == MediaPlayerEntity.SUPPORT_TURN_OFF) ==
MediaPlayerEntity.SUPPORT_TURN_OFF); MediaPlayerEntity.SUPPORT_TURN_OFF);
bool get supportPlayMedia => ((attributes["supported_features"] & bool get supportPlayMedia => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_PLAY_MEDIA) == MediaPlayerEntity.SUPPORT_PLAY_MEDIA) ==
MediaPlayerEntity.SUPPORT_PLAY_MEDIA); MediaPlayerEntity.SUPPORT_PLAY_MEDIA);
bool get supportVolumeStep => ((attributes["supported_features"] & bool get supportVolumeStep => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_VOLUME_STEP) == MediaPlayerEntity.SUPPORT_VOLUME_STEP) ==
MediaPlayerEntity.SUPPORT_VOLUME_STEP); MediaPlayerEntity.SUPPORT_VOLUME_STEP);
bool get supportSelectSource => ((attributes["supported_features"] & bool get supportSelectSource => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_SELECT_SOURCE) == MediaPlayerEntity.SUPPORT_SELECT_SOURCE) ==
MediaPlayerEntity.SUPPORT_SELECT_SOURCE); MediaPlayerEntity.SUPPORT_SELECT_SOURCE);
bool get supportStop => ((attributes["supported_features"] & bool get supportStop => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_STOP) == MediaPlayerEntity.SUPPORT_STOP) ==
MediaPlayerEntity.SUPPORT_STOP); MediaPlayerEntity.SUPPORT_STOP);
bool get supportClearPlaylist => ((attributes["supported_features"] & bool get supportClearPlaylist => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) == MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) ==
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST); MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST);
bool get supportPlay => ((attributes["supported_features"] & bool get supportPlay => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_PLAY) == MediaPlayerEntity.SUPPORT_PLAY) ==
MediaPlayerEntity.SUPPORT_PLAY); MediaPlayerEntity.SUPPORT_PLAY);
bool get supportShuffleSet => ((attributes["supported_features"] & bool get supportShuffleSet => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_SHUFFLE_SET) == MediaPlayerEntity.SUPPORT_SHUFFLE_SET) ==
MediaPlayerEntity.SUPPORT_SHUFFLE_SET); MediaPlayerEntity.SUPPORT_SHUFFLE_SET);
bool get supportSelectSoundMode => ((attributes["supported_features"] & bool get supportSelectSoundMode => ((supportedFeatures &
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) == MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) ==
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE); MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE);

View File

@ -0,0 +1,36 @@
part of '../main.dart';
class TimerEntity extends Entity {
TimerEntity(Map rawData) : super(rawData);
Duration duration;
@override
void update(Map rawData) {
super.update(rawData);
String durationSource = "${attributes["duration"]}";
List<String> durationList = durationSource.split(":");
if (durationList.length == 1) {
duration = Duration(seconds: int.tryParse(durationList[0] ?? 0));
} else if (durationList.length == 2) {
duration = Duration(
hours: int.tryParse(durationList[0]) ?? 0,
minutes: int.tryParse(durationList[1]) ?? 0
);
} else if (durationList.length == 3) {
duration = Duration(
hours: int.tryParse(durationList[0]) ?? 0,
minutes: int.tryParse(durationList[1]) ?? 0,
seconds: int.tryParse(durationList[2]) ?? 0
);
} else {
Logger.e("Cann't parse $entityId duration: $durationSource");
duration = Duration(seconds: 0);
}
}
@override
Widget _buildStatePart(BuildContext context) {
return TimerState();
}
}

View File

@ -19,7 +19,7 @@ class EntityCollection {
_allEntities.clear(); _allEntities.clear();
//views.clear(); //views.clear();
TheLogger.debug("Parsing ${rawData.length} Home Assistant entities"); Logger.d("Parsing ${rawData.length} Home Assistant entities");
rawData.forEach((rawEntityData) { rawData.forEach((rawEntityData) {
addFromRaw(rawEntityData); addFromRaw(rawEntityData);
}); });
@ -47,7 +47,10 @@ class EntityCollection {
case 'lock': { case 'lock': {
return LockEntity(rawEntityData); return LockEntity(rawEntityData);
} }
case "automation": case "automation": {
return AutomationEntity(rawEntityData);
}
case "input_boolean": case "input_boolean":
case "switch": { case "switch": {
return SwitchEntity(rawEntityData); return SwitchEntity(rawEntityData);
@ -83,17 +86,28 @@ class EntityCollection {
case "fan": { case "fan": {
return FanEntity(rawEntityData); return FanEntity(rawEntityData);
} }
case "camera": {
return CameraEntity(rawEntityData);
}
case "alarm_control_panel": {
return AlarmControlPanelEntity(rawEntityData);
}
case "timer": {
return TimerEntity(rawEntityData);
}
default: { default: {
return Entity(rawEntityData); return Entity(rawEntityData);
} }
} }
} }
void updateState(Map rawStateData) { bool updateState(Map rawStateData) {
if (isExist(rawStateData["entity_id"])) { if (isExist(rawStateData["entity_id"])) {
updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
return false;
} else { } else {
addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
return true;
} }
} }
@ -101,10 +115,9 @@ class EntityCollection {
_allEntities[entity.entityId] = entity; _allEntities[entity.entityId] = entity;
} }
Entity addFromRaw(Map rawEntityData) { void addFromRaw(Map rawEntityData) {
Entity entity = _createEntityInstance(rawEntityData); Entity entity = _createEntityInstance(rawEntityData);
_allEntities[entity.entityId] = entity; _allEntities[entity.entityId] = entity;
return entity;
} }
void updateFromRaw(Map rawEntityData) { void updateFromRaw(Map rawEntityData) {

View File

@ -0,0 +1,50 @@
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;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
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),
size: 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

@ -14,16 +14,37 @@ class BadgeWidget extends StatelessWidget {
{ {
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon" badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
? Icon( ? Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc), MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
size: iconSize, size: iconSize,
) )
: Icon( : Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8), MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
size: iconSize, size: iconSize,
); );
break; break;
} }
case "sensor": case "camera":
case "media_player":
case "binary_sensor":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
break;
}
case "device_tracker":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
onBadgeTextValue = entityModel.entityWrapper.entity.state;
break;
}
default:
{ {
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement; onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
badgeIcon = Center( badgeIcon = Center(
@ -37,18 +58,6 @@ class BadgeWidget extends StatelessWidget {
); );
break; break;
} }
case "device_tracker":
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entityWrapper, iconSize, Colors.black);
onBadgeTextValue = entityModel.entityWrapper.entity.state;
break;
}
default:
{
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
entityModel.entityWrapper, iconSize, Colors.black);
}
} }
Widget onBadgeText; Widget onBadgeText;
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) { if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {

View File

@ -0,0 +1,175 @@
part of '../../main.dart';
class CameraStreamView extends StatefulWidget {
CameraStreamView({Key key}) : super(key: key);
@override
_CameraStreamViewState createState() => _CameraStreamViewState();
}
class _CameraStreamViewState extends State<CameraStreamView> {
@override
void initState() {
super.initState();
}
CameraEntity _entity;
http.Client client;
http.StreamedResponse response;
List<int> binaryImage = [];
bool timeToStop = false;
Completer streamCompleter;
bool started = false;
bool useSVG = false;
void _connect() async {
started = true;
timeToStop = false;
String streamUrl = '$homeAssistantWebHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}';
client = new http.Client(); // create a client to make api calls
http.Request request = new http.Request("GET", Uri.parse(streamUrl)); // create get request
Logger.d("[Sending] ==> $streamUrl");
response = await client.send(request);
Logger.d("[Received] <== ${response.headers}");
String frameBoundary = response.headers['content-type'].split('boundary=')[1];
final int frameBoundarySize = frameBoundary.length;
List<int> primaryBuffer=[];
int imageSizeStart = 59;
int imageSizeEnd = 0;
int imageStart = 0;
int imageSize = 0;
String strBuffer = "";
String contentType = "";
streamCompleter = Completer();
response.stream.transform(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
primaryBuffer.addAll(data);
imageStart = 0;
imageSizeEnd = 0;
if (primaryBuffer.length >= imageSizeStart + 10) {
contentType = utf8.decode(
primaryBuffer.sublist(frameBoundarySize+16, imageSizeStart + 10), allowMalformed: true).split("\r\n")[0];
useSVG = contentType == "image/svg+xml";
imageSizeStart = frameBoundarySize + 16 + contentType.length + 18;
for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) {
strBuffer = utf8.decode(
primaryBuffer.sublist(i, i + 4), allowMalformed: true);
if (strBuffer == "\r\n\r\n") {
imageSizeEnd = i;
imageStart = i + 4;
break;
}
}
if (imageSizeEnd > 0) {
imageSize = int.tryParse(utf8.decode(
primaryBuffer.sublist(imageSizeStart, imageSizeEnd),
allowMalformed: true));
//Logger.d("content-length: $imageSize");
if (imageSize != null &&
primaryBuffer.length >= imageStart + imageSize + 2) {
sink.add(
primaryBuffer.sublist(
imageStart, imageStart + imageSize));
primaryBuffer.removeRange(0, imageStart + imageSize + 2);
}
}
}
if (timeToStop) {
sink?.close();
streamCompleter.complete();
}
},
handleError: (error, stack, sink) {
Logger.e("Error parsing MJPEG stream: $error");
},
handleDone: (sink) {
Logger.d("Camera stream finished. Reconnecting...");
sink?.close();
streamCompleter?.complete();
_reconnect();
},
)
).listen((d) {
if (!timeToStop) {
setState(() {
binaryImage = d;
});
}
});
}
void _reconnect() {
disconnect().then((_){
_connect();
});
}
Future disconnect() {
Completer disconF = Completer();
timeToStop = true;
if (streamCompleter != null && !streamCompleter.isCompleted) {
streamCompleter.future.then((_) {
client?.close();
disconF.complete();
});
} else {
client?.close();
disconF.complete();
}
return disconF.future;
}
@override
Widget build(BuildContext context) {
if (!started) {
_entity = EntityModel
.of(context)
.entityWrapper
.entity;
_connect();
}
if (binaryImage.isEmpty) {
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.all(20.0),
child: const CircularProgressIndicator()
)
],
);
} else {
if (useSVG) {
return Column(
children: <Widget>[
SvgPicture.memory(
Uint8List.fromList(binaryImage),
placeholderBuilder: (BuildContext context) =>
new Container(
padding: const EdgeInsets.all(20.0),
child: const CircularProgressIndicator()
),
)
],
);
} else {
return Column(
children: <Widget>[
Image.memory(
Uint8List.fromList(binaryImage), gaplessPlayback: true),
],
);
}
}
}
@override
void dispose() {
disconnect();
super.dispose();
}
}

View File

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

View File

@ -0,0 +1,41 @@
part of '../../main.dart';
class FlatServiceButton extends StatelessWidget {
final String serviceDomain;
final String serviceName;
final String entityId;
final String text;
final double fontSize;
FlatServiceButton({
Key key,
@required this.serviceDomain,
@required this.serviceName,
@required this.entityId,
@required this.text,
this.fontSize: Sizes.stateFontSize
}) : super(key: key);
void _setNewState() {
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null));
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: fontSize*2.5,
child: FlatButton(
onPressed: (() {
_setNewState();
}),
child: Text(
text,
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: fontSize, color: Colors.blue),
),
)
);
}
}

View File

@ -6,7 +6,7 @@ class LastUpdatedWidget extends StatelessWidget {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
return Padding( return Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, 0.0, 0.0, 0.0), Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, 0.0),
child: Text( child: Text(
'${entityModel.entityWrapper.entity.lastUpdated}', '${entityModel.entityWrapper.entity.lastUpdated}',
textAlign: TextAlign.left, textAlign: TextAlign.left,

View File

@ -0,0 +1,101 @@
part of '../../main.dart';
class LightColorPicker extends StatefulWidget {
final HSVColor color;
final onColorSelected;
final double hueStep;
final double saturationStep;
final EdgeInsets padding;
LightColorPicker({this.color, this.onColorSelected, this.hueStep: 15.0, this.saturationStep: 0.2, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)});
@override
LightColorPickerState createState() => new LightColorPickerState();
}
class LightColorPickerState extends State<LightColorPicker> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
List<Widget> colorRows = [];
Border border;
bool isSomethingSelected = false;
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
List<Widget> rowChildren = [];
//Logger.d("$saturation");
double roundedSaturation = double.parse(widget.color.saturation.toStringAsFixed(1));
//Logger.d("Rounded saturation=$roundedSaturation");
for (double hue = 0; hue <= (365 - widget.hueStep);
hue += widget.hueStep) {
bool isExactHue = widget.color.hue.round() == hue;
bool isHueInRange = widget.color.hue.round() > hue && widget.color.hue.round() < (hue+widget.hueStep);
bool isExactSaturation = roundedSaturation == saturation;
bool isSaturationInRange = roundedSaturation > saturation && roundedSaturation < double.parse((saturation+widget.saturationStep).toStringAsFixed(1));
if ((isExactHue || isHueInRange) && (isExactSaturation || isSaturationInRange)) {
//Logger.d("$isExactHue $isHueInRange $isExactSaturation $isSaturationInRange (${saturation+widget.saturationStep})");
border = Border.all(
width: 2.0,
color: Colors.white,
);
isSomethingSelected = true;
} else {
border = null;
}
HSVColor currentColor = HSVColor.fromAHSV(1.0, hue, double.parse(saturation.toStringAsFixed(2)), 1.0);
rowChildren.add(
Flexible(
child: GestureDetector(
child: Container(
height: 40.0,
decoration: BoxDecoration(
color: currentColor.toColor(),
border: border,
),
),
onTap: () => widget.onColorSelected(currentColor),
)
)
);
}
colorRows.add(
Row(
children: rowChildren,
)
);
}
colorRows.add(
Flexible(
child: GestureDetector(
child: Container(
height: 40.0,
decoration: BoxDecoration(
color: Colors.white,
border: isSomethingSelected ? null : Border.all(
width: 2.0,
color: Colors.amber[200],
)
),
),
onTap: () => widget.onColorSelected(HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0)),
)
)
);
return Padding(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: colorRows,
),
padding: widget.padding,
);
}
}

View File

@ -7,8 +7,8 @@ class ModeSelectorWidget extends StatelessWidget {
final String value; final String value;
final double captionFontSize; final double captionFontSize;
final double valueFontSize; final double valueFontSize;
final double bottomPadding;
final onChange; final onChange;
final EdgeInsets padding;
ModeSelectorWidget({ ModeSelectorWidget({
Key key, Key key,
@ -18,45 +18,47 @@ class ModeSelectorWidget extends StatelessWidget {
@required this.onChange, @required this.onChange,
this.captionFontSize, this.captionFontSize,
this.valueFontSize, this.valueFontSize,
this.bottomPadding this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: padding,
children: <Widget>[ child: Column(
Text("$caption", style: TextStyle( crossAxisAlignment: CrossAxisAlignment.start,
fontSize: captionFontSize ?? Sizes.stateFontSize children: <Widget>[
)), Text("$caption", style: TextStyle(
Row( fontSize: captionFontSize ?? Sizes.stateFontSize
children: <Widget>[ )),
Expanded( Row(
child: ButtonTheme( children: <Widget>[
alignedDropdown: true, Expanded(
child: DropdownButton<String>( child: ButtonTheme(
value: value, alignedDropdown: true,
iconSize: 30.0, child: DropdownButton<String>(
isExpanded: true, value: value,
style: TextStyle( iconSize: 30.0,
fontSize: valueFontSize ?? Sizes.largeFontSize, isExpanded: true,
color: Colors.black, style: TextStyle(
fontSize: valueFontSize ?? Sizes.largeFontSize,
color: Colors.black,
),
hint: Text("Select ${caption.toLowerCase()}"),
items: options.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (mode) => onChange(mode),
), ),
hint: Text("Select ${caption.toLowerCase()}"),
items: options.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (mode) => onChange(mode),
), ),
), )
) ],
], )
), ],
Container(height: bottomPadding ?? Sizes.rowPadding,) ),
],
); );
} }
} }

View File

@ -7,6 +7,7 @@ class ModeSwitchWidget extends StatelessWidget {
final double captionFontSize; final double captionFontSize;
final bool value; final bool value;
final bool expanded; final bool expanded;
final EdgeInsets padding;
ModeSwitchWidget({ ModeSwitchWidget({
Key key, Key key,
@ -14,19 +15,23 @@ class ModeSwitchWidget extends StatelessWidget {
@required this.onChange, @required this.onChange,
this.captionFontSize, this.captionFontSize,
this.value, this.value,
this.expanded: true this.expanded: true,
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Padding(
children: <Widget>[ padding: this.padding,
_buildCaption(), child: Row(
Switch( children: <Widget>[
onChanged: (value) => onChange(value), _buildCaption(),
value: value ?? false, Switch(
) onChanged: (value) => onChange(value),
], value: value ?? false,
)
],
)
); );
} }

View File

@ -10,8 +10,9 @@ class UniversalSlider extends StatelessWidget {
final double min; final double min;
final double max; final double max;
final double value; final double value;
final EdgeInsets padding;
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value}) : super(key: key); const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -33,21 +34,24 @@ class UniversalSlider extends StatelessWidget {
if (closing != null) { if (closing != null) {
row.add(closing); row.add(closing);
} }
return Column( return Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: padding,
children: <Widget>[ child: Column(
Container(height: Sizes.rowPadding,), crossAxisAlignment: CrossAxisAlignment.start,
Text( children: <Widget>[
"$title", Container(height: Sizes.rowPadding,),
style: TextStyle(fontSize: Sizes.stateFontSize), Text(
), "$title",
Container(height: Sizes.rowPadding,), style: TextStyle(fontSize: Sizes.stateFontSize),
Row( ),
mainAxisSize: MainAxisSize.min, Container(height: Sizes.rowPadding,),
children: row, Row(
), mainAxisSize: MainAxisSize.min,
Container(height: Sizes.rowPadding,) children: row,
], ),
Container(height: Sizes.rowPadding,)
],
),
); );
} }

View File

@ -0,0 +1,262 @@
part of '../../main.dart';
class AlarmControlPanelControlsWidget extends StatefulWidget {
final bool extended;
final List states;
const AlarmControlPanelControlsWidget({Key key, @required this.extended, this.states}) : super(key: key);
@override
_AlarmControlPanelControlsWidgetWidgetState createState() => _AlarmControlPanelControlsWidgetWidgetState();
}
class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPanelControlsWidget> {
String code = "";
List supportedStates;
@override
void initState() {
super.initState();
supportedStates = widget.states ?? ["arm_home", "arm_away"];
}
void _callService(AlarmControlPanelEntity entity, String service) {
eventBus.fire(new ServiceCallEvent(
entity.domain, service, entity.entityId,
{"code": "$code"}));
setState(() {
code = "";
});
}
void _pinPadHandler(value) {
setState(() {
code += "$value";
});
}
void _pinPadClear() {
setState(() {
code = "";
});
}
void _askToTrigger(AlarmControlPanelEntity entity) {
// flutter defined function
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text("Are you sure?"),
content: new Text("Are you sure want to trigger alarm ${entity.displayName}?"),
actions: <Widget>[
FlatButton(
child: new Text("Yes"),
onPressed: () {
eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null));
Navigator.of(context).pop();
},
),
FlatButton(
child: new Text("No"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final AlarmControlPanelEntity entity = entityModel.entityWrapper.entity;
List<Widget> buttons = [];
if (entity.state == EntityState.alarm_disarmed) {
if (supportedStates.contains("arm_home")) {
buttons.add(
RaisedButton(
onPressed: () => _callService(entity, "alarm_arm_home"),
child: Text("ARM HOME"),
)
);
}
if (supportedStates.contains("arm_away")) {
buttons.add(
RaisedButton(
onPressed: () => _callService(entity, "alarm_arm_away"),
child: Text("ARM AWAY"),
)
);
}
if (widget.extended) {
if (supportedStates.contains("arm_night")) {
buttons.add(
RaisedButton(
onPressed: () => _callService(entity, "alarm_arm_night"),
child: Text("ARM NIGHT"),
)
);
}
if (supportedStates.contains("arm_custom_bypass")) {
buttons.add(
RaisedButton(
onPressed: () =>
_callService(entity, "alarm_arm_custom_bypass"),
child: Text("ARM CUSTOM BYPASS"),
)
);
}
}
} else {
buttons.add(
RaisedButton(
onPressed: () => _callService(entity, "alarm_disarm"),
child: Text("DISARM"),
)
);
}
Widget pinPad;
if (entity.attributes["code_format"] == null) {
pinPad = Container(width: 0.0, height: 0.0,);
} else {
pinPad = Padding(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Wrap(
spacing: 5.0,
children: <Widget>[
RaisedButton(
onPressed: () => _pinPadHandler("1"),
child: Text("1"),
),
RaisedButton(
onPressed: () => _pinPadHandler("2"),
child: Text("2"),
),
RaisedButton(
onPressed: () => _pinPadHandler("3"),
child: Text("3"),
)
],
),
Wrap(
spacing: 5.0,
children: <Widget>[
RaisedButton(
onPressed: () => _pinPadHandler("4"),
child: Text("4"),
),
RaisedButton(
onPressed: () => _pinPadHandler("5"),
child: Text("5"),
),
RaisedButton(
onPressed: () => _pinPadHandler("6"),
child: Text("6"),
)
],
),
Wrap(
spacing: 5.0,
children: <Widget>[
RaisedButton(
onPressed: () => _pinPadHandler("7"),
child: Text("7"),
),
RaisedButton(
onPressed: () => _pinPadHandler("8"),
child: Text("8"),
),
RaisedButton(
onPressed: () => _pinPadHandler("9"),
child: Text("9"),
)
],
),
Wrap(
spacing: 5.0,
alignment: WrapAlignment.end,
children: <Widget>[
RaisedButton(
onPressed: () => _pinPadHandler("0"),
child: Text("0"),
),
RaisedButton(
onPressed: () => _pinPadClear(),
child: Text("CLEAR"),
)
],
)
],
)
);
}
Widget inputWrapper;
if (entity.attributes["code_format"] == null) {
inputWrapper = Container(width: 0.0, height: 0.0,);
} else {
inputWrapper = Container(
width: 150.0,
child: TextField(
decoration: InputDecoration(
labelText: "Alarm Code"
),
//focusNode: _focusNode,
obscureText: true,
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: code,
selection:
new TextSelection.collapsed(offset: code.length)
)
),
onChanged: (value) {
code = value;
}
)
);
}
Widget buttonsWrapper = Padding(
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 15.0,
runSpacing: Sizes.rowPadding,
children: buttons
)
);
Widget triggerButton = Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlatButton(
child: Text(
"TRIGGER",
style: TextStyle(color: Colors.redAccent)
),
onPressed: () => _askToTrigger(entity),
)
]
);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
widget.extended ? buttonsWrapper : inputWrapper,
widget.extended ? inputWrapper : buttonsWrapper,
widget.extended ? pinPad : triggerButton
]
);
}
}

View File

@ -13,6 +13,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _showPending = false; bool _showPending = false;
bool _changedHere = false; bool _changedHere = false;
Timer _resetTimer; Timer _resetTimer;
Timer _tempThrottleTimer;
Timer _targetTempThrottleTimer;
double _tmpTemperature = 0.0; double _tmpTemperature = 0.0;
double _tmpTargetLow = 0.0; double _tmpTargetLow = 0.0;
double _tmpTargetHigh = 0.0; double _tmpTargetHigh = 0.0;
@ -40,52 +42,68 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_changedHere = false; _changedHere = false;
} }
void _temperatureUp(ClimateEntity entity, double step) { void _temperatureUp(ClimateEntity entity) {
_tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp; _tmpTemperature = ((_tmpTemperature + entity.temperatureStep) <= entity.maxTemp) ? _tmpTemperature + entity.temperatureStep : entity.maxTemp;
_setTemperature(entity); _setTemperature(entity);
} }
void _temperatureDown(ClimateEntity entity, double step) { void _temperatureDown(ClimateEntity entity) {
_tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp; _tmpTemperature = ((_tmpTemperature - entity.temperatureStep) >= entity.minTemp) ? _tmpTemperature - entity.temperatureStep : entity.minTemp;
_setTemperature(entity); _setTemperature(entity);
} }
void _targetLowUp(ClimateEntity entity, double step) { void _targetLowUp(ClimateEntity entity) {
_tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp; _tmpTargetLow = ((_tmpTargetLow + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetLow + entity.temperatureStep : entity.maxTemp;
_setTargetTemp(entity); _setTargetTemp(entity);
} }
void _targetLowDown(ClimateEntity entity, double step) { void _targetLowDown(ClimateEntity entity) {
_tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp; _tmpTargetLow = ((_tmpTargetLow - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetLow - entity.temperatureStep : entity.minTemp;
_setTargetTemp(entity); _setTargetTemp(entity);
} }
void _targetHighUp(ClimateEntity entity, double step) { void _targetHighUp(ClimateEntity entity) {
_tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp; _tmpTargetHigh = ((_tmpTargetHigh + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetHigh + entity.temperatureStep : entity.maxTemp;
_setTargetTemp(entity); _setTargetTemp(entity);
} }
void _targetHighDown(ClimateEntity entity, double step) { void _targetHighDown(ClimateEntity entity) {
_tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp; _tmpTargetHigh = ((_tmpTargetHigh - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetHigh - entity.temperatureStep : entity.minTemp;
_setTargetTemp(entity); _setTargetTemp(entity);
} }
void _setTemperature(ClimateEntity entity) { void _setTemperature(ClimateEntity entity) {
if (_tempThrottleTimer!=null) {
_tempThrottleTimer.cancel();
}
setState(() { setState(() {
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
_changedHere = true; _changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"})); _tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
_resetStateTimer(entity); });
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() {
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
});
}); });
} }
void _setTargetTemp(ClimateEntity entity) { void _setTargetTemp(ClimateEntity entity) {
if (_targetTempThrottleTimer!=null) {
_targetTempThrottleTimer.cancel();
}
setState(() { setState(() {
_changedHere = true;
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1)); _tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1)); _tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
_changedHere = true; });
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"})); _targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
_resetStateTimer(entity); setState(() {
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
});
}); });
} }
@ -167,7 +185,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
final entityModel = EntityModel.of(context); final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity; final ClimateEntity entity = entityModel.entityWrapper.entity;
if (_changedHere) { if (_changedHere) {
_showPending = (_tmpTemperature != entity.temperature); _showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
_changedHere = false; _changedHere = false;
} else { } else {
_resetTimer?.cancel(); _resetTimer?.cancel();
@ -278,10 +296,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
TemperatureControlWidget( TemperatureControlWidget(
value: _tmpTemperature, value: _tmpTemperature,
fontColor: _showPending ? Colors.red : Colors.black, fontColor: _showPending ? Colors.red : Colors.black,
onLargeDec: () => _temperatureDown(entity, 0.5), onDec: () => _temperatureDown(entity),
onLargeInc: () => _temperatureUp(entity, 0.5), onInc: () => _temperatureUp(entity),
onSmallDec: () => _temperatureDown(entity, 0.1),
onSmallInc: () => _temperatureUp(entity, 0.1),
) )
], ],
); );
@ -297,10 +313,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
TemperatureControlWidget( TemperatureControlWidget(
value: _tmpTargetLow, value: _tmpTargetLow,
fontColor: _showPending ? Colors.red : Colors.black, fontColor: _showPending ? Colors.red : Colors.black,
onLargeDec: () => _targetLowDown(entity, 0.5), onDec: () => _targetLowDown(entity),
onLargeInc: () => _targetLowUp(entity, 0.5), onInc: () => _targetLowUp(entity),
onSmallDec: () => _targetLowDown(entity, 0.1),
onSmallInc: () => _targetLowUp(entity, 0.1),
), ),
Expanded( Expanded(
child: Container(height: 10.0), child: Container(height: 10.0),
@ -312,10 +326,8 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
TemperatureControlWidget( TemperatureControlWidget(
value: _tmpTargetHigh, value: _tmpTargetHigh,
fontColor: _showPending ? Colors.red : Colors.black, fontColor: _showPending ? Colors.red : Colors.black,
onLargeDec: () => _targetHighDown(entity, 0.5), onDec: () => _targetHighDown(entity),
onLargeInc: () => _targetHighUp(entity, 0.5), onInc: () => _targetHighUp(entity),
onSmallDec: () => _targetHighDown(entity, 0.1),
onSmallInc: () => _targetHighUp(entity, 0.1),
) )
); );
} }
@ -401,18 +413,14 @@ class TemperatureControlWidget extends StatelessWidget {
final double value; final double value;
final double fontSize; final double fontSize;
final Color fontColor; final Color fontColor;
final onSmallInc; final onInc;
final onLargeInc; final onDec;
final onSmallDec;
final onLargeDec;
TemperatureControlWidget( TemperatureControlWidget(
{Key key, {Key key,
@required this.value, @required this.value,
@required this.onSmallInc, @required this.onInc,
@required this.onSmallDec, @required this.onDec,
@required this.onLargeInc,
@required this.onLargeDec,
this.fontSize, this.fontSize,
this.fontColor}) this.fontColor})
: super(key: key); : super(key: key);
@ -432,32 +440,16 @@ class TemperatureControlWidget extends StatelessWidget {
Column( Column(
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-up')), 'mdi:chevron-up')),
iconSize: 30.0, iconSize: 30.0,
onPressed: () => onSmallInc(), onPressed: () => onInc(),
), ),
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-down')), 'mdi:chevron-down')),
iconSize: 30.0, iconSize: 30.0,
onPressed: () => onSmallDec(), onPressed: () => onDec(),
)
],
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-double-up')),
iconSize: 30.0,
onPressed: () => onLargeInc(),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-double-down')),
iconSize: 30.0,
onPressed: () => onLargeDec(),
) )
], ],
) )

View File

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

View File

@ -10,16 +10,18 @@ class LightControlsWidget extends StatefulWidget {
class _LightControlsWidgetState extends State<LightControlsWidget> { class _LightControlsWidgetState extends State<LightControlsWidget> {
int _tmpBrightness; int _tmpBrightness;
int _tmpColorTemp; int _tmpWhiteValue;
Color _tmpColor; int _tmpColorTemp = 0;
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
bool _changedHere = false; bool _changedHere = false;
String _tmpEffect; String _tmpEffect;
void _resetState(LightEntity entity) { void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 0; _tmpBrightness = entity.brightness ?? 0;
_tmpColorTemp = entity.colorTemp; _tmpWhiteValue = entity.whiteValue ?? 0;
_tmpColor = entity.color; _tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
_tmpEffect = null; _tmpColor = entity.color ?? _tmpColor;
_tmpEffect = entity.effect;
} }
void _setBrightness(LightEntity entity, double value) { void _setBrightness(LightEntity entity, double value) {
@ -38,6 +40,17 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}); });
} }
void _setWhiteValue(LightEntity entity, double value) {
setState(() {
_tmpWhiteValue = value.round();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"white_value": _tmpWhiteValue}));
});
}
void _setColorTemp(LightEntity entity, double value) { void _setColorTemp(LightEntity entity, double value) {
setState(() { setState(() {
_tmpColorTemp = value.round(); _tmpColorTemp = value.round();
@ -48,20 +61,14 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}); });
} }
void _setColor(LightEntity entity, Color color) { void _setColor(LightEntity entity, HSVColor color) {
setState(() { setState(() {
_tmpColor = color; _tmpColor = color;
_changedHere = true; _changedHere = true;
TheLogger.debug( "Color: [${color.red}, ${color.green}, ${color.blue}]"); Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
if ((color == Colors.black) || ((color.red == color.green) && (color.green == color.blue))) { eventBus.fire(new ServiceCallEvent(
eventBus.fire(new ServiceCallEvent( entity.domain, "turn_on", entity.entityId,
entity.domain, "turn_off", entity.entityId, {"hs_color": [color.hue, color.saturation*100]}));
null));
} else {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"rgb_color": [color.red, color.green, color.blue]}));
}
}); });
} }
@ -90,6 +97,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
_buildBrightnessControl(entity), _buildBrightnessControl(entity),
_buildWhiteValueControl(entity),
_buildColorTempControl(entity), _buildColorTempControl(entity),
_buildColorControl(entity), _buildColorControl(entity),
_buildEffectControl(entity) _buildEffectControl(entity)
@ -98,7 +106,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
} }
Widget _buildBrightnessControl(LightEntity entity) { Widget _buildBrightnessControl(LightEntity entity) {
if ((entity.supportBrightness) && (_tmpBrightness != null) && (entity.state != EntityState.unavailable)) { if ((entity.supportBrightness) && (_tmpBrightness != null)) {
return UniversalSlider( return UniversalSlider(
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
@ -109,7 +117,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
min: 0.0, min: 0.0,
max: 255.0, max: 255.0,
onChangeEnd: (value) => _setBrightness(entity, value), onChangeEnd: (value) => _setBrightness(entity, value),
value: _tmpBrightness.toDouble(), value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(),
leading: Icon(Icons.brightness_5), leading: Icon(Icons.brightness_5),
title: "Brightness", title: "Brightness",
); );
@ -118,12 +126,33 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
} }
} }
Widget _buildWhiteValueControl(LightEntity entity) {
if ((entity.supportWhiteValue) && (_tmpWhiteValue != null)) {
return UniversalSlider(
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpWhiteValue = value.round();
});
},
min: 0.0,
max: 255.0,
onChangeEnd: (value) => _setWhiteValue(entity, value),
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
title: "White",
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildColorTempControl(LightEntity entity) { Widget _buildColorTempControl(LightEntity entity) {
if ((entity.supportColorTemp) && (_tmpColorTemp != null)) { if (entity.supportColorTemp) {
return UniversalSlider( return UniversalSlider(
title: "Color temperature", title: "Color temperature",
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),), leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
value: _tmpColorTemp.toDouble(), value: _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(),
onChangeEnd: (value) => _setColorTemp(entity, value), onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds, max: entity.maxMireds,
min: entity.minMireds, min: entity.minMireds,
@ -141,25 +170,39 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
} }
Widget _buildColorControl(LightEntity entity) { Widget _buildColorControl(LightEntity entity) {
if ((entity.supportColor) && (entity.color != null)) { if (entity.supportColor) {
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Container(height: Sizes.rowPadding,), LightColorPicker(
RaisedButton( color: _tmpColor,
onPressed: () => _showColorPicker(entity), onColorSelected: (color) => _setColor(entity, color),
color: _tmpColor ?? Colors.black45,
child: Text(
"COLOR",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 50.0,
fontWeight: FontWeight.bold,
color: Colors.black12,
),
),
), ),
Container(height: 2*Sizes.rowPadding,), Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
color: _tmpColor.toColor(),
child: Text('Copy color'),
onPressed: _tmpColor == null ? null : () {
setState(() {
HomeAssistantModel
.of(context)
.homeAssistant
.savedColor = _tmpColor;
});
},
),
FlatButton(
color: savedColor?.toColor() ?? Colors.transparent,
child: Text('Paste color'),
onPressed: savedColor == null ? null : () {
_setColor(entity, savedColor);
},
)
],
)
], ],
); );
} else { } else {
@ -167,28 +210,6 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
} }
} }
void _showColorPicker(LightEntity entity) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
titlePadding: EdgeInsets.all(0.0),
contentPadding: EdgeInsets.all(0.0),
content: SingleChildScrollView(
child: MaterialPicker(
pickerColor: _tmpColor,
onColorChanged: (color) {
_setColor(entity, color);
Navigator.of(context).pop();
},
enableLabel: true,
),
),
);
},
);
}
Widget _buildEffectControl(LightEntity entity) { Widget _buildEffectControl(LightEntity entity) {
if ((entity.supportEffect) && (entity.effectList != null)) { if ((entity.supportEffect) && (entity.effectList != null)) {
return ModeSelectorWidget( return ModeSelectorWidget(

View File

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

View File

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

View File

@ -2,6 +2,8 @@ part of '../main.dart';
class EntityColor { class EntityColor {
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
static const badgeColors = { static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0), "default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0) "binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
@ -10,19 +12,29 @@ class EntityColor {
static const _stateColors = { static const _stateColors = {
EntityState.on: Colors.amber, EntityState.on: Colors.amber,
"auto": Colors.amber, "auto": Colors.amber,
EntityState.idle: Colors.amber, EntityState.active: Colors.amber,
EntityState.playing: Colors.amber, EntityState.playing: Colors.amber,
"above_horizon": Colors.amber, "above_horizon": Colors.amber,
EntityState.home: Colors.amber, EntityState.home: Colors.amber,
EntityState.open: Colors.amber, EntityState.open: Colors.amber,
EntityState.off: Color.fromRGBO(68, 115, 158, 1.0), EntityState.off: defaultStateColor,
EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0), EntityState.closed: defaultStateColor,
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0), "below_horizon": defaultStateColor,
"default": Color.fromRGBO(68, 115, 158, 1.0), "default": defaultStateColor,
EntityState.idle: defaultStateColor,
"heat": Colors.redAccent, "heat": Colors.redAccent,
"cool": Colors.lightBlue, "cool": Colors.lightBlue,
EntityState.unavailable: Colors.black26, EntityState.unavailable: Colors.black26,
EntityState.unknown: Colors.black26, EntityState.unknown: Colors.black26,
EntityState.alarm_disarmed: Colors.green,
EntityState.alarm_armed_away: Colors.redAccent,
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
EntityState.alarm_armed_home: Colors.redAccent,
EntityState.alarm_armed_night: Colors.redAccent,
EntityState.alarm_triggered: Colors.redAccent,
EntityState.alarm_arming: Colors.amber,
EntityState.alarm_disarming: Colors.amber,
EntityState.alarm_pending: Colors.amber,
}; };
static Color stateColor(String state) { static Color stateColor(String state) {

View File

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

View File

@ -7,19 +7,25 @@ class EntityName extends StatelessWidget {
final bool wordsWrap; final bool wordsWrap;
final double fontSize; final double fontSize;
final TextAlign textAlign; final TextAlign textAlign;
final int maxLines;
const EntityName({Key key, 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); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
TextStyle textStyle = TextStyle(fontSize: fontSize);
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
}
return Padding( return Padding(
padding: padding, padding: padding,
child: Text( child: Text(
"${entityWrapper.displayName}", "${entityWrapper.displayName}",
overflow: textOverflow, overflow: textOverflow,
softWrap: wordsWrap, softWrap: wordsWrap,
style: TextStyle(fontSize: fontSize), maxLines: maxLines,
style: textStyle,
textAlign: textAlign, textAlign: textAlign,
), ),
); );

View File

@ -4,41 +4,58 @@ class GlanceEntityContainer extends StatelessWidget {
final bool showName; final bool showName;
final bool showState; final bool showState;
final bool nameInTheBottom;
final double iconSize;
final double nameFontSize;
final bool wordsWrapInName;
GlanceEntityContainer({ GlanceEntityContainer({
Key key, @required this.showName, @required this.showState, 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); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = []; List<Widget> result = [];
if (showName) { if (!nameInTheBottom) {
result.add(EntityName( if (showName) {
padding: EdgeInsets.only(bottom: Sizes.rowPadding), result.add(_buildName());
textOverflow: TextOverflow.ellipsis, }
wordsWrap: false, } else {
textAlign: TextAlign.center, if (showState) {
fontSize: Sizes.smallFontSize, result.add(_buildState());
)); }
} }
result.add( result.add(
EntityIcon( EntityIcon(
padding: EdgeInsets.all(0.0), padding: EdgeInsets.all(0.0),
iconSize: Sizes.iconSize, size: iconSize,
) )
); );
if (showState) { if (!nameInTheBottom) {
result.add(SimpleEntityState( if (showState) {
textAlign: TextAlign.center, result.add(_buildState());
expanded: false, }
padding: EdgeInsets.only(top: Sizes.rowPadding), } else {
)); result.add(_buildName());
} }
return Center( return Center(
child: InkResponse( child: InkResponse(
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(minWidth: Sizes.iconSize*2), constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
//mainAxisAlignment: MainAxisAlignment.start, //mainAxisAlignment: MainAxisAlignment.start,
@ -51,4 +68,23 @@ class GlanceEntityContainer extends StatelessWidget {
), ),
); );
} }
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

@ -94,11 +94,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
} }
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() { List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
TheLogger.debug(" parsing history..."); Logger.d(" parsing history...");
Map<String, List<EntityHistoryMoment>> numericDataLists = {}; Map<String, List<EntityHistoryMoment>> numericDataLists = {};
int colorIdCounter = 0; int colorIdCounter = 0;
widget.config.numericAttributesToShow.forEach((String attrName) { widget.config.numericAttributesToShow.forEach((String attrName) {
TheLogger.debug(" parsing attribute $attrName"); Logger.d(" parsing attribute $attrName");
List<EntityHistoryMoment> data = []; List<EntityHistoryMoment> data = [];
DateTime now = DateTime.now(); DateTime now = DateTime.now();
for (var i = 0; i < widget.rawHistory.length; i++) { for (var i = 0; i < widget.rawHistory.length; i++) {
@ -152,7 +152,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
} }
List<charts.Series<EntityHistoryMoment, DateTime>> result = []; List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
numericDataLists.forEach((attrName, dataList) { numericDataLists.forEach((attrName, dataList) {
TheLogger.debug(" adding ${dataList.length} data values"); Logger.d(" adding ${dataList.length} data values");
result.add( result.add(
new charts.Series<EntityHistoryMoment, DateTime>( new charts.Series<EntityHistoryMoment, DateTime>(
id: "value", id: "value",

View File

@ -32,6 +32,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
List _history; List _history;
bool _needToUpdateHistory; bool _needToUpdateHistory;
DateTime _historyLastUpdated; DateTime _historyLastUpdated;
bool _disposed = false;
@override @override
void initState() { void initState() {
@ -42,21 +43,25 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
void _loadHistory(HomeAssistant ha, String entityId) { void _loadHistory(HomeAssistant ha, String entityId) {
DateTime now = DateTime.now(); DateTime now = DateTime.now();
if (_historyLastUpdated != null) { if (_historyLastUpdated != null) {
TheLogger.debug("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago"); Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
} }
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) { if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now; _historyLastUpdated = now;
ha.getHistory(entityId).then((history){ ha.getHistory(entityId).then((history){
setState(() { if (!_disposed) {
_history = history.isNotEmpty ? history[0] : []; setState(() {
_needToUpdateHistory = false; _history = history.isNotEmpty ? history[0] : [];
}); _needToUpdateHistory = false;
});
}
}).catchError((e) { }).catchError((e) {
TheLogger.error("Error loading $entityId history: $e"); Logger.e("Error loading $entityId history: $e");
setState(() { if (!_disposed) {
_history = []; setState(() {
_needToUpdateHistory = false; _history = [];
}); _needToUpdateHistory = false;
});
}
}); });
} }
} }
@ -122,7 +127,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
} }
default: { default: {
TheLogger.debug(" Simple selected as default"); Logger.d(" Simple selected as default");
return SimpleStateHistoryChartWidget( return SimpleStateHistoryChartWidget(
rawHistory: _history, rawHistory: _history,
); );
@ -131,4 +136,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
} }
@override
void dispose() {
_disposed = true;
super.dispose();
}
} }

View File

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

View File

@ -1,27 +0,0 @@
part of '../../main.dart';
class ButtonStateWidget extends StatelessWidget {
void _setNewState(Entity entity) {
eventBus.fire(new ServiceCallEvent(entity.domain, "turn_on", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
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

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

View File

@ -54,7 +54,7 @@ class DateTimeStateWidget extends StatelessWidget {
} }
}); });
} else { } else {
TheLogger.warning( "${entity.entityId} has no date and no time"); Logger.w( "${entity.entityId} has no date and no time");
} }
} }

View File

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

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

View File

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

View File

@ -85,7 +85,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
}), }),
); );
} else { } else {
TheLogger.warning( "Unsupported input mode for ${entity.entityId}"); Logger.w( "Unsupported input mode for ${entity.entityId}");
return SimpleEntityState(); return SimpleEntityState();
} }
} }

View File

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

View File

@ -3,33 +3,26 @@ part of 'main.dart';
class HomeAssistant { class HomeAssistant {
String _webSocketAPIEndpoint; String _webSocketAPIEndpoint;
String _password; String _password;
String _authType;
bool _useLovelace = false; bool _useLovelace = false;
IOWebSocketChannel _hassioChannel; IOWebSocketChannel _hassioChannel;
SendMessageQueue _messageQueue; SendMessageQueue _messageQueue;
int _currentMessageId = 0; int _currentMessageId = 0;
int _statesMessageId = 0;
int _servicesMessageId = 0;
int _subscriptionMessageId = 0; int _subscriptionMessageId = 0;
int _configMessageId = 0; Map<int, Completer> _messageResolver = {};
int _userInfoMessageId = 0;
int _lovelaceMessageId = 0;
EntityCollection entities; EntityCollection entities;
HomeAssistantUI ui; HomeAssistantUI ui;
Map _instanceConfig = {}; Map _instanceConfig = {};
String _userName; String _userName;
HSVColor savedColor;
Map _rawLovelaceData; Map _rawLovelaceData;
List<Panel> panels = [];
Completer _fetchCompleter; Completer _fetchCompleter;
Completer _statesCompleter;
Completer _servicesCompleter;
Completer _lovelaceCompleter;
Completer _configCompleter;
Completer _connectionCompleter; Completer _connectionCompleter;
Completer _userInfoCompleter;
Timer _connectionTimer; Timer _connectionTimer;
Timer _fetchTimer; Timer _fetchTimer;
bool autoReconnect = false; bool autoReconnect = false;
@ -56,21 +49,20 @@ class HomeAssistant {
_messageQueue = SendMessageQueue(messageExpirationTime); _messageQueue = SendMessageQueue(messageExpirationTime);
} }
void updateSettings(String url, String password, String authType, bool useLovelace) { void updateSettings(String url, String password, bool useLovelace) {
_webSocketAPIEndpoint = url; _webSocketAPIEndpoint = url;
_password = password; _password = password;
_authType = authType;
_useLovelace = useLovelace; _useLovelace = useLovelace;
TheLogger.debug( "Use lovelace is $_useLovelace"); Logger.d( "Use lovelace is $_useLovelace");
} }
Future fetch() { Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) { if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
TheLogger.warning("Previous fetch is not complited"); Logger.w("Previous fetch is not complited");
} else { } else {
_fetchCompleter = new Completer(); _fetchCompleter = new Completer();
_fetchTimer = Timer(fetchTimeout, () { _fetchTimer = Timer(fetchTimeout, () {
TheLogger.error( "Data fetching timeout"); Logger.e( "Data fetching timeout");
disconnect().then((_) { disconnect().then((_) {
_completeFetching({ _completeFetching({
"errorCode": 9, "errorCode": 9,
@ -90,7 +82,7 @@ class HomeAssistant {
disconnect() async { disconnect() async {
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) { if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
await _hassioChannel.sink.close().timeout(Duration(seconds: 3), await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
onTimeout: () => TheLogger.debug( "Socket sink closed") onTimeout: () => Logger.d( "Socket sink closed")
); );
await _socketSubscription.cancel(); await _socketSubscription.cancel();
_hassioChannel = null; _hassioChannel = null;
@ -100,15 +92,15 @@ class HomeAssistant {
Future _connection() { Future _connection() {
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) { if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
TheLogger.debug("Previous connection is not complited"); Logger.d("Previous connection is not complited");
} else { } else {
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionCompleter = new Completer(); _connectionCompleter = new Completer();
autoReconnect = false; autoReconnect = false;
disconnect().then((_){ disconnect().then((_){
TheLogger.debug( "Socket connecting..."); Logger.d( "Socket connecting...");
_connectionTimer = Timer(connectTimeout, () { _connectionTimer = Timer(connectTimeout, () {
TheLogger.error( "Socket connection timeout"); Logger.e( "Socket connection timeout");
_handleSocketError(null); _handleSocketError(null);
}); });
if (_socketSubscription != null) { if (_socketSubscription != null) {
@ -131,15 +123,15 @@ class HomeAssistant {
} }
void _handleSocketClose() { void _handleSocketClose() {
TheLogger.debug("Socket disconnected. Automatic reconnect is $autoReconnect"); Logger.d("Socket disconnected. Automatic reconnect is $autoReconnect");
if (autoReconnect) { if (autoReconnect) {
_reconnect(); _reconnect();
} }
} }
void _handleSocketError(e) { void _handleSocketError(e) {
TheLogger.error("Socket stream Error: $e"); Logger.e("Socket stream Error: $e");
TheLogger.debug("Automatic reconnect is $autoReconnect"); Logger.d("Automatic reconnect is $autoReconnect");
if (autoReconnect) { if (autoReconnect) {
_reconnect(); _reconnect();
} else { } else {
@ -169,6 +161,7 @@ class HomeAssistant {
futures.add(_getConfig()); futures.add(_getConfig());
futures.add(_getServices()); futures.add(_getServices());
futures.add(_getUserInfo()); futures.add(_getUserInfo());
futures.add(_getPanels());
try { try {
await Future.wait(futures); await Future.wait(futures);
_createUI(); _createUI();
@ -186,7 +179,7 @@ class HomeAssistant {
_fetchCompleter.completeError(error); _fetchCompleter.completeError(error);
} else { } else {
autoReconnect = true; autoReconnect = true;
TheLogger.debug( "Fetch complete successful"); Logger.d( "Fetch complete successful");
_fetchCompleter.complete(); _fetchCompleter.complete();
} }
} }
@ -213,109 +206,104 @@ class HomeAssistant {
_handleMessage(String message) { _handleMessage(String message) {
var data = json.decode(message); var data = json.decode(message);
if (data["type"] == "auth_required") { if (data["type"] == "auth_required") {
_sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}'); _sendAuthMessage('{"type": "auth","access_token": "$_password"}');
} else if (data["type"] == "auth_ok") { } else if (data["type"] == "auth_ok") {
_completeConnecting(null); _completeConnecting(null);
_sendSubscribe(); _sendSubscribe();
} else if (data["type"] == "auth_invalid") { } else if (data["type"] == "auth_invalid") {
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"}); _completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") { } else if (data["type"] == "result") {
TheLogger.debug("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}"); Logger.d("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
if (data["id"] == _configMessageId) { _messageResolver[data["id"]]?.complete(data);
_parseConfig(data); _messageResolver.remove(data["id"]);
} else if (data["id"] == _statesMessageId) {
_parseEntities(data);
} else if (data["id"] == _lovelaceMessageId) {
_handleLovelace(data);
} else if (data["id"] == _servicesMessageId) {
_parseServices(data);
} else if (data["id"] == _userInfoMessageId) {
_parseUserInfo(data);
}
} else if (data["type"] == "event") { } else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) { if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
TheLogger.debug("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}"); Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
_handleEntityStateChange(data["event"]["data"]); _handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) { } else if (data["event"] != null) {
TheLogger.warning("Unhandled event type: ${data["event"]["event_type"]}"); Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else { } else {
TheLogger.error("Event is null: $message"); Logger.e("Event is null: $message");
} }
} else { } else {
TheLogger.warning("Unknown message type: $message"); Logger.w("Unknown message type: $message");
} }
} }
void _sendSubscribe() { void _sendSubscribe() {
_incrementMessageId(); _incrementMessageId();
_subscriptionMessageId = _currentMessageId; _subscriptionMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false); _send('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
} }
Future _getConfig() { Future _getConfig() async {
_configCompleter = new Completer(); await _sendInitialMessage("get_config").then((data) => _instanceConfig = Map.from(data["result"]));
_incrementMessageId();
_configMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
return _configCompleter.future;
} }
Future _getStates() { Future _getStates() async {
_statesCompleter = new Completer(); await _sendInitialMessage("get_states").then((data) => entities.parse(data["result"]));
_incrementMessageId();
_statesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
return _statesCompleter.future;
} }
Future _getLovelace() { Future _getLovelace() async {
_lovelaceCompleter = new Completer(); await _sendInitialMessage("lovelace/config").then((data) => _rawLovelaceData = data["result"]);
_incrementMessageId();
_lovelaceMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false);
return _lovelaceCompleter.future;
} }
Future _getUserInfo() { Future _getUserInfo() async {
_userInfoCompleter = new Completer(); _userName = null;
_incrementMessageId(); await _sendInitialMessage("auth/current_user").then((data) => _userName = data["result"]["name"]);
_userInfoMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
return _userInfoCompleter.future;
} }
Future _getServices() { Future _getServices() async {
_servicesCompleter = new Completer(); await _sendInitialMessage("get_services").then((data) => Logger.d("We actually don`t need the list of servcies for now"));
_incrementMessageId(); }
_servicesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
return _servicesCompleter.future; Future _getPanels() async {
panels.clear();
await _sendInitialMessage("get_panels").then((data) {
if (data["success"]) {
data["result"].forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
panels.add(Panel(
id: k,
type: v["component_name"],
title: title,
urlPath: v["url_path"],
config: v["config"],
icon: v["icon"]
)
);
});
}
});
} }
_incrementMessageId() { _incrementMessageId() {
_currentMessageId += 1; _currentMessageId += 1;
} }
void _sendAuthMessageRaw(String message) { void _sendAuthMessage(String message) {
TheLogger.debug( "[Sending] ==> auth request"); Logger.d( "[Sending] ==> auth request");
_hassioChannel.sink.add(message); _hassioChannel.sink.add(message);
} }
_sendMessageRaw(String message, bool queued) { Future _sendInitialMessage(String type) {
Completer _completer = Completer();
_incrementMessageId();
_messageResolver[_currentMessageId] = _completer;
_send('{"id": $_currentMessageId, "type": "$type"}', false);
return _completer.future;
}
_send(String message, bool queued) {
var sendCompleter = Completer(); var sendCompleter = Completer();
if (queued) _messageQueue.add(message); if (queued) _messageQueue.add(message);
_connection().then((r) { _connection().then((r) {
_messageQueue.getActualMessages().forEach((message){ _messageQueue.getActualMessages().forEach((message){
TheLogger.debug( "[Sending queued] ==> $message"); Logger.d( "[Sending queued] ==> $message");
_hassioChannel.sink.add(message); _hassioChannel.sink.add(message);
}); });
if (!queued) { if (!queued) {
TheLogger.debug( "[Sending] ==> $message"); Logger.d( "[Sending] ==> $message");
_hassioChannel.sink.add(message); _hassioChannel.sink.add(message);
} }
sendCompleter.complete(); sendCompleter.complete();
@ -361,62 +349,41 @@ class HomeAssistant {
} }
message += '}'; message += '}';
} }
return _sendMessageRaw(message, true); return _send(message, true);
} }
void _handleEntityStateChange(Map eventData) { void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}"); //TheLogger.debug( "New state for ${eventData['entity_id']}");
Map data = Map.from(eventData); Map data = Map.from(eventData);
entities.updateState(data); eventBus.fire(new StateChangedEvent(
eventBus.fire(new StateChangedEvent(data["entity_id"], null)); entityId: data["entity_id"],
} needToRebuildUI: entities.updateState(data)
));
void _parseConfig(Map data) {
if (data["success"] == true) {
_instanceConfig = Map.from(data["result"]);
_configCompleter.complete();
} else {
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
}
}
void _parseUserInfo(Map data) {
if (data["success"] == true) {
_userName = data["result"]["name"];
} else {
_userName = null;
TheLogger.warning("There was an error getting current user: $data");
}
_userInfoCompleter.complete();
}
void _parseServices(response) {
_servicesCompleter.complete();
}
void _handleLovelace(response) {
if (response["success"] == true) {
_rawLovelaceData = response["result"];
} else {
TheLogger.error("There was an error getting Lovelace config: $response");
_rawLovelaceData = null;
}
_lovelaceCompleter.complete();
} }
void _parseLovelace() { void _parseLovelace() {
TheLogger.debug("--Title: ${_rawLovelaceData["title"]}"); Logger.d("--Title: ${_rawLovelaceData["title"]}");
ui.title = _rawLovelaceData["title"]; ui.title = _rawLovelaceData["title"];
int viewCounter = 0; int viewCounter = 0;
TheLogger.debug("--Views count: ${_rawLovelaceData['views'].length}"); Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
_rawLovelaceData["views"].forEach((rawView){ _rawLovelaceData["views"].forEach((rawView){
TheLogger.debug("----view id: ${rawView['id']}"); Logger.d("----view id: ${rawView['id']}");
HAView view = HAView( HAView view = HAView(
count: viewCounter, count: viewCounter,
id: "${rawView['id']}", id: "${rawView['id']}",
name: rawView['title'], name: rawView['title'],
iconName: rawView['icon'] iconName: rawView['icon']
); );
if (rawView['badges'] != null && rawView['badges'] is List) {
rawView['badges'].forEach((entity) {
if (entities.isExist(entity)) {
Entity e = entities.get(entity);
view.badges.add(e);
}
});
}
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? [])); view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
ui.views.add( ui.views.add(
view view
@ -428,42 +395,91 @@ class HomeAssistant {
List<HACard> _createLovelaceCards(List rawCards) { List<HACard> _createLovelaceCards(List rawCards) {
List<HACard> result = []; List<HACard> result = [];
rawCards.forEach((rawCard){ rawCards.forEach((rawCard){
if (rawCard["cards"] != null) { try {
result.addAll(_createLovelaceCards(rawCard["cards"])); bool isThereCardOptionsInside = rawCard["card"] != null;
} else {
HACard card = HACard( HACard card = HACard(
id: "card", id: "card",
name: rawCard["title"], name: isThereCardOptionsInside ? rawCard["card"]["title"] ??
type: rawCard['type'], rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
columnsCount: rawCard['columns'] ?? 4, type: isThereCardOptionsInside
showName: rawCard['show_name'] ?? true, ? rawCard["card"]['type']
showState: rawCard['show_state'] ?? true, : 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'] ?? [],
states: rawCard['states'],
content: rawCard['content']
); );
if (rawCard["cards"] != null) {
card.childCards = _createLovelaceCards(rawCard["cards"]);
}
rawCard["entities"]?.forEach((rawEntity) { rawCard["entities"]?.forEach((rawEntity) {
if (rawEntity is String) { if (rawEntity is String) {
if (entities.isExist(rawEntity)) { if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity))); card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
} }
} else { } else {
if (entities.isExist(rawEntity["entity"])) { if (rawEntity["type"] == "divider") {
card.entities.add(EntityWrapper(entity: Entity.divider()));
} else if (rawEntity["type"] == "section") {
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
} else if (rawEntity["type"] == "call-service") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.callService,
"service": rawEntity["service"],
"service_data": rawEntity["service_data"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (rawEntity["type"] == "weblink") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.navigate,
"service": rawEntity["url"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
url: rawEntity["url"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (entities.isExist(rawEntity["entity"])) {
Entity e = entities.get(rawEntity["entity"]); Entity e = entities.get(rawEntity["entity"]);
String tapAction = EntityTapAction.moreInfo;
String holdAction = EntityTapAction.none;
if (card.type == CardType.glance) {
tapAction = rawEntity["tap_action"] ?? EntityTapAction.moreInfo;
holdAction = rawEntity["hold_action"] ?? EntityTapAction.none;
}
card.entities.add( card.entities.add(
EntityWrapper( EntityWrapper(
entity: e, entity: e,
displayName: rawEntity["name"], displayName: rawEntity["name"],
icon: rawEntity["icon"], icon: rawEntity["icon"],
tapAction: tapAction, uiAction: EntityUIAction(rawEntityData: rawEntity)
holdAction: holdAction,
tapActionService: rawEntity["service"],
tapActionServiceData: rawEntity["service_data"] ?? {"entity_id": e.entityId}
) )
); );
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
} }
} }
}); });
@ -471,41 +487,45 @@ class HomeAssistant {
var en = rawCard["entity"]; var en = rawCard["entity"];
if (en is String) { if (en is String) {
if (entities.isExist(en)) { if (entities.isExist(en)) {
card.linkedEntity = EntityWrapper(entity: entities.get(en)); Entity e = entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: rawCard["icon"],
displayName: rawCard["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
} }
} else { } else {
if (entities.isExist(en["entity"])) { if (entities.isExist(en["entity"])) {
card.linkedEntity = EntityWrapper( Entity e = entities.get(en["entity"]);
entity: entities.get(en["entity"]), card.linkedEntityWrapper = EntityWrapper(
icon: en["icon"], entity: e,
displayName: en["name"] icon: en["icon"],
displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
); );
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
} }
} }
} }
result.add(card); result.add(card);
} catch (e) {
Logger.e("There was an error parsing card: ${e.toString()}");
} }
}); });
return result; return result;
} }
void _parseEntities(response) async {
if (response["success"] == false) {
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
return;
}
entities.parse(response["result"]);
_statesCompleter.complete();
}
void _createUI() { void _createUI() {
ui = HomeAssistantUI(); ui = HomeAssistantUI();
if ((_useLovelace) && (_rawLovelaceData != null)) { if ((_useLovelace) && (_rawLovelaceData != null)) {
TheLogger.debug("Creating Lovelace UI"); Logger.d("Creating Lovelace UI");
_parseLovelace(); _parseLovelace();
} else { } else {
TheLogger.debug("Creating group-based UI"); Logger.d("Creating group-based UI");
int viewCounter = 0; int viewCounter = 0;
if (!entities.hasDefaultView) { if (!entities.hasDefaultView) {
HAView view = HAView( HAView view = HAView(
@ -535,8 +555,8 @@ class HomeAssistant {
} }
} }
Widget buildViews(BuildContext context, bool lovelace) { Widget buildViews(BuildContext context, bool lovelace, TabController tabController) {
return ui.build(context); return ui.build(context, tabController);
} }
Future<List> getHistory(String entityId) async { Future<List> getHistory(String entityId) async {
@ -544,22 +564,15 @@ class HomeAssistant {
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]); //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 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"; String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
TheLogger.debug("[Sending] ==> $url"); Logger.d("[Sending] ==> $url");
http.Response historyResponse; http.Response historyResponse;
if (_authType == "access_token") { historyResponse = await http.get(url, headers: {
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_password", "authorization": "Bearer $_password",
"Content-Type": "application/json" "Content-Type": "application/json"
}); });
} else {
historyResponse = await http.get(url, headers: {
"X-HA-Access": "$_password",
"Content-Type": "application/json"
});
}
var history = json.decode(historyResponse.body); var history = json.decode(historyResponse.body);
if (history is List) { if (history is List) {
TheLogger.debug( "[Received] <== ${history.first.length} history recors"); Logger.d( "[Received] <== ${history.first.length} history recors");
return history; return history;
} else { } else {
return []; return [];

View File

@ -19,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> {
} }
_loadLog() async { _loadLog() async {
_logData = TheLogger.getLog(); _logData = Logger.getLog();
} }
@override @override

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -7,17 +8,20 @@ import 'package:web_socket_channel/io.dart';
import 'package:event_bus/event_bus.dart'; import 'package:event_bus/event_bus.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart' as urlLauncher;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:date_format/date_format.dart'; import 'package:date_format/date_format.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter_colorpicker/material_picker.dart';
import 'package:charts_flutter/flutter.dart' as charts; import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart'; import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
part 'entity_class/const.dart'; part 'entity_class/const.dart';
part 'entity_class/entity.class.dart'; part 'entity_class/entity.class.dart';
part 'entity_class/entity_wrapper.class.dart'; part 'entity_class/entity_wrapper.class.dart';
part 'entity_class/timer_entity.dart';
part 'entity_class/switch_entity.class.dart'; part 'entity_class/switch_entity.class.dart';
part 'entity_class/button_entity.class.dart'; part 'entity_class/button_entity.class.dart';
part 'entity_class/text_entity.class.dart'; part 'entity_class/text_entity.class.dart';
@ -32,10 +36,15 @@ part 'entity_class/media_player_entity.class.dart';
part 'entity_class/lock_entity.class.dart'; part 'entity_class/lock_entity.class.dart';
part 'entity_class/group_entity.class.dart'; part 'entity_class/group_entity.class.dart';
part 'entity_class/fan_entity.class.dart'; part 'entity_class/fan_entity.class.dart';
part 'entity_class/automation_entity.dart';
part 'entity_class/camera_entity.class.dart';
part 'entity_class/alarm_control_panel.class.dart';
part 'entity_widgets/common/badge.dart'; part 'entity_widgets/common/badge.dart';
part 'entity_widgets/model_widgets.dart'; part 'entity_widgets/model_widgets.dart';
part 'entity_widgets/default_entity_container.dart'; part 'entity_widgets/default_entity_container.dart';
part 'entity_widgets/missed_entity.dart';
part 'entity_widgets/glance_entity_container.dart'; part 'entity_widgets/glance_entity_container.dart';
part 'entity_widgets/button_entity_container.dart';
part 'entity_widgets/common/entity_attributes_list.dart'; part 'entity_widgets/common/entity_attributes_list.dart';
part 'entity_widgets/entity_icon.dart'; part 'entity_widgets/entity_icon.dart';
part 'entity_widgets/entity_name.dart'; part 'entity_widgets/entity_name.dart';
@ -43,6 +52,9 @@ part 'entity_widgets/common/last_updated.dart';
part 'entity_widgets/common/mode_swicth.dart'; part 'entity_widgets/common/mode_swicth.dart';
part 'entity_widgets/common/mode_selector.dart'; part 'entity_widgets/common/mode_selector.dart';
part 'entity_widgets/common/universal_slider.dart'; part 'entity_widgets/common/universal_slider.dart';
part 'entity_widgets/common/flat_service_button.dart';
part 'entity_widgets/common/light_color_picker.dart';
part 'entity_widgets/common/camera_stream_view.dart';
part 'entity_widgets/entity_colors.class.dart'; part 'entity_widgets/entity_colors.class.dart';
part 'entity_widgets/entity_page_container.dart'; part 'entity_widgets/entity_page_container.dart';
part 'entity_widgets/history_chart/entity_history.dart'; part 'entity_widgets/history_chart/entity_history.dart';
@ -56,17 +68,19 @@ part 'entity_widgets/controls/slider_controls.dart';
part 'entity_widgets/state/text_input_state.dart'; part 'entity_widgets/state/text_input_state.dart';
part 'entity_widgets/state/select_state.dart'; part 'entity_widgets/state/select_state.dart';
part 'entity_widgets/state/simple_state.dart'; part 'entity_widgets/state/simple_state.dart';
part 'entity_widgets/state/timer_state.dart';
part 'entity_widgets/state/climate_state.dart'; part 'entity_widgets/state/climate_state.dart';
part 'entity_widgets/state/cover_state.dart'; part 'entity_widgets/state/cover_state.dart';
part 'entity_widgets/state/date_time_state.dart'; part 'entity_widgets/state/date_time_state.dart';
part 'entity_widgets/state/button_state.dart';
part 'entity_widgets/state/lock_state.dart'; part 'entity_widgets/state/lock_state.dart';
part 'entity_widgets/controls/climate_controls.dart'; part 'entity_widgets/controls/climate_controls.dart';
part 'entity_widgets/controls/cover_controls.dart'; part 'entity_widgets/controls/cover_controls.dart';
part 'entity_widgets/controls/light_controls.dart'; part 'entity_widgets/controls/light_controls.dart';
part 'entity_widgets/controls/media_player_widgets.dart'; part 'entity_widgets/controls/media_player_widgets.dart';
part 'entity_widgets/controls/fan_controls.dart'; part 'entity_widgets/controls/fan_controls.dart';
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
part 'settings.page.dart'; part 'settings.page.dart';
part 'panel.page.dart';
part 'home_assistant.class.dart'; part 'home_assistant.class.dart';
part 'log.page.dart'; part 'log.page.dart';
part 'entity.page.dart'; part 'entity.page.dart';
@ -77,24 +91,23 @@ part 'ui_class/ui.dart';
part 'ui_class/view.class.dart'; part 'ui_class/view.class.dart';
part 'ui_class/card.class.dart'; part 'ui_class/card.class.dart';
part 'ui_class/sizes_class.dart'; part 'ui_class/sizes_class.dart';
part 'ui_class/panel_class.dart';
part 'ui_widgets/view.dart'; part 'ui_widgets/view.dart';
part 'ui_widgets/entities_card.dart'; part 'ui_widgets/card_widget.dart';
part 'ui_widgets/glance_card.dart';
part 'ui_widgets/unsupported_card.dart';
part 'ui_widgets/media_control_card.dart';
part 'ui_widgets/card_header_widget.dart'; part 'ui_widgets/card_header_widget.dart';
part 'ui_widgets/config_panel_widget.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
const String appName = "HA Client"; const String appName = "HA Client";
const appVersion = "0.3.10-73"; const appVersion = "0.5.0";
String homeAssistantWebHost; String homeAssistantWebHost;
void main() { void main() {
FlutterError.onError = (errorDetails) { FlutterError.onError = (errorDetails) {
TheLogger.error( "${errorDetails.exception}"); Logger.e( "${errorDetails.exception}");
if (TheLogger.isInDebugMode) { if (Logger.isInDebugMode) {
FlutterError.dumpErrorToConsole(errorDetails); FlutterError.dumpErrorToConsole(errorDetails);
} }
}; };
@ -102,9 +115,9 @@ void main() {
runZoned(() { runZoned(() {
runApp(new HAClientApp()); runApp(new HAClientApp());
}, onError: (error, stack) { }, onError: (error, stack) {
TheLogger.error("$error"); Logger.e("$error");
TheLogger.error("$stack"); Logger.e("$stack");
if (TheLogger.isInDebugMode) { if (Logger.isInDebugMode) {
debugPrint("$stack"); debugPrint("$stack");
} }
}); });
@ -123,6 +136,7 @@ class HAClientApp extends StatelessWidget {
routes: { routes: {
"/": (context) => MainPage(title: 'HA Client'), "/": (context) => MainPage(title: 'HA Client'),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/configuration": (context) => PanelPage(title: "Configuration"),
"/log-view": (context) => LogViewPage(title: "Log") "/log-view": (context) => LogViewPage(title: "Log")
}, },
); );
@ -138,23 +152,22 @@ class MainPage extends StatefulWidget {
_MainPageState createState() => new _MainPageState(); _MainPageState createState() => new _MainPageState();
} }
class _MainPageState extends State<MainPage> with WidgetsBindingObserver { class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
HomeAssistant _homeAssistant; HomeAssistant _homeAssistant;
//Map _instanceConfig; //Map _instanceConfig;
String _webSocketApiEndpoint; String _webSocketApiEndpoint;
String _password; String _password;
String _authType;
//int _uiViewsCount = 0; //int _uiViewsCount = 0;
String _instanceHost; String _instanceHost;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
StreamSubscription _settingsSubscription; StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription; StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription; StreamSubscription _showEntityPageSubscription;
StreamSubscription _refreshDataSubscription;
StreamSubscription _showErrorSubscription; StreamSubscription _showErrorSubscription;
bool _settingsLoaded = false; bool _settingsLoaded = false;
bool _accountMenuExpanded = false; bool _accountMenuExpanded = false;
bool _useLovelaceUI; bool _useLovelaceUI;
int _previousViewCount;
@override @override
void initState() { void initState() {
@ -162,11 +175,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_settingsLoaded = false; _settingsLoaded = false;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
TheLogger.debug("<!!!> Creating new HomeAssistant instance"); Logger.d("<!!!> Creating new HomeAssistant instance");
_homeAssistant = HomeAssistant(); _homeAssistant = HomeAssistant();
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
TheLogger.debug("Settings change event: reconnect=${event.reconnect}"); Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) { if (event.reconnect) {
_homeAssistant.disconnect().then((_){ _homeAssistant.disconnect().then((_){
_initialLoad(); _initialLoad();
@ -187,7 +200,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
TheLogger.debug("$state"); Logger.d("$state");
if (state == AppLifecycleState.resumed && _settingsLoaded) { if (state == AppLifecycleState.resumed && _settingsLoaded) {
_refreshData(); _refreshData();
} }
@ -201,8 +214,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; _webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
_password = prefs.getString('hassio-password'); _password = prefs.getString('hassio-password');
_authType = prefs.getString('hassio-auth-type'); _useLovelaceUI = prefs.getBool('use-lovelace') ?? true;
_useLovelaceUI = prefs.getBool('use-lovelace') ?? false;
if ((domain == null) || (port == null) || (_password == null) || if ((domain == null) || (port == null) || (_password == null) ||
(domain.length == 0) || (port.length == 0) || (_password.length == 0)) { (domain.length == 0) || (port.length == 0) || (_password.length == 0)) {
throw("Check connection settings"); throw("Check connection settings");
@ -214,7 +226,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_subscribe() { _subscribe() {
if (_stateSubscription == null) { if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
setState(() {}); if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI");
_refreshData();
} else {
setState(() {});
}
}); });
} }
if (_serviceCallSubscription == null) { if (_serviceCallSubscription == null) {
@ -232,12 +249,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}); });
} }
if (_refreshDataSubscription == null) {
_refreshDataSubscription = eventBus.on<RefreshDataEvent>().listen((event){
_refreshData();
});
}
if (_showErrorSubscription == null) { if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){ _showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorBottomBar(message: event.text, errorCode: event.errorCode); _showErrorBottomBar(message: event.text, errorCode: event.errorCode);
@ -246,11 +257,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
} }
_refreshData() async { _refreshData() async {
_homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _authType, _useLovelaceUI); _homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI);
_hideBottomBar(); _hideBottomBar();
_showInfoBottomBar(progress: true,); _showInfoBottomBar(progress: true,);
await _homeAssistant.fetch().then((result) { await _homeAssistant.fetch().then((result) {
_hideBottomBar(); _hideBottomBar();
int currentViewCount = _homeAssistant.ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
_viewsTabController = TabController(vsync: this, length: currentViewCount);
_previousViewCount = currentViewCount;
}
}).catchError((e) { }).catchError((e) {
_setErrorState(e); _setErrorState(e);
}); });
@ -259,8 +276,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_setErrorState(e) { _setErrorState(e) {
if (e is Error) { if (e is Error) {
TheLogger.error(e.toString()); Logger.e(e.toString());
TheLogger.error("${e.stackTrace}"); Logger.e("${e.stackTrace}");
_showErrorBottomBar( _showErrorBottomBar(
message: "There was some error", message: "There was some error",
errorCode: 13 errorCode: 13
@ -336,6 +353,27 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
Divider(), Divider(),
]); ]);
} else { } else {
if (_homeAssistant != null && _homeAssistant.panels.isNotEmpty) {
_homeAssistant.panels.forEach((Panel panel) {
if (!panel.isHidden) {
menuItems.add(
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
title: Text("${panel.title}"),
onTap: () => panel.handleOpen(context)
)
);
}
});
menuItems.addAll([
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
title: Text("Open Web UI"),
onTap: () => HAUtils.launchURL(homeAssistantWebHost),
),
Divider()
]);
}
menuItems.addAll([ menuItems.addAll([
new ListTile( new ListTile(
leading: Icon(Icons.insert_drive_file), leading: Icon(Icons.insert_drive_file),
@ -346,7 +384,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}, },
), ),
new ListTile( new ListTile(
leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")), leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
title: Text("Report an issue"), title: Text("Report an issue"),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -359,10 +397,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
HAUtils.launchURL("http://www.keyboardcrumbs.io/"); HAUtils.launchURL("http://ha-client.homemade.systems/");
}, },
child: Text( child: Text(
"www.keyboardcrumbs.io", "ha-client.homemade.systems",
style: TextStyle( style: TextStyle(
color: Colors.blue, color: Colors.blue,
decoration: TextDecoration.underline decoration: TextDecoration.underline
@ -371,8 +409,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
) )
], ],
applicationName: appName, applicationName: appName,
applicationVersion: appVersion, applicationVersion: appVersion
applicationLegalese: "Keyboard Crumbs",
) )
]); ]);
} }
@ -516,6 +553,26 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
pinned: true, pinned: true,
primary: true, primary: true,
title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""), title: Text(_homeAssistant != null ? _homeAssistant.locationName : ""),
actions: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,),
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
context: context,
items: [PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
)]
).then((String val) {
if (val == "reload") {
_refreshData();
}
});
}
)
],
leading: IconButton( leading: IconButton(
icon: Icon(Icons.menu), icon: Icon(Icons.menu),
onPressed: () { onPressed: () {
@ -526,6 +583,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
}, },
), ),
bottom: empty ? null : TabBar( bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(), tabs: buildUIViewTabs(),
isScrollable: true, isScrollable: true,
), ),
@ -539,7 +597,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant"),
size: 100.0, size: 100.0,
color: Colors.blue, color: Colors.blue,
), ),
@ -547,10 +605,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
), ),
) )
: :
_homeAssistant.buildViews(context, _useLovelaceUI), _homeAssistant.buildViews(context, _useLovelaceUI, _viewsTabController),
); );
} }
TabController _viewsTabController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget bottomBar; Widget bottomBar;
@ -616,9 +676,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
drawer: _buildAppDrawer(), drawer: _buildAppDrawer(),
primary: false, primary: false,
bottomNavigationBar: bottomBar, bottomNavigationBar: bottomBar,
body: DefaultTabController( body: HomeAssistantModel(
length: _homeAssistant.ui?.views?.length ?? 0,
child: _buildScaffoldBody(false), child: _buildScaffoldBody(false),
homeAssistant: _homeAssistant
), ),
); );
} }
@ -627,11 +687,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_viewsTabController.dispose();
if (_stateSubscription != null) _stateSubscription.cancel(); if (_stateSubscription != null) _stateSubscription.cancel();
if (_settingsSubscription != null) _settingsSubscription.cancel(); if (_settingsSubscription != null) _settingsSubscription.cancel();
if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); if (_serviceCallSubscription != null) _serviceCallSubscription.cancel();
if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel();
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
if (_showErrorSubscription != null) _showErrorSubscription.cancel(); if (_showErrorSubscription != null) _showErrorSubscription.cancel();
_homeAssistant.disconnect(); _homeAssistant.disconnect();
super.dispose(); super.dispose();

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,40 @@
part of 'main.dart';
class PanelPage extends StatefulWidget {
PanelPage({Key key, this.title, this.panel}) : super(key: key);
final String title;
final Panel panel;
@override
_PanelPageState createState() => new _PanelPageState();
}
class _PanelPageState extends State<PanelPage> {
List<ConfigurationItem> _items;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text(widget.title),
),
body: widget.panel.getWidget(),
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -18,10 +18,8 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _newHassioPassword = ""; String _newHassioPassword = "";
String _socketProtocol = "wss"; String _socketProtocol = "wss";
String _newSocketProtocol = "wss"; String _newSocketProtocol = "wss";
String _authType = "access_token"; bool _useLovelace = true;
String _newAuthType = "access_token"; bool _newUseLovelace = true;
bool _useLovelace = false;
bool _newUseLovelace = false;
@override @override
void initState() { void initState() {
@ -37,11 +35,10 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? ""; _hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
_authType = _newAuthType = prefs.getString("hassio-auth-type") ?? 'access_token';
try { try {
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? false; _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
} catch (e) { } catch (e) {
_useLovelace = _newUseLovelace = false; _useLovelace = _newUseLovelace = true;
} }
}); });
} }
@ -51,7 +48,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
(_newHassioPort != _hassioPort) || (_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) || (_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) || (_newSocketProtocol != _socketProtocol) ||
(_newAuthType != _authType) ||
(_newUseLovelace != _useLovelace)); (_newUseLovelace != _useLovelace));
} }
@ -66,7 +62,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
prefs.setString("hassio-password", _newHassioPassword); prefs.setString("hassio-password", _newHassioPassword);
prefs.setString("hassio-protocol", _newSocketProtocol); prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setString("hassio-auth-type", _newAuthType);
prefs.setBool("use-lovelace", _newUseLovelace); prefs.setBool("use-lovelace", _newUseLovelace);
} }
@ -83,13 +78,13 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
icon: Icon(Icons.check), icon: Icon(Icons.check),
onPressed: (){ onPressed: (){
if (_checkConfigChanged()) { if (_checkConfigChanged()) {
TheLogger.debug("Settings changed. Saving..."); Logger.d("Settings changed. Saving...");
_saveSettings().then((r) { _saveSettings().then((r) {
Navigator.pop(context); Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true)); eventBus.fire(SettingsChangedEvent(true));
}); });
} else { } else {
TheLogger.debug("Settings was not changed"); Logger.d("Settings was not changed");
Navigator.pop(context); Navigator.pop(context);
} }
} }
@ -150,29 +145,13 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
_newHassioPort = value; _newHassioPort = value;
} }
), ),
new Row( new Text(
mainAxisSize: MainAxisSize.min, "Try ports 80 and 443 if default is not working and you don't know why.",
children: [ style: TextStyle(color: Colors.grey),
Flexible(
child: Text(
"Login with access token (HA >= 0.78.0)",
softWrap: true,
maxLines: 2,
),
),
Switch(
value: (_newAuthType == "access_token"),
onChanged: (value) {
setState(() {
_newAuthType = value ? "access_token" : "api_password";
});
},
)
],
), ),
new TextField( new TextField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: _newAuthType == "access_token" ? "Access token" : "API password" labelText: "Access token"
), ),
controller: new TextEditingController.fromValue( controller: new TextEditingController.fromValue(
new TextEditingValue( new TextEditingValue(

View File

@ -2,76 +2,49 @@ part of '../main.dart';
class HACard { class HACard {
List<EntityWrapper> entities = []; List<EntityWrapper> entities = [];
EntityWrapper linkedEntity; List<HACard> childCards = [];
EntityWrapper linkedEntityWrapper;
String name; String name;
String id; String id;
String type; String type;
bool showName; bool showName;
bool showState; bool showState;
bool showEmpty;
int columnsCount; int columnsCount;
List stateFilter;
List states;
String content;
HACard({ HACard({
this.name, this.name,
this.id, this.id,
this.linkedEntity, this.linkedEntityWrapper,
this.columnsCount: 4, this.columnsCount: 4,
this.showName: true, this.showName: true,
this.showState: true, this.showState: true,
this.stateFilter: const [],
this.showEmpty: true,
this.content,
this.states,
@required this.type @required this.type
}); });
Widget build(BuildContext context) { List<EntityWrapper> getEntitiesToShow() {
switch (type) { return entities.where((entityWrapper) {
if (entityWrapper.entity.isHidden) {
case CardType.entities: { return false;
return EntitiesCardWidget(
card: this,
);
}
case CardType.glance: {
return GlanceCardWidget(
card: this,
);
}
case CardType.mediaControl: {
return MediaControlCardWidget(
card: this,
);
}
case CardType.weatherForecast:
case CardType.thermostat:
case CardType.sensor:
case CardType.plantStatus:
case CardType.pictureEntity:
case CardType.pictureElements:
case CardType.picture:
case CardType.map:
case CardType.iframe:
case CardType.gauge:
case CardType.entityButton:
case CardType.conditional:
case CardType.alarmPanel: {
return UnsupportedCardWidget(
card: this,
);
}
default: {
if ((linkedEntity == null) && (entities.isNotEmpty)) {
return EntitiesCardWidget(
card: this,
);
} else {
return UnsupportedCardWidget(
card: this,
);
}
}
} }
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,57 @@
part of '../main.dart';
class Panel {
static const iconsByComponent = {
"config": "mdi:settings",
"history": "mdi:poll-box",
"map": "mdi:tooltip-account",
"logbook": "mdi:format-list-bulleted-type",
"custom": "mdi:home-assistant"
};
final String id;
final String type;
final String title;
final String urlPath;
final Map config;
String icon;
bool isHidden = true;
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
if (icon == null || !icon.startsWith("mdi:")) {
icon = Panel.iconsByComponent[type];
}
isHidden = (type != "iframe" && type != "config");
}
void handleOpen(BuildContext context) {
if (type == "iframe") {
Logger.d("Launching custom tab with ${config["url"]}");
HAUtils.launchURLInCustomTab(context, config["url"]);
} else if (type == "config") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this),
)
);
} else {
String url = "$homeAssistantWebHost/$urlPath";
Logger.d("Launching custom tab with $url");
HAUtils.launchURLInCustomTab(context, url);
}
}
Widget getWidget() {
switch (type) {
case "config": {
return ConfigPanelWidget();
}
default: {
return Text("Unsupported panel component: $type");
}
}
}
}

View File

@ -1,11 +1,12 @@
part of '../main.dart'; part of '../main.dart';
class Sizes { class Sizes {
static const rightWidgetPadding = 14.0; static const rightWidgetPadding = 16.0;
static const leftWidgetPadding = 8.0; static const leftWidgetPadding = 16.0;
static const buttonPadding = 4.0;
static const extendedWidgetHeight = 50.0; static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0; static const iconSize = 28.0;
static const largeIconSize = 34.0; static const largeIconSize = 46.0;
static const stateFontSize = 15.0; static const stateFontSize = 15.0;
static const nameFontSize = 15.0; static const nameFontSize = 15.0;
static const smallFontSize = 14.0; static const smallFontSize = 14.0;

View File

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

View File

@ -28,8 +28,8 @@ class HAView {
HACard card = HACard( HACard card = HACard(
name: e.displayName, name: e.displayName,
id: e.entityId, id: e.entityId,
linkedEntity: EntityWrapper(entity: e), linkedEntityWrapper: EntityWrapper(entity: e),
type: "media-control" type: CardType.mediaControl
); );
cards.add(card); cards.add(card);
}); });
@ -40,7 +40,7 @@ class HAView {
HACard card = HACard( HACard card = HACard(
id: groupIdToAdd, id: groupIdToAdd,
name: entity.domain, name: entity.domain,
type: "entities" type: CardType.entities
); );
card.entities.add(EntityWrapper(entity: entity)); card.entities.add(EntityWrapper(entity: entity));
autoGeneratedCards.add(card); autoGeneratedCards.add(card);
@ -51,16 +51,16 @@ class HAView {
HACard card = HACard( HACard card = HACard(
name: entity.displayName, name: entity.displayName,
id: entity.entityId, id: entity.entityId,
linkedEntity: EntityWrapper(entity: entity), linkedEntityWrapper: EntityWrapper(entity: entity),
type: "entities" type: CardType.entities
); );
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);})); 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){ entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
HACard mediaCard = HACard( HACard mediaCard = HACard(
name: entity.displayName, name: entity.displayName,
id: entity.entityId, id: entity.entityId,
linkedEntity: EntityWrapper(entity: entity), linkedEntityWrapper: EntityWrapper(entity: entity),
type: "media-control" type: CardType.mediaControl
); );
cards.add(mediaCard); cards.add(mediaCard);
}); });
@ -77,7 +77,7 @@ class HAView {
Tab( Tab(
icon: icon:
Icon( Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
iconName ?? "mdi:home-assistant"), iconName ?? "mdi:home-assistant"),
size: 24.0, size: 24.0,
) )
@ -92,7 +92,7 @@ class HAView {
if (linkedEntity.icon != null && linkedEntity.icon.length > 0) { if (linkedEntity.icon != null && linkedEntity.icon.length > 0) {
return Tab( return Tab(
icon: Icon( icon: Icon(
MaterialDesignIcons.createIconDataFromIconName( MaterialDesignIcons.getIconDataFromIconName(
linkedEntity.icon), linkedEntity.icon),
size: 24.0, size: 24.0,
) )

View File

@ -3,14 +3,18 @@ part of '../main.dart';
class CardHeaderWidget extends StatelessWidget { class CardHeaderWidget extends StatelessWidget {
final String name; final String name;
final Widget trailing;
final Widget subtitle;
const CardHeaderWidget({Key key, this.name}) : super(key: key); const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var result; var result;
if ((name != null) && (name.trim().length > 0)) { if ((name != null) && (name.trim().length > 0)) {
result = new ListTile( result = new ListTile(
trailing: trailing,
subtitle: subtitle,
title: Text("$name", title: Text("$name",
textAlign: TextAlign.left, textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@ -0,0 +1,289 @@
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) {
if (card.linkedEntityWrapper.entity.isHidden) {
return Container(width: 0.0, height: 0.0,);
}
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: MissedEntityWidget(),
handleTap: false,
);
}
}
switch (card.type) {
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.markdown: {
return _buildMarkdownCard(context);
}
case CardType.alarmPanel: {
return _buildAlarmPanelCard(context);
}
case CardType.horizontalStack: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
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(10.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
child: entity.entity.buildDefaultWidget(context)
),
));
}
});
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: body)
);
}
Widget _buildMarkdownCard(BuildContext context) {
if (card.content == null) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name));
body.add(MarkdownBody(data: card.content));
return Card(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildAlarmPanelCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeaderWidget(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle(
color: Colors.grey
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
EntityIcon(
size: 50.0,
),
Container(
width: 26.0,
child: IconButton(
padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
)
)
]
),
));
body.add(
AlarmControlPanelControlsWidget(
extended: true,
states: card.states,
)
);
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
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) {
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: MediaPlayerWidget()
)
);
}
Widget _buildEntityButtonCard(BuildContext context) {
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

@ -0,0 +1,95 @@
part of '../main.dart';
class ConfigPanelWidget extends StatefulWidget {
ConfigPanelWidget({Key key}) : super(key: key);
@override
_ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState();
}
class ConfigurationItem {
ConfigurationItem({ this.isExpanded: false, this.header, this.body });
bool isExpanded;
final String header;
final Widget body;
}
class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
List<ConfigurationItem> _items;
@override
void initState() {
super.initState();
_items = <ConfigurationItem>[
ConfigurationItem(
header: 'General',
body: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)),
Container(height: Sizes.rowPadding,),
Text("Control your Home Assistant server from HA Client."),
Divider(),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatServiceButton(
text: "Restart",
serviceName: "restart",
serviceDomain: "homeassistant",
entityId: null,
),
FlatServiceButton(
text: "Stop",
serviceName: "stop",
serviceDomain: "homeassistant",
entityId: null,
),
],
)
],
),
)
)
];
}
@override
Widget build(BuildContext context) {
return ListView(
children: [
new ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_items[index].isExpanded = !_items[index].isExpanded;
});
},
children: _items.map((ConfigurationItem item) {
return new ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return CardHeaderWidget(
name: item.header,
);
},
isExpanded: item.isExpanded,
body: new Container(
child: item.body,
),
);
}).toList(),
),
],
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -1,43 +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.entity.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((EntityWrapper entity) {
if (!entity.entity.isHidden) {
result.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 result;
}
}

View File

@ -1,52 +0,0 @@
part of '../main.dart';
class GlanceCardWidget extends StatelessWidget {
final HACard card;
const GlanceCardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if ((card.linkedEntity!= null) && (card.linkedEntity.entity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> rows = [];
rows.add(CardHeaderWidget(name: card.name));
rows.add(_buildRows(context));
return Card(
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
);
}
Widget _buildRows(BuildContext context) {
List<Widget> result = [];
List<EntityWrapper> toShow = card.entities.where((entity) {return !entity.entity.isHidden;}).toList();
int columnsCount = toShow.length >= card.columnsCount ? card.columnsCount : toShow.length;
toShow.forEach((EntityWrapper entity) {
result.add(
FractionallySizedBox(
widthFactor: 1/columnsCount,
child: EntityModel(
entityWrapper: entity,
child: entity.entity.buildGlanceWidget(context, card.showName, card.showState),
handleTap: true
),
)
);
});
return Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
child: Wrap(
//alignment: WrapAlignment.spaceAround,
runSpacing: Sizes.rowPadding*2,
children: result,
),
);
}
}

View File

@ -1,29 +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.entity.isHidden)) {
return Container(width: 0.0, height: 0.0,);
}
return Card(
child: EntityModel(
entityWrapper: card.linkedEntity,
handleTap: null,
child: MediaPlayerWidget()
)
);
}
}

View File

@ -1,53 +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.entity.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, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: EntityModel(
entityWrapper: card.linkedEntity,
handleTap: true,
child: card.linkedEntity.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"),
),
]);
}
return result;
}
}

View File

@ -17,29 +17,17 @@ class ViewWidget extends StatefulWidget {
class ViewWidgetState extends State<ViewWidget> { class ViewWidgetState extends State<ViewWidget> {
StreamSubscription _refreshDataSubscription;
Completer _refreshCompleter;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
_refreshCompleter.complete();
}
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshIndicator( return ListView(
color: Colors.amber, padding: EdgeInsets.all(0.0),
child: ListView( //physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.all(0.0), children: _buildChildren(context),
physics: const AlwaysScrollableScrollPhysics(),
children: _buildChildren(context),
),
onRefresh: () => _refreshData(),
); );
} }
@ -57,12 +45,22 @@ class ViewWidgetState extends State<ViewWidget> {
); );
} }
List<Widget> cards = [];
widget.view.cards.forEach((HACard card){ widget.view.cards.forEach((HACard card){
result.add( cards.add(
card.build(context) ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: card.build(context),
)
); );
}); });
result.add(
Column (
children: cards,
)
);
return result; return result;
} }
@ -76,19 +74,8 @@ class ViewWidgetState extends State<ViewWidget> {
return result; return result;
} }
Future _refreshData() {
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
TheLogger.debug("Previous data refresh is still in progress");
} else {
_refreshCompleter = Completer();
eventBus.fire(RefreshDataEvent());
}
return _refreshCompleter.future;
}
@override @override
void dispose() { void dispose() {
_refreshDataSubscription.cancel();
super.dispose(); super.dispose();
} }

View File

@ -1,6 +1,6 @@
part of 'main.dart'; part of 'main.dart';
class TheLogger { class Logger {
static List<String> _log = []; static List<String> _log = [];
@ -20,15 +20,15 @@ class TheLogger {
return inDebugMode; return inDebugMode;
} }
static void error(String message) { static void e(String message) {
_writeToLog("Error", message); _writeToLog("Error", message);
} }
static void warning(String message) { static void w(String message) {
_writeToLog("Warning", message); _writeToLog("Warning", message);
} }
static void debug(String message) { static void d(String message) {
_writeToLog("Debug", message); _writeToLog("Debug", message);
} }
@ -47,10 +47,42 @@ class TheLogger {
class HAUtils { class HAUtils {
static void launchURL(String url) async { static void launchURL(String url) async {
if (await canLaunch(url)) { if (await urlLauncher.canLaunch(url)) {
await launch(url); await urlLauncher.launch(url);
} else { } else {
TheLogger.error( "Could not launch $url"); Logger.e( "Could not launch $url");
}
}
static void launchURLInCustomTab(BuildContext context, String url) async {
try {
await launch(
"$url",
option: new CustomTabsOption(
toolbarColor: Theme.of(context).primaryColor,
enableDefaultShare: true,
enableUrlBarHiding: true,
showPageTitle: true,
animation: new CustomTabsAnimation.slideIn()
// or user defined animation.
/*animation: new CustomTabsAnimation(
startEnter: 'slide_up',
startExit: 'android:anim/fade_out',
endEnter: 'android:anim/fade_in',
endExit: 'slide_down',
)*/,
extraCustomTabs: <String>[
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
'org.mozilla.firefox',
// ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx
'com.microsoft.emmx',
],
),
);
} catch (e) {
Logger.w("Can't open custom tab: ${e.toString()}");
Logger.w("Launching in default browser");
HAUtils.launchURL(url);
} }
} }
} }
@ -58,8 +90,13 @@ class HAUtils {
class StateChangedEvent { class StateChangedEvent {
String entityId; String entityId;
String newState; String newState;
bool needToRebuildUI;
StateChangedEvent(this.entityId, this.newState); StateChangedEvent({
this.entityId,
this.newState,
this.needToRebuildUI: false
});
} }
class SettingsChangedEvent { class SettingsChangedEvent {
@ -68,10 +105,6 @@ class SettingsChangedEvent {
SettingsChangedEvent(this.reconnect); SettingsChangedEvent(this.reconnect);
} }
class RefreshDataEvent {
RefreshDataEvent();
}
class RefreshDataFinishedEvent { class RefreshDataFinishedEvent {
RefreshDataFinishedEvent(); RefreshDataFinishedEvent();
} }

View File

@ -7,7 +7,7 @@ packages:
name: archive name: archive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.8"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -35,7 +35,7 @@ packages:
name: cached_network_image name: cached_network_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.0+1" version: "0.7.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -49,14 +49,14 @@ packages:
name: charts_common name: charts_common
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.0" version: "0.6.0"
charts_flutter: charts_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: charts_flutter name: charts_flutter
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.0" version: "0.6.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@ -70,7 +70,7 @@ packages:
name: convert name: convert
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -83,7 +83,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: c5727795659e886a7db8b39a14e2c8987280fe1f resolved-ref: a7ed88a4793e094a4d5d5c2d88a89e55510accde
url: "https://github.com/MarkOSullivan94/dart_config.git" url: "https://github.com/MarkOSullivan94/dart_config.git"
source: git source: git
version: "0.5.0" version: "0.5.0"
@ -93,14 +93,14 @@ packages:
name: date_format name: date_format
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.6"
event_bus: event_bus:
dependency: "direct main" dependency: "direct main"
description: description:
name: event_bus name: event_bus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.3"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -112,21 +112,35 @@ packages:
name: flutter_cache_manager name: flutter_cache_manager
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0+1" version: "0.3.2"
flutter_colorpicker: flutter_custom_tabs:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_colorpicker name: flutter_custom_tabs
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.0" version: "0.6.0"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.1" version: "0.7.0"
flutter_markdown:
dependency: "direct main"
description:
name: flutter_markdown
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.4"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -138,7 +152,7 @@ packages:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.0" version: "0.12.0+1"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -152,7 +166,7 @@ packages:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.7"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@ -167,6 +181,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.11.3+2" version: "0.11.3+2"
markdown:
dependency: transitive
description:
name: markdown
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -188,20 +209,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.6.2" version: "1.6.2"
path_drawing:
dependency: transitive
description:
name: path_drawing
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
path_provider: path_provider:
dependency: transitive dependency: transitive
description: description:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.1" version: "0.5.0+1"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.1"
progress_indicators: progress_indicators:
dependency: "direct main" dependency: "direct main"
description: description:
@ -222,7 +264,7 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3" version: "0.5.1+1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -234,7 +276,14 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.1" version: "1.5.4"
sqflite:
dependency: transitive
description:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -262,21 +311,21 @@ packages:
name: synchronized name: synchronized
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.3" version: "2.1.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.1.0"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.1" version: "0.2.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -290,14 +339,14 @@ packages:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.1" version: "5.0.2"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
name: uuid name: uuid
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "2.0.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -318,7 +367,7 @@ packages:
name: xml name: xml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.3" version: "3.2.5"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@ -327,5 +376,5 @@ packages:
source: hosted source: hosted
version: "2.1.15" version: "2.1.15"
sdks: sdks:
dart: ">=2.0.0 <=2.1.0-dev.9.3.flutter-9c07fb64c4" dart: ">=2.1.0 <3.0.0"
flutter: ">=0.5.6 <2.0.0" flutter: ">=1.2.1 <2.0.0"

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 0.3.10+73 version: 0.5.0+97
environment: environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0" sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -16,12 +16,10 @@ dependencies:
cached_network_image: any cached_network_image: any
url_launcher: any url_launcher: any
date_format: any date_format: any
flutter_colorpicker: any
charts_flutter: any charts_flutter: any
flutter_markdown: any
# The following adds the Cupertino Icons font to your application. flutter_svg: ^0.10.3
# Use with the CupertinoIcons class for iOS style icons. flutter_custom_tabs: ^0.6.0
#cupertino_icons: ^0.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -62,7 +60,7 @@ flutter:
fonts: fonts:
- family: "Material Design Icons" - family: "Material Design Icons"
fonts: fonts:
- asset: fonts/materialdesignicons-webfont.ttf - asset: fonts/materialdesignicons-webfont-3-5-95.ttf
# fonts: # fonts:
# - family: Schyler # - family: Schyler
# fonts: # fonts:

File diff suppressed because one or more lines are too long