Compare commits

...

237 Commits

Author SHA1 Message Date
0219f7bfbb Version 0.3.10 2018-11-24 17:04:41 +02:00
5f3c77f4b9 Resolves #145 Fan support 2018-11-24 17:00:45 +02:00
a36c7a9ca3 Resolves #186 Switch for group with same domain antities 2018-11-24 11:33:59 +02:00
56ce6dfeeb Improved tap animation for glance card 2018-11-24 11:02:28 +02:00
67c214454f build number 2018-11-24 01:20:18 +02:00
73398378c4 Resolves #227 2018-11-24 00:37:55 +02:00
215871ce9e Resolves #226 2018-11-23 21:59:33 +02:00
fd8ea6befd Show auto groups that is not hidden 2018-11-23 19:30:16 +02:00
809a1a1c8c Resolves #146 Lock support 2018-11-23 19:18:17 +02:00
fc8f2f200f Small screens support 2018-11-23 18:16:38 +02:00
f41c9f9197 Resolves #202 Service call info 2018-11-23 16:38:26 +02:00
cdf55ce68b Resolves #201 New progress indicator in the bottom of the app 2018-11-23 16:30:42 +02:00
12088d9516 Resolves #223, Resolves #197 2018-11-23 16:03:38 +02:00
a0235ee385 Handle entity taps and holds in one place 2018-11-23 15:06:42 +02:00
67fbdb13c6 Proper autogenerateg groups detection 2018-11-23 14:33:03 +02:00
c5960de0be Resolves #193 Source selection support for media_player 2018-11-23 14:18:25 +02:00
da15e880ec Sound mode support for media player 2018-11-23 14:11:34 +02:00
efbe33f4e3 Fix font sizes, long entity states 2018-11-18 16:40:12 +02:00
af84c99a2d build 69 2018-11-18 13:25:00 +02:00
438449cad8 Handling taps on entity name and state for glance card 2018-11-18 13:24:05 +02:00
d9ca55c3b7 Resolves #131 2018-11-18 13:19:00 +02:00
f248268984 New bottom info bar 2018-11-18 12:46:54 +02:00
8ee096595c Resolves #192 Don't fetch data on every app resume 2018-11-18 09:47:22 +02:00
a8e79c289b Version 0.3.9 2018-11-17 23:07:58 +02:00
2cd8533882 Link in about box 2018-11-17 23:02:05 +02:00
0a21d9c690 Fix repository url 2018-11-17 22:55:04 +02:00
e77bb533b1 Fix non-lovelace UI cards creating issue 2018-11-17 22:52:06 +02:00
96f1211395 Resolves #55 2018-11-17 22:40:33 +02:00
1e4cb03470 build number 66 2018-11-16 23:36:23 +02:00
ab67b557ca Resolves #183 Service call support for glance card 2018-11-16 23:35:08 +02:00
82c9bd26d1 WIP #183 tap_action support. State change event fix 2018-11-16 22:32:43 +02:00
1bd04abd37 Resolves #189 2018-11-16 14:30:43 +02:00
c5942d22b3 WIP #183 Custom names and icons 2018-11-15 19:08:47 +02:00
37ad5e81cf WIP #183 Glance card ui improvements 2018-11-15 16:13:54 +02:00
26187e6233 Resolves #155 2018-11-15 15:57:17 +02:00
b8f6fda8d3 Remove assumedState from Entity class 2018-11-15 12:49:46 +02:00
62b4e99810 build number 2018-11-14 19:53:00 +02:00
25bf10a64e WIP #183 2018-11-14 19:52:17 +02:00
874410964d Resolves #178 2018-11-14 18:03:50 +02:00
57c30917b3 WIP #55 main media controls 2018-11-14 15:43:04 +02:00
87f89b63e1 State const 2018-11-14 15:14:46 +02:00
3190b45db3 Resolves #176 History can be requested only once per 30 seconds 2018-11-14 13:38:02 +02:00
f5434e26e5 Resolves #174 2018-11-14 13:14:45 +02:00
86b6ad6bba Resolves #171 2018-11-14 12:35:08 +02:00
8a9641fbed Internal build 63 2018-11-12 20:54:48 +02:00
5142391da2 Resolves #184 2018-11-12 20:47:49 +02:00
01090dc3b1 Some improvements 2018-11-12 20:47:49 +02:00
0a7bbb5a38 Update README.md 2018-11-12 16:53:26 +02:00
c347eee9f0 Update README.md 2018-11-12 16:52:10 +02:00
90f197ba54 Update README.md 2018-11-12 12:41:01 +02:00
e09917c687 Merge pull request #177 from estevez-dev/add-license-1
Create LICENSE
2018-11-12 10:35:29 +02:00
a69da832cb Create LICENSE 2018-11-12 10:35:17 +02:00
c1708fd980 WIP #55 Fix preview 2018-11-11 22:21:29 +02:00
c85a9bbe27 WIP #55 2018-11-11 20:54:54 +02:00
d9790dedbb Cards creating optimization 2018-11-11 19:51:02 +02:00
30e4eaa023 Move history widget under additional controls 2018-11-11 18:49:04 +02:00
54e00c3403 WIP #55 2018-11-11 18:36:49 +02:00
0e3474bbcb version 0.3.8 2018-11-06 21:12:24 +02:00
efd06ca547 Resolves #164
Template cover has supported_features = 11, but it supports setting position.
2018-11-06 14:10:34 +02:00
69fd37d4fe Fix settings saving issue 2018-11-06 14:01:00 +02:00
4a49372410 version code 2018-11-05 20:41:19 +02:00
478f58e2d8 v.0.3.7 hotfixes 2018-11-05 20:37:35 +02:00
a87aff67ac Resolves #170 Saving settings button issue fix 2018-11-05 20:34:56 +02:00
644f5e7fc6 Resolves #166 2018-11-05 20:21:44 +02:00
3cddac3dc6 Resolves #167 2018-11-05 20:15:20 +02:00
ab30c64eab v.0.3.7 2018-11-04 23:20:58 +02:00
6d79487219 Resolves #165 Hide controls for unavailable lights 2018-11-04 23:13:25 +02:00
9f7444eae0 Remove widgetHeigth 2018-11-04 22:57:53 +02:00
788d682f2f Resolves #160 Flexible entity heigth 2018-11-04 22:55:09 +02:00
66f84952f0 Get entity name from entity id if was not set 2018-11-04 22:25:22 +02:00
5d95c3702d Resolves #162 View names display 2018-11-04 22:19:45 +02:00
1f0bd8059b Resolves #164 Allow to open cover if it is not fully opened 2018-11-04 21:55:37 +02:00
a7830df628 Fix covers tilt position issue 2018-11-04 21:40:41 +02:00
790446d592 Resolves #161 Colors for more then 10 states in history 2018-11-04 21:36:15 +02:00
bb17885b4a Resolves #163 Location title 2018-11-04 21:02:12 +02:00
04d8681656 log improve and v.0.3.6 2018-11-04 19:30:10 +02:00
71c4ac7fed v.0.3.5 2018-11-04 18:28:02 +02:00
3f7e21e97e Fix Lovelace views int id issue 2018-11-04 18:26:31 +02:00
e24c47b041 Error handling improvements 2018-11-04 18:20:06 +02:00
73b32b30a8 Build number inc 2018-11-04 12:01:41 +02:00
5b6155057c Fix views count issue on app loading 2018-11-04 11:23:21 +02:00
ff4185effe internal build version 2018-11-03 23:33:12 +02:00
b2da9fc04d Fix target temp history 2018-11-03 23:10:25 +02:00
f281fab744 Version 0.3.4 2018-11-03 22:54:36 +02:00
3b99f4feeb Resolves #120 2018-11-03 22:50:21 +02:00
efab8b60b1 WIP #120 null values handling 2018-11-03 21:56:06 +02:00
0e96406573 WIP #120 show all states for climate 2018-11-03 19:54:26 +02:00
ed8757c08d Version code 2018-10-31 01:39:53 +02:00
813770329c WIP #120 render only needed states 2018-10-31 01:37:36 +02:00
1853bd466e WIP #120 combined history state 2018-10-31 01:02:53 +02:00
07258477b3 Unsupported cards improvements 2018-10-30 22:53:49 +02:00
a3adb72cf8 Unsupported lovelace cards showing entities 2018-10-30 22:51:45 +02:00
e25162f7b5 Version code 2018-10-29 23:54:52 +02:00
d30c9d574b WIP #120 Remove custom renderers for dots 2018-10-29 23:30:11 +02:00
efa5a1958c WIP #120 Simple state chart improvements 2018-10-29 23:06:36 +02:00
37f20fae5a Version code change 2018-10-29 00:59:44 +02:00
91db34badb WIP #120 History chart based on attributes 2018-10-29 00:58:52 +02:00
c20200b609 WIP #120 Random color for states 2018-10-28 21:02:38 +02:00
fcd4ac7292 WIP #120 Numeric state charts 2018-10-28 20:01:01 +02:00
e16338c3f2 WIP #120 History widget improvements 2018-10-28 18:07:52 +02:00
6e038b0685 WIP #120 Convert history time to local 2018-10-28 15:25:12 +02:00
052cd3894e WIP #120 Simplest on/off state history chart 2018-10-28 14:56:23 +02:00
809c7d6355 Card separation by type 2018-10-27 17:28:47 +03:00
9edfec7dff Code structure 2018-10-27 14:27:41 +03:00
df56f6ceda version code change 2018-10-27 01:27:12 +03:00
5e834b0645 Logger improvements 2018-10-27 01:24:23 +03:00
8fb0d61a84 Resolves #122 2018-10-27 00:54:05 +03:00
54979b583b version change for internal testing 2018-10-25 00:58:03 +03:00
4e955e98d8 Still #154 default view 2018-10-25 00:54:20 +03:00
88cfcb4382 Resolves #153 hidden entities 2018-10-25 00:13:50 +03:00
5338e45ddc Resolves #154 UI building refactoring 2018-10-25 00:08:26 +03:00
24d071e2f8 WIP #154 UI building refactoring 2018-10-24 23:53:10 +03:00
988cd4a72f Version 0.3.3 2018-10-21 19:19:55 +03:00
d1ea916781 Fix assumed state switch 2018-10-21 19:18:33 +03:00
ce9f25b86c Light color button 2018-10-21 19:12:37 +03:00
f29762c931 Fix hidden group issue 2018-10-21 18:52:29 +03:00
30e4496ef1 Resolves #148 assumed_state support 2018-10-21 17:13:11 +03:00
7f9dc5dd3a Set Light britness to 0 if light is turned off 2018-10-21 16:18:27 +03:00
0f6babc243 Resolves #151 Group visibility support 2018-10-21 16:11:47 +03:00
6a43e04b31 Just small method rename 2018-10-21 15:26:14 +03:00
36fa5a50c4 Remove cancelling null subscription 2018-10-21 14:48:25 +03:00
9ad6d92ccd View entities in entityCollection. Child entities in parse 2018-10-21 14:43:52 +03:00
fafa8f43f4 Minor light fixes 2018-10-21 13:55:18 +03:00
9b490d33d5 Reverting views refactoring 2018-10-21 02:39:51 +03:00
33f9a1075e Remove ViewWrapper widget 2018-10-21 01:09:07 +03:00
b83006e2c3 View as widget refactoring 2018-10-21 00:30:58 +03:00
ba09c36bd2 Resloves #133 Light support 2018-10-18 23:47:55 +03:00
c71ee568b0 Merge pull request #152 from estevez-dev/release/0.3.2
Fix empty cards on default_view
2018-10-18 22:03:51 +03:00
75041f5c23 Fix empty cards on default_view 2018-10-18 21:57:10 +03:00
14da471774 Merge pull request #150 from estevez-dev/release/0.3.1
Resolves #136 cover state
2018-10-17 21:34:36 +03:00
369b44f1c8 Merge branch 'master' into release/0.3.1 2018-10-17 21:34:27 +03:00
8284bb6e76 Resolves #136 cover state 2018-10-17 21:21:00 +03:00
9b3b4dfbbc WIP #133 Lights 2018-10-17 02:19:46 +03:00
5ca4424933 Fix dropdown width 2018-10-16 23:30:17 +03:00
a308aa29a4 Add mode switch stateless widget 2018-10-16 23:20:27 +03:00
9e80b0eaaf Add temperature control stateless widget 2018-10-16 22:35:17 +03:00
85379cf491 Resolves #132 2018-10-16 21:10:59 +03:00
758376a891 Version 0.3.0 2018-10-16 17:53:50 +03:00
2ebba364e3 Resolves #76 Covers support 2018-10-16 17:35:13 +03:00
6e604440c0 Resolves #106 Climate support 2018-10-16 15:14:54 +03:00
c23034688e WIP #106 2018-10-15 18:04:16 +03:00
69f45b52cf WIP #106 2018-10-15 00:29:40 +03:00
ffc053fbe6 Full ui structure refactoring. InheritedWidget as entity model 2018-10-15 00:15:09 +03:00
b5f9ecf601 Minor fixes 2018-10-12 18:03:27 +03:00
948d1d4e23 Resolves #106 Climate support 2018-10-11 23:02:05 +03:00
136297c18b Climate default icon. Icon colors fix 2018-10-08 23:30:09 +03:00
164800951d Resolves #129 2018-10-08 23:11:56 +03:00
84d283de2b VIP #120 2018-10-07 23:06:06 +03:00
2fa35d771a Resolves #123 Account details and settings. Get user name from HA 2018-10-07 20:18:14 +03:00
326cd073b9 Async data fetching 2018-10-07 18:27:10 +03:00
e99c3f5742 Fix wrong password issue and infinity reconnects issue 2018-10-07 18:21:55 +03:00
16a9392fa6 Resolves #79 Too many tabs issue 2018-10-07 17:16:24 +03:00
5bf063969b Resolves #128 Enpty settings change issue 2018-10-07 17:07:06 +03:00
c19a0511a6 Version 0.2.5 2018-10-07 15:08:50 +03:00
a4ac40b366 Resolves #107 Show entity attributes 2018-10-07 15:03:51 +03:00
ce69f044fb Resolves #110: Slider improvements 2018-10-07 12:40:45 +03:00
70b6469bd1 Resolves #118 Fix message queue issue 2018-10-07 12:14:48 +03:00
253316fb1f TODOs 2018-10-07 10:41:41 +03:00
ec71200ab0 Resolves #127 Fix entities order in card 2018-10-07 10:36:50 +03:00
bc1f4eab2e Showing error snakbar improvements. Error icon in header 2018-10-07 10:28:28 +03:00
4085006446 Fix save settings issue 2018-10-07 09:55:37 +03:00
b7fb821abe View now a stateful widget to prevent memory leeks 2018-10-07 09:45:04 +03:00
284e7ba451 Resolves #125 UI building refactored 2018-10-07 02:17:14 +03:00
17a3bd8d35 Resolves #126 Connection settings save button 2018-10-06 20:03:20 +03:00
c2b88c8a12 Resolves #124: Connection handling improvements 2018-10-06 16:01:38 +03:00
c975af4c79 Unnecessary dependency removed 2018-10-03 21:50:11 +03:00
debf1b71f1 Remove some debug messages 2018-10-03 21:42:28 +03:00
4725953b32 Add entity widget type. Preparing to make entity build it's own badge 2018-10-03 16:44:11 +03:00
e7ca1209e2 Update app icon 2018-10-03 16:15:09 +03:00
f9afa663f5 Version code change 2018-10-03 15:55:48 +03:00
5068cbbcf4 Menu quick fix 2018-10-03 15:55:11 +03:00
043d3a9905 Changing only version code 2018-10-03 15:26:46 +03:00
77c5f80c13 Fix fetch timeout on app start 2018-10-03 15:25:01 +03:00
e0d35d07dc Version 0.2.4 2018-10-03 14:37:54 +03:00
285447a5b7 Resolves #114 Error going back from settings 2018-10-03 14:36:23 +03:00
ed3e4ba272 COnnection closing improvements 2018-10-03 10:35:40 +03:00
908563063a Fix input_boolean control 2018-10-03 09:50:14 +03:00
7f2611b410 Version 0.2.3 2018-10-03 00:55:50 +03:00
648750655c Resolves #109 No static width for inputs 2018-10-02 23:21:50 +03:00
8a0d5581d9 Resolves #111: Assumed state 2018-10-02 23:10:40 +03:00
98d716109b Resolves #21: Handling socket disconnect by sink done Future 2018-10-02 22:48:47 +03:00
ebb2f2b4e5 Decline all timeouts as variables 2018-10-02 18:05:50 +03:00
d910e4dd43 Add socket ping interval 2018-10-02 17:42:06 +03:00
95d80fbbfc Resolves #58: Message queue 2018-10-02 17:23:19 +03:00
41297150c2 Implement fetch timer with 30 timeout along with connection timer 2018-10-02 16:00:55 +03:00
b14b248f2f Resolves #72 reconnect on message sending 2018-10-02 15:46:24 +03:00
13fc1bff27 Resolves #61: Prevent second connection opening 2018-10-02 14:50:42 +03:00
eee8f21e76 Version 0.2.2 2018-10-02 00:48:25 +03:00
8ce3560d8d Merge pull request #108 from estevez-dev/feature/entity_widget
Refactoring: Stateful entity widgets
2018-10-01 21:44:31 +00:00
9e97bac85b Refactoring: Stateful entity widgets 2018-10-02 00:41:40 +03:00
4a0b447f00 Separate entity classes on different files 2018-10-01 21:57:54 +03:00
bc4969dae8 Resolves #104: wrong value is set for input_text 2018-10-01 10:24:38 +03:00
5025b3d384 Merge pull request #101 from estevez-dev/release/0.2.1
Release/0.2.1
2018-09-30 20:29:12 +00:00
0d7e7eb6f7 Version 0.2.3 2018-09-30 23:14:40 +03:00
062392b38c Resolves #100 input_datetime 2018-09-30 23:12:27 +03:00
acd468ae75 Resolves #53 input_select support 2018-09-30 20:57:07 +03:00
60f216df13 Resolves #99 Slider with mutliplier 2018-09-30 20:24:13 +03:00
9de8a659d3 Resolves #95: Input value validation 2018-09-30 12:51:55 +03:00
7dd8f65af7 Last updated time in duration 2018-09-30 10:15:32 +03:00
9e83a3e447 Refactoring: Input text and focus 2018-09-30 10:00:19 +03:00
2f135169a9 Fix slider step issue 2018-09-30 01:37:33 +03:00
76d2750ad6 Fix slider issues. Siplify Entity view 2018-09-30 01:07:02 +03:00
571778fbd4 Resolves #80: Card without title 2018-09-29 23:13:02 +03:00
b89b5dfb98 version 0.2.0 2018-09-29 18:13:00 +03:00
a196b0d8d4 Close entity view after setting input value 2018-09-29 18:09:17 +03:00
95f7c14296 Fix entity name padding 2018-09-29 18:02:29 +03:00
2fcd27d240 Fix state handling on entity view page 2018-09-29 17:59:38 +03:00
6834f2ca34 Resolves #52, Resolves #54 Inputs 2018-09-29 17:38:00 +03:00
c0a9b89d40 Resolves #26 Entity view page 2018-09-29 16:19:01 +03:00
067ccfde02 Refactoring: Entity classes by action type. Wntity widget building in
entity
2018-09-29 13:49:25 +03:00
4b4fc338f6 Resolves #91: input_boolean action support 2018-09-29 12:15:31 +03:00
08c07e8398 Resolves #92 Spelling fix 2018-09-29 12:09:01 +03:00
df04d000b2 Fixes #94 Gropups state change event parsing 2018-09-29 12:02:41 +03:00
d0d1ab2740 New card ui. Input_number mode: slider support 2018-09-29 11:52:17 +03:00
af3a5bc611 Resolves #70 Build default_view automatically 2018-09-28 13:33:15 +03:00
b935a0e372 Optimize View creation 2018-09-28 11:23:48 +03:00
49444ab3df Refactoring: badges and cards 2018-09-28 11:18:37 +03:00
098a556279 Massive refactoring: UIBuilder, Vew, HACArd, Badge 2018-09-28 10:16:15 +03:00
375ae36884 Massive refactoring: HomeAssistant, EntityCollection, Entity 2018-09-27 14:51:57 +03:00
0b42019ef3 [WIP] Entity collection 2018-09-26 22:16:50 +03:00
516d38a8a9 version code fix 2018-09-26 00:00:47 +03:00
fb886a4622 Version 0.1.3 2018-09-25 23:41:14 +03:00
662b44d443 CHeck if entity for card has attributes 2018-09-25 23:14:33 +03:00
f9c48e6cc7 Resolves #84: default icon for media players 2018-09-25 22:50:52 +03:00
88d6e1008f Resolves #82: default icon for scenes 2018-09-25 22:48:39 +03:00
4540fadf1e Code refactoring 2018-09-25 22:47:06 +03:00
bd13d3693d Remove state change event messages from log 2018-09-25 22:23:28 +03:00
5db9d6005f Resolves #86 #89: Add 'copy to lipboard' and 'post to github' to log 2018-09-25 22:11:31 +03:00
7e4f744598 Closes #88: Remove version suffix 2018-09-25 21:19:11 +03:00
772b569da5 Minor code cleaning 2018-09-24 22:54:51 +03:00
0e11c1a146 Closes #60 Hide app drawer on item tap 2018-09-24 22:23:01 +03:00
60793dbf89 Add link to github in app drawer 2018-09-24 22:12:56 +03:00
2b622cff04 version 0.1.2 2018-09-24 20:32:16 +03:00
94bcc30421 Fix #77 Skip non existing entities in view 2018-09-24 20:28:17 +03:00
94f43ded6f Fix: #75 Hadle any exception and show it in log view 2018-09-24 20:07:22 +03:00
7f7be8aa78 [#74] Remove floating button 2018-09-24 10:42:31 +03:00
c0e0059487 Settings texts update 2018-09-24 10:27:08 +03:00
97 changed files with 6874 additions and 1243 deletions

201
LICENSE Normal file
View File

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

View File

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

View File

@ -39,8 +39,8 @@ android {
applicationId "com.keyboardcrumbs.haclient"
minSdkVersion 21
targetSdkVersion 27
versionCode 19
versionName "0.1.1-alpha"
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

65
lib/entity.page.dart Normal file
View File

@ -0,0 +1,65 @@
part of 'main.dart';
class EntityViewPage extends StatefulWidget {
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
final String entityId;
final HomeAssistant homeAssistant;
@override
_EntityViewPageState createState() => new _EntityViewPageState();
}
class _EntityViewPageState extends State<EntityViewPage> {
String _title;
StreamSubscription _refreshDataSubscription;
StreamSubscription _stateSubscription;
@override
void initState() {
super.initState();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
TheLogger.debug("State change event handled by entity page: ${event.entityId}");
if (event.entityId == widget.entityId) {
setState(() {});
}
});
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
setState(() {});
});
_prepareData();
}
void _prepareData() async {
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text(_title),
),
body: Padding(
padding: EdgeInsets.all(10.0),
child: HomeAssistantModel(
homeAssistant: widget.homeAssistant,
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
)
),
);
}
@override
void dispose(){
if (_stateSubscription != null) _stateSubscription.cancel();
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
super.dispose();
}
}

View File

@ -0,0 +1,10 @@
part of '../main.dart';
class ButtonEntity extends Entity {
ButtonEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return ButtonStateWidget();
}
}

View File

@ -0,0 +1,128 @@
part of '../main.dart';
class ClimateEntity extends Entity {
@override
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.numericAttributes,
numericState: false,
numericAttributesToShow: ["current_temperature"]
);
static const SUPPORT_TARGET_TEMPERATURE = 1;
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
static const SUPPORT_TARGET_HUMIDITY = 8;
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
static const SUPPORT_FAN_MODE = 64;
static const SUPPORT_OPERATION_MODE = 128;
static const SUPPORT_HOLD_MODE = 256;
static const SUPPORT_SWING_MODE = 512;
static const SUPPORT_AWAY_MODE = 1024;
static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096;
bool get supportTargetTemperature => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
bool get supportTargetTemperatureLow => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
bool get supportTargetHumidity => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
bool get supportTargetHumidityHigh => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
bool get supportTargetHumidityLow => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
bool get supportFanMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) ==
ClimateEntity.SUPPORT_FAN_MODE);
bool get supportOperationMode => ((attributes["supported_features"] &
ClimateEntity.SUPPORT_OPERATION_MODE) ==
ClimateEntity.SUPPORT_OPERATION_MODE);
bool get supportHoldMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) ==
ClimateEntity.SUPPORT_HOLD_MODE);
bool get supportSwingMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE);
bool get supportAwayMode =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
ClimateEntity.SUPPORT_AWAY_MODE);
bool get supportAuxHeat =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) ==
ClimateEntity.SUPPORT_AUX_HEAT);
bool get supportOnOff =>
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) ==
ClimateEntity.SUPPORT_ON_OFF);
List<String> get operationList => attributes["operation_list"] != null
? (attributes["operation_list"] as List).cast<String>()
: null;
List<String> get fanList => attributes["fan_list"] != null
? (attributes["fan_list"] as List).cast<String>()
: null;
List<String> get swingList => attributes["swing_list"] != null
? (attributes["swing_list"] as List).cast<String>()
: null;
double get temperature => _getDoubleAttributeValue('temperature');
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
double get targetLow => _getDoubleAttributeValue('target_temp_low');
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
double get targetHumidity => _getDoubleAttributeValue('humidity');
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
double get minHumidity => _getDoubleAttributeValue('min_humidity');
String get operationMode => attributes['operation_mode'];
String get fanMode => attributes['fan_mode'];
String get swingMode => attributes['swing_mode'];
bool get awayMode => attributes['away_mode'] == "on";
bool get isOff => state == EntityState.off;
bool get auxHeat => attributes['aux_heat'] == "on";
ClimateEntity(Map rawData) : super(rawData);
@override
void update(Map rawData) {
super.update(rawData);
if (supportTargetTemperature) {
historyConfig.numericAttributesToShow.add("temperature");
}
if (supportTargetTemperatureHigh) {
historyConfig.numericAttributesToShow.add("target_temp_high");
}
if (supportTargetTemperatureLow) {
historyConfig.numericAttributesToShow.add("target_temp_low");
}
}
@override
Widget _buildStatePart(BuildContext context) {
return ClimateStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return ClimateControlWidget();
}
@override
double _getDoubleAttributeValue(String attributeName) {
var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1.toDouble();
} else if (temp1 is double) {
return temp1;
} else {
return null;
}
}
}

View File

@ -0,0 +1,57 @@
part of '../main.dart';
class EntityState {
static const on = 'on';
static const off = 'off';
static const home = 'home';
static const not_home = 'not_home';
static const unknown = 'unknown';
static const open = 'open';
static const opening = 'opening';
static const closed = 'closed';
static const closing = 'closing';
static const playing = 'playing';
static const paused = 'paused';
static const idle = 'idle';
static const standby = 'standby';
static const alarm_disarmed = 'disarmed';
static const alarm_armed_home = 'armed_home';
static const alarm_armed_away = 'armed_away';
static const alarm_armed_night = 'armed_night';
static const alarm_armed_custom_bypass = 'armed_custom_bypass';
static const alarm_pending = 'pending';
static const alarm_arming = 'arming';
static const alarm_disarming = 'disarming';
static const alarm_triggered = 'triggered';
static const locked = 'locked';
static const unlocked = 'unlocked';
static const unavailable = 'unavailable';
static const ok = 'ok';
static const problem = 'problem';
}
class EntityTapAction {
static const moreInfo = 'more-info';
static const toggle = 'toggle';
static const callService = 'call-service';
static const none = 'none';
}
class CardType {
static const entities = "entities";
static const glance = "glance";
static const mediaControl = "media-control";
static const weatherForecast = "weather-forecast";
static const thermostat = "thermostat";
static const sensor = "sensor";
static const plantStatus = "plant-status";
static const pictureEntity = "picture-entity";
static const pictureElements = "picture-elements";
static const picture = "picture";
static const map = "map";
static const iframe = "iframe";
static const gauge = "gauge";
static const entityButton = "entity-button";
static const conditional = "conditional";
static const alarmPanel = "alarm-panel";
}

View File

@ -0,0 +1,60 @@
part of '../main.dart';
class CoverEntity extends Entity {
static const SUPPORT_OPEN = 1;
static const SUPPORT_CLOSE = 2;
static const SUPPORT_SET_POSITION = 4;
static const SUPPORT_STOP = 8;
static const SUPPORT_OPEN_TILT = 16;
static const SUPPORT_CLOSE_TILT = 32;
static const SUPPORT_STOP_TILT = 64;
static const SUPPORT_SET_TILT_POSITION = 128;
bool get supportOpen => ((attributes["supported_features"] &
CoverEntity.SUPPORT_OPEN) ==
CoverEntity.SUPPORT_OPEN);
bool get supportClose => ((attributes["supported_features"] &
CoverEntity.SUPPORT_CLOSE) ==
CoverEntity.SUPPORT_CLOSE);
bool get supportSetPosition => ((attributes["supported_features"] &
CoverEntity.SUPPORT_SET_POSITION) ==
CoverEntity.SUPPORT_SET_POSITION);
bool get supportStop => ((attributes["supported_features"] &
CoverEntity.SUPPORT_STOP) ==
CoverEntity.SUPPORT_STOP);
bool get supportOpenTilt => ((attributes["supported_features"] &
CoverEntity.SUPPORT_OPEN_TILT) ==
CoverEntity.SUPPORT_OPEN_TILT);
bool get supportCloseTilt => ((attributes["supported_features"] &
CoverEntity.SUPPORT_CLOSE_TILT) ==
CoverEntity.SUPPORT_CLOSE_TILT);
bool get supportStopTilt => ((attributes["supported_features"] &
CoverEntity.SUPPORT_STOP_TILT) ==
CoverEntity.SUPPORT_STOP_TILT);
bool get supportSetTiltPosition => ((attributes["supported_features"] &
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
CoverEntity.SUPPORT_SET_TILT_POSITION);
double get currentPosition => _getDoubleAttributeValue('current_position');
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0);
bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed));
bool get canTiltBeOpened => currentTiltPosition < 100;
bool get canTiltBeClosed => currentTiltPosition > 0;
CoverEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return CoverStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return CoverControlWidget();
}
}

View File

@ -0,0 +1,42 @@
part of '../main.dart';
class DateTimeEntity extends Entity {
bool get hasDate => attributes["has_date"] ?? false;
bool get hasTime => attributes["has_time"] ?? false;
int get year => attributes["year"] ?? 1970;
int get month => attributes["month"] ?? 1;
int get day => attributes["day"] ?? 1;
int get hour => attributes["hour"] ?? 0;
int get minute => attributes["minute"] ?? 0;
int get second => attributes["second"] ?? 0;
String get formattedState => _getFormattedState();
DateTime get dateTimeState => _getDateTimeState();
DateTimeEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return DateTimeStateWidget();
}
DateTime _getDateTimeState() {
return DateTime(
this.year, this.month, this.day, this.hour, this.minute, this.second);
}
String _getFormattedState() {
String formattedState = "";
if (this.hasDate) {
formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]);
}
if (this.hasTime) {
formattedState += " " + formatDate(dateTimeState, [HH, ':', nn]);
}
return formattedState;
}
void setNewState(newValue) {
eventBus
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
}
}

View File

@ -0,0 +1,181 @@
part of '../main.dart';
class Entity {
static List badgeDomains = [
"alarm_control_panel",
"binary_sensor",
"device_tracker",
"updater",
"sun",
"timer",
"sensor"
];
Map attributes;
String domain;
String entityId;
String state;
DateTime _lastUpdated;
List<Entity> childEntities = [];
List<String> attributesToShow = ["all"];
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.simple
);
String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
String get deviceClass => attributes["device_class"] ?? null;
bool get isView =>
(domain == "group") &&
(attributes != null ? attributes["view"] ?? false : false);
bool get isGroup => domain == "group";
bool get isBadge => Entity.badgeDomains.contains(domain);
String get icon => attributes["icon"] ?? "";
bool get isOn => state == EntityState.on;
String get entityPicture => attributes["entity_picture"];
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
List get childEntityIds => attributes["entity_id"] ?? [];
String get lastUpdated => _getLastUpdatedFormatted();
bool get isHidden => attributes["hidden"] ?? false;
double get doubleState => double.tryParse(state) ?? 0.0;
Entity(Map rawData) {
update(rawData);
}
void update(Map rawData) {
attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"];
state = rawData["state"];
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
}
double _getDoubleAttributeValue(String attributeName) {
var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1.toDouble();
} else if (temp1 is double) {
return temp1;
} else {
return double.tryParse("$temp1");
}
}
int _getIntAttributeValue(String attributeName) {
var temp1 = attributes["$attributeName"];
if (temp1 is int) {
return temp1;
} else if (temp1 is double) {
return temp1.round();
} else {
return int.tryParse("$temp1");
}
}
List<String> getStringListAttributeValue(String attribute) {
if (attributes["$attribute"] != null) {
List<String> result = (attributes["$attribute"] as List).cast<String>();
return result;
} else {
return null;
}
}
Widget buildDefaultWidget(BuildContext context) {
return DefaultEntityContainer(
state: _buildStatePart(context)
);
}
Widget buildGlanceWidget(BuildContext context, bool showName, bool showState) {
return GlanceEntityContainer(
showName: showName,
showState: showState,
);
}
Widget _buildStatePart(BuildContext context) {
return SimpleEntityState();
}
Widget _buildStatePartForPage(BuildContext context) {
return _buildStatePart(context);
}
Widget _buildAdditionalControlsForPage(BuildContext context) {
return Container(
width: 0.0,
height: 0.0,
);
}
Widget buildEntityPageWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[
DefaultEntityContainer(state: _buildStatePartForPage(context)),
LastUpdatedWidget(),
Divider(),
_buildAdditionalControlsForPage(context),
Divider(),
buildHistoryWidget(),
EntityAttributesList()
]),
handleTap: false,
);
}
Widget buildHistoryWidget() {
return EntityHistoryWidget(
config: historyConfig,
);
}
Widget buildBadgeWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: BadgeWidget(),
handleTap: true,
);
}
String getAttribute(String attributeName) {
if (attributes != null) {
return attributes["$attributeName"];
}
return null;
}
String _getLastUpdatedFormatted() {
if (_lastUpdated == null) {
return "-";
} else {
DateTime now = DateTime.now();
Duration d = now.difference(_lastUpdated);
String text;
int v;
if (d.inDays == 0) {
if (d.inHours == 0) {
if (d.inMinutes == 0) {
text = "seconds ago";
v = d.inSeconds;
} else {
text = "minutes ago";
v = d.inMinutes;
}
} else {
text = "hours ago";
v = d.inHours;
}
} else {
text = "days ago";
v = d.inDays;
}
return "$v $text";
}
}
}

View File

@ -0,0 +1,83 @@
part of '../main.dart';
class EntityWrapper {
String displayName;
String icon;
String tapAction;
String holdAction;
String tapActionService;
Map<String, dynamic> tapActionServiceData;
String holdActionService;
Map<String, dynamic> holdActionServiceData;
Entity entity;
EntityWrapper({
this.entity,
String icon,
String displayName,
this.tapAction: EntityTapAction.moreInfo,
this.holdAction: EntityTapAction.none,
this.tapActionService,
this.tapActionServiceData,
this.holdActionService,
this.holdActionServiceData
}) {
this.icon = icon ?? entity.icon;
this.displayName = displayName ?? entity.displayName;
}
void handleTap() {
switch (tapAction) {
case EntityTapAction.toggle: {
eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break;
}
case EntityTapAction.callService: {
eventBus.fire(
ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData));
break;
}
case EntityTapAction.none: {
break;
}
default: {
eventBus.fire(
new ShowEntityPageEvent(entity));
break;
}
}
}
void handleHold() {
switch (holdAction) {
case EntityTapAction.toggle: {
eventBus.fire(
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
break;
}
case EntityTapAction.callService: {
eventBus.fire(
ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData));
break;
}
case EntityTapAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity));
break;
}
default: {
break;
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,72 @@
part of '../main.dart';
class LightEntity extends Entity {
static const SUPPORT_BRIGHTNESS = 1;
static const SUPPORT_COLOR_TEMP = 2;
static const SUPPORT_EFFECT = 4;
static const SUPPORT_FLASH = 8;
static const SUPPORT_COLOR = 16;
static const SUPPORT_TRANSITION = 32;
static const SUPPORT_WHITE_VALUE = 128;
bool get supportBrightness => ((attributes["supported_features"] &
LightEntity.SUPPORT_BRIGHTNESS) ==
LightEntity.SUPPORT_BRIGHTNESS);
bool get supportColorTemp => ((attributes["supported_features"] &
LightEntity.SUPPORT_COLOR_TEMP) ==
LightEntity.SUPPORT_COLOR_TEMP);
bool get supportEffect => ((attributes["supported_features"] &
LightEntity.SUPPORT_EFFECT) ==
LightEntity.SUPPORT_EFFECT);
bool get supportFlash => ((attributes["supported_features"] &
LightEntity.SUPPORT_FLASH) ==
LightEntity.SUPPORT_FLASH);
bool get supportColor => ((attributes["supported_features"] &
LightEntity.SUPPORT_COLOR) ==
LightEntity.SUPPORT_COLOR);
bool get supportTransition => ((attributes["supported_features"] &
LightEntity.SUPPORT_TRANSITION) ==
LightEntity.SUPPORT_TRANSITION);
bool get supportWhiteValue => ((attributes["supported_features"] &
LightEntity.SUPPORT_WHITE_VALUE) ==
LightEntity.SUPPORT_WHITE_VALUE);
int get brightness => _getIntAttributeValue("brightness");
int get colorTemp => _getIntAttributeValue("color_temp");
double get maxMireds => _getDoubleAttributeValue("max_mireds");
double get minMireds => _getDoubleAttributeValue("min_mireds");
Color get color => _getColor();
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
List<String> get effectList => getStringListAttributeValue("effect_list");
LightEntity(Map rawData) : super(rawData);
Color _getColor() {
List rgb = attributes["rgb_color"];
try {
if ((rgb != null) && (rgb.length > 0)) {
return Color.fromARGB(255, rgb[0], rgb[1], rgb[2]);
} else {
return null;
}
} catch (e) {
return null;
}
}
@override
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
if (!isAdditionalControls) {
return Container(height: 0.0, width: 0.0);
} else {
return LightControlsWidget();
}
}
}

View File

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

View File

@ -0,0 +1,83 @@
part of '../main.dart';
class MediaPlayerEntity extends Entity {
static const SUPPORT_PAUSE = 1;
static const SUPPORT_SEEK = 2;
static const SUPPORT_VOLUME_SET = 4;
static const SUPPORT_VOLUME_MUTE = 8;
static const SUPPORT_PREVIOUS_TRACK = 16;
static const SUPPORT_NEXT_TRACK = 32;
static const SUPPORT_TURN_ON = 128;
static const SUPPORT_TURN_OFF = 256;
static const SUPPORT_PLAY_MEDIA = 512;
static const SUPPORT_VOLUME_STEP = 1024;
static const SUPPORT_SELECT_SOURCE = 2048;
static const SUPPORT_STOP = 4096;
static const SUPPORT_CLEAR_PLAYLIST = 8192;
static const SUPPORT_PLAY = 16384;
static const SUPPORT_SHUFFLE_SET = 32768;
static const SUPPORT_SELECT_SOUND_MODE = 65536;
MediaPlayerEntity(Map rawData) : super(rawData);
bool get supportPause => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_PAUSE) ==
MediaPlayerEntity.SUPPORT_PAUSE);
bool get supportSeek => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_SEEK) ==
MediaPlayerEntity.SUPPORT_SEEK);
bool get supportVolumeSet => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_VOLUME_SET) ==
MediaPlayerEntity.SUPPORT_VOLUME_SET);
bool get supportVolumeMute => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_VOLUME_MUTE) ==
MediaPlayerEntity.SUPPORT_VOLUME_MUTE);
bool get supportPreviousTrack => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) ==
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK);
bool get supportNextTrack => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_NEXT_TRACK) ==
MediaPlayerEntity.SUPPORT_NEXT_TRACK);
bool get supportTurnOn => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_TURN_ON) ==
MediaPlayerEntity.SUPPORT_TURN_ON);
bool get supportTurnOff => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_TURN_OFF) ==
MediaPlayerEntity.SUPPORT_TURN_OFF);
bool get supportPlayMedia => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_PLAY_MEDIA) ==
MediaPlayerEntity.SUPPORT_PLAY_MEDIA);
bool get supportVolumeStep => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_VOLUME_STEP) ==
MediaPlayerEntity.SUPPORT_VOLUME_STEP);
bool get supportSelectSource => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_SELECT_SOURCE) ==
MediaPlayerEntity.SUPPORT_SELECT_SOURCE);
bool get supportStop => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_STOP) ==
MediaPlayerEntity.SUPPORT_STOP);
bool get supportClearPlaylist => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) ==
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST);
bool get supportPlay => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_PLAY) ==
MediaPlayerEntity.SUPPORT_PLAY);
bool get supportShuffleSet => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_SHUFFLE_SET) ==
MediaPlayerEntity.SUPPORT_SHUFFLE_SET);
bool get supportSelectSoundMode => ((attributes["supported_features"] &
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) ==
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE);
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
List<String> get sourceList => getStringListAttributeValue("source_list");
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return MediaPlayerControls();
}
}

View File

@ -0,0 +1,17 @@
part of '../main.dart';
class SunEntity extends Entity {
SunEntity(Map rawData) : super(rawData);
}
class SensorEntity extends Entity {
@override
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.numericState,
numericState: true
);
SensorEntity(Map rawData) : super(rawData);
}

View File

@ -0,0 +1,14 @@
part of '../main.dart';
class SelectEntity extends Entity {
List<String> get listOptions => attributes["options"] != null
? (attributes["options"] as List).cast<String>()
: [];
SelectEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return SelectStateWidget();
}
}

View File

@ -0,0 +1,44 @@
part of '../main.dart';
class SliderEntity extends Entity {
SliderEntity(Map rawData) : super(rawData);
double get minValue => _getDoubleAttributeValue("min") ?? 0.0;
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
double get valueStep => _getDoubleAttributeValue("step") ?? 1.0;
@override
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.numericState,
numericState: true
);
/*@override
Widget _buildStatePart(BuildContext context) {
return Expanded(
//width: 200.0,
child: Row(
children: <Widget>[
SliderStateWidget(
expanded: true,
),
SimpleEntityState(
expanded: false,
),
],
),
);
}
@override
Widget _buildStatePartForPage(BuildContext context) {
return SimpleEntityState(
expanded: false,
);
}*/
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return SliderControlsWidget();
}
}

View File

@ -0,0 +1,10 @@
part of '../main.dart';
class SwitchEntity extends Entity {
SwitchEntity(Map rawData) : super(rawData);
@override
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();
}
}

View File

@ -0,0 +1,16 @@
part of '../main.dart';
class TextEntity extends Entity {
TextEntity(Map rawData) : super(rawData);
int get valueMinLength => attributes["min"] ?? -1;
int get valueMaxLength => attributes["max"] ?? -1;
String get valuePattern => attributes["pattern"] ?? null;
bool get isTextField => attributes["mode"] == "text";
bool get isPasswordField => attributes["mode"] == "password";
@override
Widget _buildStatePart(BuildContext context) {
return TextInputStateWidget();
}
}

View File

@ -0,0 +1,161 @@
part of 'main.dart';
class EntityCollection {
Map<String, Entity> _allEntities;
//Map<String, Entity> views;
bool get isEmpty => _allEntities.isEmpty;
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
EntityCollection() {
_allEntities = {};
//views = {};
}
bool get hasDefaultView => _allEntities.keys.contains("group.default_view");
void parse(List rawData) {
_allEntities.clear();
//views.clear();
TheLogger.debug("Parsing ${rawData.length} Home Assistant entities");
rawData.forEach((rawEntityData) {
addFromRaw(rawEntityData);
});
_allEntities.forEach((entityId, entity){
if ((entity.isGroup) && (entity.childEntityIds != null)) {
entity.childEntities = getAll(entity.childEntityIds);
}
/*if (entity.isView) {
views[entityId] = entity;
}*/
});
}
Entity _createEntityInstance(rawEntityData) {
switch (rawEntityData["entity_id"].split(".")[0]) {
case 'sun': {
return SunEntity(rawEntityData);
}
case "media_player": {
return MediaPlayerEntity(rawEntityData);
}
case 'sensor': {
return SensorEntity(rawEntityData);
}
case 'lock': {
return LockEntity(rawEntityData);
}
case "automation":
case "input_boolean":
case "switch": {
return SwitchEntity(rawEntityData);
}
case "light": {
return LightEntity(rawEntityData);
}
case "group": {
return GroupEntity(rawEntityData);
}
case "script":
case "scene": {
return ButtonEntity(rawEntityData);
}
case "input_datetime": {
return DateTimeEntity(rawEntityData);
}
case "input_select": {
return SelectEntity(rawEntityData);
}
case "input_number": {
return SliderEntity(rawEntityData);
}
case "input_text": {
return TextEntity(rawEntityData);
}
case "climate": {
return ClimateEntity(rawEntityData);
}
case "cover": {
return CoverEntity(rawEntityData);
}
case "fan": {
return FanEntity(rawEntityData);
}
default: {
return Entity(rawEntityData);
}
}
}
void updateState(Map rawStateData) {
if (isExist(rawStateData["entity_id"])) {
updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
} else {
addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
}
}
void add(Entity entity) {
_allEntities[entity.entityId] = entity;
}
Entity addFromRaw(Map rawEntityData) {
Entity entity = _createEntityInstance(rawEntityData);
_allEntities[entity.entityId] = entity;
return entity;
}
void updateFromRaw(Map rawEntityData) {
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
}
Entity get(String entityId) {
return _allEntities[entityId];
}
List<Entity> getAll(List ids) {
List<Entity> result = [];
ids.forEach((id){
Entity en = get(id);
if (en != null) {
result.add(en);
}
});
return result;
}
bool isExist(String entityId) {
return _allEntities[entityId] != null;
}
List<Entity> filterEntitiesForDefaultView() {
List<Entity> result = [];
List<Entity> groups = [];
List<Entity> nonGroupEntities = [];
_allEntities.forEach((id, entity){
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
groups.add(entity);
}
if (!entity.isGroup) {
nonGroupEntities.add(entity);
}
});
nonGroupEntities.forEach((entity) {
bool foundInGroup = false;
groups.forEach((groupEntity) {
if (groupEntity.childEntityIds.contains(entity.entityId)) {
foundInGroup = true;
}
});
if (!foundInGroup) {
result.add(entity);
}
});
result.insertAll(0, groups);
return result;
}
}

View File

@ -0,0 +1,125 @@
part of '../../main.dart';
class BadgeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
double iconSize = 26.0;
Widget badgeIcon;
String onBadgeTextValue;
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
EntityColor.badgeColors["default"];
switch (entityModel.entityWrapper.entity.domain) {
case "sun":
{
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
? Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
size: iconSize,
)
: Icon(
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
size: iconSize,
);
break;
}
case "sensor":
{
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${entityModel.entityWrapper.entity.state}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17.0),
),
);
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;
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
onBadgeText = Container(width: 0.0, height: 0.0);
} else {
onBadgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$onBadgeTextValue",
style: TextStyle(fontSize: 12.0, color: Colors.white),
textAlign: TextAlign.center,
softWrap: false,
overflow: TextOverflow.fade),
decoration: new BoxDecoration(
// Circle shape
//shape: BoxShape.circle,
color: iconColor,
borderRadius: BorderRadius.circular(9.0),
));
}
return GestureDetector(
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
width: 50.0,
height: 50.0,
decoration: new BoxDecoration(
// Circle shape
shape: BoxShape.circle,
color: Colors.white,
// The border you want
border: new Border.all(
width: 2.0,
color: iconColor,
),
),
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
width: 46.0,
height: 46.0,
top: 0.0,
left: 0.0,
child: badgeIcon,
),
Positioned(
//width: 50.0,
bottom: -9.0,
left: -10.0,
right: -10.0,
child: Center(
child: onBadgeText,
))
],
),
),
Container(
width: 60.0,
child: Text(
"${entityModel.entityWrapper.displayName}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.0),
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity)));
}
}

View File

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

View File

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

View File

@ -0,0 +1,62 @@
part of '../../main.dart';
class ModeSelectorWidget extends StatelessWidget {
final String caption;
final List<String> options;
final String value;
final double captionFontSize;
final double valueFontSize;
final double bottomPadding;
final onChange;
ModeSelectorWidget({
Key key,
@required this.caption,
@required this.options,
this.value,
@required this.onChange,
this.captionFontSize,
this.valueFontSize,
this.bottomPadding
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("$caption", style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
)),
Row(
children: <Widget>[
Expanded(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton<String>(
value: value,
iconSize: 30.0,
isExpanded: true,
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),
),
),
)
],
),
Container(height: bottomPadding ?? Sizes.rowPadding,)
],
);
}
}

View File

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

View File

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

View File

@ -0,0 +1,467 @@
part of '../../main.dart';
class ClimateControlWidget extends StatefulWidget {
ClimateControlWidget({Key key}) : super(key: key);
@override
_ClimateControlWidgetState createState() => _ClimateControlWidgetState();
}
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _showPending = false;
bool _changedHere = false;
Timer _resetTimer;
double _tmpTemperature = 0.0;
double _tmpTargetLow = 0.0;
double _tmpTargetHigh = 0.0;
double _tmpTargetHumidity = 0.0;
String _tmpOperationMode;
String _tmpFanMode;
String _tmpSwingMode;
bool _tmpAwayMode = false;
bool _tmpIsOff = false;
bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
_tmpOperationMode = entity.operationMode;
_tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode;
_tmpAwayMode = entity.awayMode;
_tmpIsOff = entity.isOff;
_tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity;
_showPending = false;
_changedHere = false;
}
void _temperatureUp(ClimateEntity entity, double step) {
_tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp;
_setTemperature(entity);
}
void _temperatureDown(ClimateEntity entity, double step) {
_tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp;
_setTemperature(entity);
}
void _targetLowUp(ClimateEntity entity, double step) {
_tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetLowDown(ClimateEntity entity, double step) {
_tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp;
_setTargetTemp(entity);
}
void _targetHighUp(ClimateEntity entity, double step) {
_tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetHighDown(ClimateEntity entity, double step) {
_tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp;
_setTargetTemp(entity);
}
void _setTemperature(ClimateEntity entity) {
setState(() {
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
});
}
void _setTargetTemp(ClimateEntity entity) {
setState(() {
_tmpTargetLow = double.parse(_tmpTargetLow.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)}"}));
_resetStateTimer(entity);
});
}
void _setTargetHumidity(ClimateEntity entity, double value) {
setState(() {
_tmpTargetHumidity = value.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
_resetStateTimer(entity);
});
}
void _setOperationMode(ClimateEntity entity, value) {
setState(() {
_tmpOperationMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
_resetStateTimer(entity);
});
}
void _setSwingMode(ClimateEntity entity, value) {
setState(() {
_tmpSwingMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
_resetStateTimer(entity);
});
}
void _setFanMode(ClimateEntity entity, value) {
setState(() {
_tmpFanMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
_resetStateTimer(entity);
});
}
void _setAwayMode(ClimateEntity entity, value) {
setState(() {
_tmpAwayMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
_resetStateTimer(entity);
});
}
void _setOnOf(ClimateEntity entity, value) {
setState(() {
_tmpIsOff = !value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
_resetStateTimer(entity);
});
}
void _setAuxHeat(ClimateEntity entity, value) {
setState(() {
_tmpAuxHeat = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
_resetStateTimer(entity);
});
}
void _resetStateTimer(ClimateEntity entity) {
if (_resetTimer!=null) {
_resetTimer.cancel();
}
_resetTimer = Timer(Duration(seconds: 3), () {
setState(() {});
_resetVars(entity);
});
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity;
if (_changedHere) {
_showPending = (_tmpTemperature != entity.temperature);
_changedHere = false;
} else {
_resetTimer?.cancel();
_resetVars(entity);
}
return Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildOnOffControl(entity),
_buildTemperatureControls(entity),
_buildTargetTemperatureControls(entity),
_buildHumidityControls(entity),
_buildOperationControl(entity),
_buildFanControl(entity),
_buildSwingControl(entity),
_buildAwayModeControl(entity),
_buildAuxHeatControl(entity)
],
),
);
}
Widget _buildAwayModeControl(ClimateEntity entity) {
if (entity.supportAwayMode) {
return ModeSwitchWidget(
caption: "Away mode",
onChange: (value) => _setAwayMode(entity, value),
value: _tmpAwayMode,
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildOnOffControl(ClimateEntity entity) {
if (entity.supportOnOff) {
return ModeSwitchWidget(
onChange: (value) => _setOnOf(entity, value),
caption: "On / Off",
value: !_tmpIsOff
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildAuxHeatControl(ClimateEntity entity) {
if (entity.supportAuxHeat ) {
return ModeSwitchWidget(
caption: "Aux heat",
onChange: (value) => _setAuxHeat(entity, value),
value: _tmpAuxHeat
);
} else {
return Container(height: 0.0, width: 0.0,);
}
}
Widget _buildOperationControl(ClimateEntity entity) {
if (entity.supportOperationMode) {
return ModeSelectorWidget(
onChange: (mode) => _setOperationMode(entity, mode),
options: entity.operationList,
caption: "Operation",
value: _tmpOperationMode,
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildFanControl(ClimateEntity entity) {
if (entity.supportFanMode) {
return ModeSelectorWidget(
options: entity.fanList,
onChange: (mode) => _setFanMode(entity, mode),
caption: "Fan mode",
value: _tmpFanMode,
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildSwingControl(ClimateEntity entity) {
if (entity.supportSwingMode) {
return ModeSelectorWidget(
onChange: (mode) => _setSwingMode(entity, mode),
options: entity.swingList,
value: _tmpSwingMode,
caption: "Swing mode"
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildTemperatureControls(ClimateEntity entity) {
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
TemperatureControlWidget(
value: _tmpTemperature,
fontColor: _showPending ? Colors.red : Colors.black,
onLargeDec: () => _temperatureDown(entity, 0.5),
onLargeInc: () => _temperatureUp(entity, 0.5),
onSmallDec: () => _temperatureDown(entity, 0.1),
onSmallInc: () => _temperatureUp(entity, 0.1),
)
],
);
} else {
return Container(width: 0.0, height: 0.0,);
}
}
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
List<Widget> controls = [];
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
controls.addAll(<Widget>[
TemperatureControlWidget(
value: _tmpTargetLow,
fontColor: _showPending ? Colors.red : Colors.black,
onLargeDec: () => _targetLowDown(entity, 0.5),
onLargeInc: () => _targetLowUp(entity, 0.5),
onSmallDec: () => _targetLowDown(entity, 0.1),
onSmallInc: () => _targetLowUp(entity, 0.1),
),
Expanded(
child: Container(height: 10.0),
)
]);
}
if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) {
controls.add(
TemperatureControlWidget(
value: _tmpTargetHigh,
fontColor: _showPending ? Colors.red : Colors.black,
onLargeDec: () => _targetHighDown(entity, 0.5),
onLargeInc: () => _targetHighUp(entity, 0.5),
onSmallDec: () => _targetHighDown(entity, 0.1),
onSmallInc: () => _targetHighUp(entity, 0.1),
)
);
}
if (controls.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature range", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
Row(
children: controls,
)
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildHumidityControls(ClimateEntity entity) {
List<Widget> result = [];
if (entity.supportTargetHumidity) {
result.addAll(<Widget>[
Text(
"$_tmpTargetHumidity%",
style: TextStyle(fontSize: Sizes.largeFontSize),
),
Expanded(
child: Slider(
value: _tmpTargetHumidity,
max: entity.maxHumidity,
min: entity.minHumidity,
onChanged: ((double val) {
setState(() {
_changedHere = true;
_tmpTargetHumidity = val.roundToDouble();
});
}),
onChangeEnd: (double v) => _setTargetHumidity(entity, v),
),
)
]);
}
if (result.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Target humidity", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
Container(
height: Sizes.rowPadding,
)
],
);
} else {
return Container(
width: 0.0,
height: 0.0,
);
}
}
@override
void dispose() {
_resetTimer?.cancel();
super.dispose();
}
}
class TemperatureControlWidget extends StatelessWidget {
final double value;
final double fontSize;
final Color fontColor;
final onSmallInc;
final onLargeInc;
final onSmallDec;
final onLargeDec;
TemperatureControlWidget(
{Key key,
@required this.value,
@required this.onSmallInc,
@required this.onSmallDec,
@required this.onLargeInc,
@required this.onLargeDec,
this.fontSize,
this.fontColor})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"$value",
style: TextStyle(
fontSize: fontSize ?? 24.0,
color: fontColor ?? Colors.black
),
),
Column(
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => onSmallInc(),
),
IconButton(
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
'mdi:chevron-down')),
iconSize: 30.0,
onPressed: () => onSmallDec(),
)
],
),
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

@ -0,0 +1,200 @@
part of '../../main.dart';
class CoverControlWidget extends StatefulWidget {
CoverControlWidget({Key key}) : super(key: key);
@override
_CoverControlWidgetState createState() => _CoverControlWidgetState();
}
class _CoverControlWidgetState extends State<CoverControlWidget> {
double _tmpPosition = 0.0;
double _tmpTiltPosition = 0.0;
bool _changedHere = false;
void _setNewPosition(CoverEntity entity, double position) {
setState(() {
_tmpPosition = position.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
});
}
void _setNewTiltPosition(CoverEntity entity, double position) {
setState(() {
_tmpTiltPosition = position.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
});
}
void _resetVars(CoverEntity entity) {
_tmpPosition = entity.currentPosition;
_tmpTiltPosition = entity.currentTiltPosition;
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entityWrapper.entity;
if (_changedHere) {
_changedHere = false;
} else {
_resetVars(entity);
}
return Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildPositionControls(entity),
_buildTiltControls(entity)
],
),
);
}
Widget _buildPositionControls(CoverEntity entity) {
if (entity.supportSetPosition) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
),
Slider(
value: _tmpPosition,
min: 0.0,
max: 100.0,
divisions: 10,
onChanged: (double value) {
setState(() {
_tmpPosition = value.roundToDouble();
_changedHere = true;
});
},
onChangeEnd: (double value) => _setNewPosition(entity, value),
),
Container(height: Sizes.rowPadding,)
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildTiltControls(CoverEntity entity) {
List<Widget> controls = [];
if (entity.supportCloseTilt || entity.supportOpenTilt || entity.supportStopTilt) {
controls.add(
CoverTiltControlsWidget()
);
}
if (entity.supportSetTiltPosition) {
controls.addAll(<Widget>[
Slider(
value: _tmpTiltPosition,
min: 0.0,
max: 100.0,
divisions: 10,
onChanged: (double value) {
setState(() {
_tmpTiltPosition = value.roundToDouble();
_changedHere = true;
});
},
onChangeEnd: (double value) => _setNewTiltPosition(entity, value),
),
Container(height: Sizes.rowPadding,)
]);
}
if (controls.isNotEmpty) {
controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Tilt position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: controls,
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
}
class CoverTiltControlsWidget extends StatelessWidget {
void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "open_cover_tilt", entity.entityId, null));
}
void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "close_cover_tilt", entity.entityId, null));
}
void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "stop_cover_tilt", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entityWrapper.entity;
List<Widget> buttons = [];
if (entity.supportOpenTilt) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName(
"mdi:arrow-top-right"),
size: Sizes.iconSize,
),
onPressed: entity.canTiltBeOpened ? () => _open(entity) : null));
} else {
buttons.add(Container(
width: Sizes.iconSize + 20.0,
));
}
if (entity.supportStopTilt) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
size: Sizes.iconSize,
),
onPressed: () => _stop(entity)));
} else {
buttons.add(Container(
width: Sizes.iconSize + 20.0,
));
}
if (entity.supportCloseTilt) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName(
"mdi:arrow-bottom-left"),
size: Sizes.iconSize,
),
onPressed: entity.canTiltBeClosed ? () => _close(entity) : null));
} else {
buttons.add(Container(
width: Sizes.iconSize + 20.0,
));
}
return Row(
children: buttons,
);
}
}

View File

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

View File

@ -0,0 +1,206 @@
part of '../../main.dart';
class LightControlsWidget extends StatefulWidget {
@override
_LightControlsWidgetState createState() => _LightControlsWidgetState();
}
class _LightControlsWidgetState extends State<LightControlsWidget> {
int _tmpBrightness;
int _tmpColorTemp;
Color _tmpColor;
bool _changedHere = false;
String _tmpEffect;
void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 0;
_tmpColorTemp = entity.colorTemp;
_tmpColor = entity.color;
_tmpEffect = null;
}
void _setBrightness(LightEntity entity, double value) {
setState(() {
_tmpBrightness = value.round();
_changedHere = true;
if (_tmpBrightness > 0) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"brightness": _tmpBrightness}));
} else {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_off", entity.entityId,
null));
}
});
}
void _setColorTemp(LightEntity entity, double value) {
setState(() {
_tmpColorTemp = value.round();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"color_temp": _tmpColorTemp}));
});
}
void _setColor(LightEntity entity, Color color) {
setState(() {
_tmpColor = color;
_changedHere = true;
TheLogger.debug( "Color: [${color.red}, ${color.green}, ${color.blue}]");
if ((color == Colors.black) || ((color.red == color.green) && (color.green == color.blue))) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_off", entity.entityId,
null));
} else {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"rgb_color": [color.red, color.green, color.blue]}));
}
});
}
void _setEffect(LightEntity entity, String value) {
setState(() {
_tmpEffect = value;
_changedHere = true;
if (_tmpEffect != null) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"effect": "$value"}));
}
});
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final LightEntity entity = entityModel.entityWrapper.entity;
if (!_changedHere) {
_resetState(entity);
} else {
_changedHere = false;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_buildBrightnessControl(entity),
_buildColorTempControl(entity),
_buildColorControl(entity),
_buildEffectControl(entity)
],
);
}
Widget _buildBrightnessControl(LightEntity entity) {
if ((entity.supportBrightness) && (_tmpBrightness != null) && (entity.state != EntityState.unavailable)) {
return UniversalSlider(
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpBrightness = value.round();
});
},
min: 0.0,
max: 255.0,
onChangeEnd: (value) => _setBrightness(entity, value),
value: _tmpBrightness.toDouble(),
leading: Icon(Icons.brightness_5),
title: "Brightness",
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildColorTempControl(LightEntity entity) {
if ((entity.supportColorTemp) && (_tmpColorTemp != null)) {
return UniversalSlider(
title: "Color temperature",
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
value: _tmpColorTemp.toDouble(),
onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds,
min: entity.minMireds,
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpColorTemp = value.round();
});
},
closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildColorControl(LightEntity entity) {
if ((entity.supportColor) && (entity.color != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(height: Sizes.rowPadding,),
RaisedButton(
onPressed: () => _showColorPicker(entity),
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,),
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
void _showColorPicker(LightEntity entity) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
titlePadding: EdgeInsets.all(0.0),
contentPadding: EdgeInsets.all(0.0),
content: SingleChildScrollView(
child: MaterialPicker(
pickerColor: _tmpColor,
onColorChanged: (color) {
_setColor(entity, color);
Navigator.of(context).pop();
},
enableLabel: true,
),
),
);
},
);
}
Widget _buildEffectControl(LightEntity entity) {
if ((entity.supportEffect) && (entity.effectList != null)) {
return ModeSelectorWidget(
onChange: (effect) => _setEffect(entity, effect),
caption: "Effect",
options: entity.effectList,
value: _tmpEffect
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
part of '../main.dart';
class EntityIcon extends StatelessWidget {
final EdgeInsetsGeometry padding;
final double iconSize;
const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: MaterialDesignIcons.createIconWidgetFromEntityData(
entityWrapper,
iconSize,
EntityColor.stateColor(entityWrapper.entity.state)
),
);
}
}

View File

@ -0,0 +1,27 @@
part of '../main.dart';
class EntityName extends StatelessWidget {
final EdgeInsetsGeometry padding;
final TextOverflow textOverflow;
final bool wordsWrap;
final double fontSize;
final TextAlign textAlign;
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);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: Text(
"${entityWrapper.displayName}",
overflow: textOverflow,
softWrap: wordsWrap,
style: TextStyle(fontSize: fontSize),
textAlign: textAlign,
),
);
}
}

View File

@ -0,0 +1,14 @@
part of '../main.dart';
class EntityPageContainer extends StatelessWidget {
EntityPageContainer({Key key, @required this.children}) : super(key: key);
final List<Widget> children;
@override
Widget build(BuildContext context) {
return ListView(
children: children,
);
}
}

View File

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

View File

@ -0,0 +1,230 @@
part of '../../main.dart';
class CombinedHistoryChartWidget extends StatefulWidget {
final rawHistory;
final EntityHistoryConfig config;
const CombinedHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _CombinedHistoryChartWidgetState();
}
}
class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget> {
int _selectedId = -1;
List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory;
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
_parsedHistory = _parseHistory();
DateTime selectedTime;
List<String> selectedStates = [];
List<int> colorIndexes = [];
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
selectedTime = _parsedHistory.first.data[_selectedId].startTime;
_parsedHistory.where((item) { return item.id == "state"; }).forEach((item) {
selectedStates.add(item.data[_selectedId].state);
colorIndexes.add(item.data[_selectedId].colorId);
});
_parsedHistory.where((item) { return item.id == "value"; }).forEach((item) {
selectedStates.add("${item.data[_selectedId].value ?? '-'}");
colorIndexes.add(item.data[_selectedId].colorId);
});
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
HistoryControlWidget(
selectedTimeStart: selectedTime,
selectedStates: selectedStates,
onPrevTap: () => _selectPrev(),
onNextTap: () => _selectNext(),
colorIndexes: colorIndexes,
),
SizedBox(
height: 150.0,
child: charts.TimeSeriesChart(
_parsedHistory,
animate: false,
primaryMeasureAxis: new charts.NumericAxisSpec(
tickProviderSpec:
new charts.BasicNumericTickProviderSpec(zeroBound: false)),
dateTimeFactory: const charts.LocalDateTimeFactory(),
defaultRenderer: charts.LineRendererConfig(
includeArea: false,
includePoints: true
),
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
changedListener: (model) => _onSelectionChanged(model),
)
],
customSeriesRenderers: [
new charts.SymbolAnnotationRendererConfig(
customRendererId: "stateBars"
)
],
),
)
],
);
}
double _parseToDouble(temp1) {
if (temp1 is int) {
return temp1.toDouble();
} else if (temp1 is double) {
return temp1;
} else {
return double.tryParse("$temp1");
}
}
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
TheLogger.debug(" parsing history...");
Map<String, List<EntityHistoryMoment>> numericDataLists = {};
int colorIdCounter = 0;
widget.config.numericAttributesToShow.forEach((String attrName) {
TheLogger.debug(" parsing attribute $attrName");
List<EntityHistoryMoment> data = [];
DateTime now = DateTime.now();
for (var i = 0; i < widget.rawHistory.length; i++) {
var stateData = widget.rawHistory[i];
DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal();
DateTime endTime;
bool hiddenLine;
double value;
double previousValue = 0.0;
value = _parseToDouble(stateData["attributes"]["$attrName"]);
bool hiddenDot = (value == null);
if (hiddenDot && i > 0) {
previousValue = data[i-1].value ?? data[i-1].previousValue;
}
if (i < (widget.rawHistory.length - 1)) {
endTime = DateTime.tryParse(widget.rawHistory[i+1]["last_updated"])?.toLocal();
double nextValue = _parseToDouble(widget.rawHistory[i+1]["attributes"]["$attrName"]);
hiddenLine = (nextValue == null || hiddenDot);
} else {
hiddenLine = hiddenDot;
endTime = now;
}
data.add(EntityHistoryMoment(
value: value,
previousValue: previousValue,
hiddenDot: hiddenDot,
hiddenLine: hiddenLine,
state: stateData["state"],
startTime: startTime,
endTime: endTime,
id: i,
colorId: colorIdCounter
));
}
data.add(EntityHistoryMoment(
value: data.last.value,
previousValue: data.last.previousValue,
hiddenDot: data.last.hiddenDot,
hiddenLine: data.last.hiddenLine,
state: data.last.state,
startTime: now,
id: widget.rawHistory.length,
colorId: colorIdCounter
));
numericDataLists.addAll({attrName: data});
colorIdCounter += 1;
});
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
_selectedId = 0;
}
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
numericDataLists.forEach((attrName, dataList) {
TheLogger.debug(" adding ${dataList.length} data values");
result.add(
new charts.Series<EntityHistoryMoment, DateTime>(
id: "value",
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor("_", historyMoment.colorId),
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
if (historyMoment.hiddenDot) {
return 0.0;
} else if (historyMoment.id == _selectedId) {
return 5.0;
} else {
return 1.0;
}
},
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => historyMoment.hiddenLine ? 0.0 : 2.0,
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
data: dataList,
/*domainLowerBoundFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time.subtract(Duration(hours: 1)),
domainUpperBoundFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time.add(Duration(hours: 1)),*/
)
);
});
result.add(
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'state',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
// No measure values are needed for symbol annotations.
measureFn: (_, __) => null,
data: numericDataLists[numericDataLists.keys.first],
)
// Configure our custom symbol annotation renderer for this series.
..setAttribute(charts.rendererIdKey, 'stateBars')
// Optional radius for the annotation shape. If not specified, this will
// default to the same radius as the points.
//..setAttribute(charts.boundsLineRadiusPxKey, 3.5)
);
return result;
}
void _selectPrev() {
if (_selectedId > 0) {
setState(() {
_selectedId -= 1;
});
}
}
void _selectNext() {
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
setState(() {
_selectedId += 1;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {
final selectedDatum = model.selectedDatum;
int selectedId;
if (selectedDatum.isNotEmpty) {
selectedId = selectedDatum.first.datum.id;
setState(() {
_selectedId = selectedId;
});
} else {
setState(() {
});
}
}
}

View File

@ -0,0 +1,134 @@
part of '../../main.dart';
class EntityHistoryWidgetType {
static const int simple = 0;
static const int numericState = 1;
static const int numericAttributes = 2;
}
class EntityHistoryConfig {
final int chartType;
final List<String> numericAttributesToShow;
final bool numericState;
EntityHistoryConfig({this.chartType, this.numericAttributesToShow, this.numericState: true});
}
class EntityHistoryWidget extends StatefulWidget {
final EntityHistoryConfig config;
const EntityHistoryWidget({Key key, @required this.config}) : super(key: key);
@override
_EntityHistoryWidgetState createState() {
return new _EntityHistoryWidgetState();
}
}
class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
List _history;
bool _needToUpdateHistory;
DateTime _historyLastUpdated;
@override
void initState() {
super.initState();
_needToUpdateHistory = true;
}
void _loadHistory(HomeAssistant ha, String entityId) {
DateTime now = DateTime.now();
if (_historyLastUpdated != null) {
TheLogger.debug("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
}
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
_historyLastUpdated = now;
ha.getHistory(entityId).then((history){
setState(() {
_history = history.isNotEmpty ? history[0] : [];
_needToUpdateHistory = false;
});
}).catchError((e) {
TheLogger.error("Error loading $entityId history: $e");
setState(() {
_history = [];
_needToUpdateHistory = false;
});
});
}
}
@override
Widget build(BuildContext context) {
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
final EntityModel entityModel = EntityModel.of(context);
final Entity entity = entityModel.entityWrapper.entity;
if (!_needToUpdateHistory) {
_needToUpdateHistory = true;
} else {
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
}
return _buildChart();
}
Widget _buildChart() {
List<Widget> children = [];
if (_history == null) {
children.add(
Text("Loading history...")
);
} else if (_history.isEmpty) {
children.add(
Text("No history")
);
} else {
children.add(
_selectChartWidget()
);
}
children.add(Divider());
return Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, Sizes.rowPadding),
child: Column(
children: children,
),
);
}
Widget _selectChartWidget() {
switch (widget.config.chartType) {
case EntityHistoryWidgetType.simple: {
return SimpleStateHistoryChartWidget(
rawHistory: _history,
);
}
case EntityHistoryWidgetType.numericState: {
return NumericStateHistoryChartWidget(
rawHistory: _history,
config: widget.config,
);
}
case EntityHistoryWidgetType.numericAttributes: {
return CombinedHistoryChartWidget(
rawHistory: _history,
config: widget.config,
);
}
default: {
TheLogger.debug(" Simple selected as default");
return SimpleStateHistoryChartWidget(
rawHistory: _history,
);
}
}
}
}

View File

@ -0,0 +1,25 @@
part of '../../main.dart';
class EntityHistoryMoment {
final DateTime startTime;
final DateTime endTime;
final double value;
final double previousValue;
final int id;
final int colorId;
final String state;
final bool hiddenDot;
final bool hiddenLine;
EntityHistoryMoment({
this.value,
this.previousValue,
this.hiddenDot,
this.hiddenLine,
this.state,
@required this.startTime,
this.endTime,
@required this.id,
this.colorId
});
}

View File

@ -0,0 +1,86 @@
part of '../../main.dart';
class HistoryControlWidget extends StatelessWidget {
final Function onPrevTap;
final Function onNextTap;
final DateTime selectedTimeStart;
final DateTime selectedTimeEnd;
final List<String> selectedStates;
final List<int> colorIndexes;
const HistoryControlWidget({Key key, this.onPrevTap, this.onNextTap, this.selectedTimeStart, this.selectedTimeEnd, this.selectedStates, @ required this.colorIndexes}) : super(key: key);
@override
Widget build(BuildContext context) {
if (selectedTimeStart != null) {
return
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.chevron_left),
padding: EdgeInsets.all(0.0),
iconSize: 40.0,
onPressed: onPrevTap,
),
Expanded(
child: Padding(
padding: EdgeInsets.only(right: 10.0),
child: _buildStates(),
),
),
_buildTime(),
IconButton(
icon: Icon(Icons.chevron_right),
padding: EdgeInsets.all(0.0),
iconSize: 40.0,
onPressed: onNextTap,
),
],
);
} else {
return Container(height: 48.0);
}
}
Widget _buildStates() {
List<Widget> children = [];
for (int i = 0; i < selectedStates.length; i++) {
children.add(
Text(
"${selectedStates[i] ?? '-'}",
textAlign: TextAlign.right,
style: TextStyle(
fontWeight: FontWeight.bold,
color: EntityColor.historyStateColor(selectedStates[i], colorIndexes[i]),
fontSize: 22.0
),
)
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: children,
);
}
Widget _buildTime() {
List<Widget> children = [];
children.add(
Text("${formatDate(selectedTimeStart, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,)
);
if (selectedTimeEnd != null) {
children.add(
Text("${formatDate(selectedTimeEnd, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,)
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
}

View File

@ -0,0 +1,160 @@
part of '../../main.dart';
class NumericStateHistoryChartWidget extends StatefulWidget {
final rawHistory;
final EntityHistoryConfig config;
const NumericStateHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _NumericStateHistoryChartWidgetState();
}
}
class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChartWidget> {
int _selectedId = -1;
List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory;
@override
Widget build(BuildContext context) {
_parsedHistory = _parseHistory();
DateTime selectedTime;
double selectedState;
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
selectedTime = _parsedHistory.first.data[_selectedId].startTime;
selectedState = _parsedHistory.first.data[_selectedId].value;
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
HistoryControlWidget(
selectedTimeStart: selectedTime,
selectedStates: ["${selectedState ?? '-'}"],
onPrevTap: () => _selectPrev(),
onNextTap: () => _selectNext(),
colorIndexes: [-1],
),
SizedBox(
height: 150.0,
child: charts.TimeSeriesChart(
_parsedHistory,
animate: false,
primaryMeasureAxis: new charts.NumericAxisSpec(
tickProviderSpec:
new charts.BasicNumericTickProviderSpec(zeroBound: false)),
dateTimeFactory: const charts.LocalDateTimeFactory(),
defaultRenderer: charts.LineRendererConfig(
includePoints: true
),
/*primaryMeasureAxis: charts.NumericAxisSpec(
renderSpec: charts.NoneRenderSpec()
),*/
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
changedListener: (model) => _onSelectionChanged(model),
)
],
),
)
],
);
}
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
List<EntityHistoryMoment> data = [];
DateTime now = DateTime.now();
for (var i = 0; i < widget.rawHistory.length; i++) {
var stateData = widget.rawHistory[i];
DateTime time = DateTime.tryParse(stateData["last_updated"])?.toLocal();
double value = double.tryParse(stateData["state"]);
double previousValue = 0.0;
bool hiddenDot = (value == null);
bool hiddenLine;
if (hiddenDot && i > 0) {
previousValue = data[i-1].value ?? data[i-1].previousValue;
}
if (i < (widget.rawHistory.length - 1)) {
double nextValue = double.tryParse(widget.rawHistory[i+1]["state"]);
hiddenLine = (nextValue == null || hiddenDot);
} else {
hiddenLine = hiddenDot;
}
data.add(EntityHistoryMoment(
value: value,
previousValue: previousValue,
hiddenDot: hiddenDot,
hiddenLine: hiddenLine,
startTime: time,
id: i
));
}
data.add(EntityHistoryMoment(
value: data.last.value,
previousValue: data.last.previousValue,
hiddenDot: data.last.hiddenDot,
hiddenLine: data.last.hiddenLine,
startTime: now,
id: widget.rawHistory.length
));
if (_selectedId == -1) {
_selectedId = 0;
}
return [
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(EntityState.on, -1),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
data: data,
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => historyMoment.hiddenLine ? 0.0 : 2.0,
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
if (historyMoment.hiddenDot) {
return 0.0;
} else if (historyMoment.id == _selectedId) {
return 5.0;
} else {
return 1.0;
}
},
)
];
}
void _selectPrev() {
if (_selectedId > 0) {
setState(() {
_selectedId -= 1;
});
}
}
void _selectNext() {
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
setState(() {
_selectedId += 1;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {
final selectedDatum = model.selectedDatum;
int selectedId;
if (selectedDatum.isNotEmpty) {
selectedId = selectedDatum.first.datum.id;
setState(() {
_selectedId = selectedId;
});
} else {
setState(() {
});
}
}
}

View File

@ -0,0 +1,176 @@
part of '../../main.dart';
class SimpleStateHistoryChartWidget extends StatefulWidget {
final rawHistory;
const SimpleStateHistoryChartWidget({Key key, this.rawHistory}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _SimpleStateHistoryChartWidgetState();
}
}
class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartWidget> {
int _selectedId = -1;
List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory;
@override
Widget build(BuildContext context) {
_parsedHistory = _parseHistory();
DateTime selectedTimeStart;
DateTime selectedTimeEnd;
String selectedState;
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
selectedTimeStart = _parsedHistory.first.data[_selectedId].startTime;
selectedTimeEnd = _parsedHistory.first.data[_selectedId].endTime;
selectedState = _parsedHistory.first.data[_selectedId].state;
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
HistoryControlWidget(
selectedTimeStart: selectedTimeStart,
selectedTimeEnd: selectedTimeEnd,
selectedStates: [selectedState],
onPrevTap: () => _selectPrev(),
onNextTap: () => _selectNext(),
colorIndexes: [_parsedHistory.first.data[_selectedId].colorId],
),
SizedBox(
height: 70.0,
child: charts.TimeSeriesChart(
_parsedHistory,
animate: false,
dateTimeFactory: const charts.LocalDateTimeFactory(),
primaryMeasureAxis: charts.NumericAxisSpec(
renderSpec: charts.NoneRenderSpec()
),
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
changedListener: (model) => _onSelectionChanged(model),
)
],
customSeriesRenderers: [
new charts.PointRendererConfig(
// ID used to link series to this renderer.
customRendererId: 'startValuePoints'),
new charts.PointRendererConfig(
// ID used to link series to this renderer.
customRendererId: 'endValuePoints')
],
),
)
],
);
}
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
List<EntityHistoryMoment> data = [];
DateTime now = DateTime.now();
Map<String, int> cachedStates = {};
for (var i = 0; i < widget.rawHistory.length; i++) {
var stateData = widget.rawHistory[i];
DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal();
DateTime endTime;
if (i < (widget.rawHistory.length - 1)) {
endTime = DateTime.tryParse(widget.rawHistory[i+1]["last_updated"])?.toLocal();
} else {
endTime = now;
}
if (cachedStates[stateData["state"]] == null) {
cachedStates.addAll({"${stateData["state"]}": cachedStates.length});
}
data.add(EntityHistoryMoment(
state: stateData["state"],
startTime: startTime,
endTime: endTime,
id: i,
colorId: cachedStates[stateData["state"]]
));
}
data.add(EntityHistoryMoment(
state: data.last.state,
startTime: now,
id: widget.rawHistory.length,
colorId: data.last.colorId
));
if (_selectedId == -1) {
_selectedId = 0;
}
return [
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
),
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
)..setAttribute(charts.rendererIdKey, 'startValuePoints'),
new charts.Series<EntityHistoryMoment, DateTime>(
id: 'State',
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
data: data,
)..setAttribute(charts.rendererIdKey, 'endValuePoints')
];
}
void _selectPrev() {
if (_selectedId > 0) {
setState(() {
_selectedId -= 1;
});
}
}
void _selectNext() {
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
setState(() {
_selectedId += 1;
});
}
}
void _onSelectionChanged(charts.SelectionModel model) {
final selectedDatum = model.selectedDatum;
int selectedId;
if ((selectedDatum.isNotEmpty) &&(selectedDatum.first.datum.endTime != null)) {
selectedId = selectedDatum.first.datum.id;
setState(() {
_selectedId = selectedId;
});
} else {
setState(() {
});
}
}
}
/*
class SimpleEntityStateHistoryMoment {
final DateTime startTime;
final DateTime endTime;
final String state;
final int id;
final int colorId;
SimpleEntityStateHistoryMoment(this.state, this.startTime, this.endTime, this.id, this.colorId);
}*/

View File

@ -0,0 +1,42 @@
part of '../main.dart';
class EntityModel extends InheritedWidget {
const EntityModel({
Key key,
@required this.entityWrapper,
@required this.handleTap,
@required Widget child,
}) : super(key: key, child: child);
final EntityWrapper entityWrapper;
final bool handleTap;
static EntityModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(EntityModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}
class HomeAssistantModel extends InheritedWidget {
const HomeAssistantModel({
Key key,
@required this.homeAssistant,
@required Widget child,
}) : super(key: key, child: child);
final HomeAssistant homeAssistant;
static HomeAssistantModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}

View File

@ -0,0 +1,27 @@
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

@ -0,0 +1,52 @@
part of '../../main.dart';
class ClimateStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity;
String targetTemp = "-";
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
targetTemp = "${entity.temperature}";
} else if ((entity.supportTargetTemperatureLow) &&
(entity.targetLow != null)) {
targetTemp = "${entity.targetLow}";
if ((entity.supportTargetTemperatureHigh) &&
(entity.targetHigh != null)) {
targetTemp += " - ${entity.targetHigh}";
}
}
return Padding(
padding: EdgeInsets.fromLTRB(
0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Text("${entity.state}",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: Sizes.stateFontSize,
)),
Text(" $targetTemp",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
))
],
),
entity.attributes["current_temperature"] != null ?
Text("Currently: ${entity.attributes["current_temperature"]}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
color: Colors.black45)
) :
Container(height: 0.0,)
],
));
}
}

View File

@ -0,0 +1,65 @@
part of '../../main.dart';
class CoverStateWidget extends StatelessWidget {
void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "open_cover", entity.entityId, null));
}
void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "close_cover", entity.entityId, null));
}
void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "stop_cover", entity.entityId, null));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final CoverEntity entity = entityModel.entityWrapper.entity;
List<Widget> buttons = [];
if (entity.supportOpen) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"),
size: Sizes.iconSize,
),
onPressed: entity.canBeOpened ? () => _open(entity) : null));
} else {
buttons.add(Container(
width: Sizes.iconSize + 20.0,
));
}
if (entity.supportStop) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:stop"),
size: Sizes.iconSize,
),
onPressed: () => _stop(entity)));
} else {
buttons.add(Container(
width: Sizes.iconSize + 20.0,
));
}
if (entity.supportClose) {
buttons.add(IconButton(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"),
size: Sizes.iconSize,
),
onPressed: entity.canBeClosed ? () => _close(entity) : null));
} else {
buttons.add(Container(
width: Sizes.iconSize + 20.0,
));
}
return Row(
children: buttons,
);
}
}

View File

@ -0,0 +1,75 @@
part of '../../main.dart';
class DateTimeStateWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final DateTimeEntity entity = entityModel.entityWrapper.entity;
return Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Text("${entity.formattedState}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
)),
onTap: () => _handleStateTap(context, entity),
));
}
void _handleStateTap(BuildContext context, DateTimeEntity entity) {
if (entity.hasDate) {
_showDatePicker(context, entity).then((date) {
if (date != null) {
if (entity.hasTime) {
_showTimePicker(context, entity).then((time) {
entity.setNewState({
"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}",
"time":
"${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [
HH,
':',
nn
])}"
});
});
} else {
entity.setNewState({
"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"
});
}
}
});
} else if (entity.hasTime) {
_showTimePicker(context, entity).then((time) {
if (time != null) {
entity.setNewState({
"time":
"${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [
HH,
':',
nn
])}"
});
}
});
} else {
TheLogger.warning( "${entity.entityId} has no date and no time");
}
}
Future _showDatePicker(BuildContext context, DateTimeEntity entity) {
return showDatePicker(
context: context,
initialDate: entity.dateTimeState,
firstDate: DateTime(1970),
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
);
}
Future _showTimePicker(BuildContext context, DateTimeEntity entity) {
return showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(entity.dateTimeState));
}
}

View File

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

View File

@ -0,0 +1,49 @@
part of '../../main.dart';
class SelectStateWidget extends StatefulWidget {
SelectStateWidget({Key key}) : super(key: key);
@override
_SelectStateWidgetState createState() => _SelectStateWidgetState();
}
class _SelectStateWidgetState extends State<SelectStateWidget> {
void setNewState(domain, entityId, newValue) {
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
{"option": "$newValue"}));
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final SelectEntity entity = entityModel.entityWrapper.entity;
Widget ctrl;
if (entity.listOptions.isNotEmpty) {
ctrl = DropdownButton<String>(
value: entity.state,
isExpanded: true,
items: entity.listOptions.map((String value) {
return new DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (_) {
setNewState(entity.domain, entity.entityId,_);
},
);
} else {
ctrl = Text('---');
}
return Flexible(
flex: 2,
fit: FlexFit.tight,
//width: Entity.INPUT_WIDTH,
child: ctrl,
);
}
}

View File

@ -0,0 +1,37 @@
part of '../../main.dart';
class SimpleEntityState extends StatelessWidget {
final bool expanded;
final TextAlign textAlign;
final EdgeInsetsGeometry padding;
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);
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
Widget result = Padding(
padding: padding,
child: Text(
"${entityModel.entityWrapper.entity.state} ${entityModel.entityWrapper.entity.unitOfMeasurement}",
textAlign: textAlign,
maxLines: 10,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
)
)
);
if (expanded) {
return Flexible(
fit: FlexFit.tight,
flex: 2,
child: result,
);
} else {
return result;
}
}
}

View File

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

View File

@ -0,0 +1,100 @@
part of '../../main.dart';
class TextInputStateWidget extends StatefulWidget {
TextInputStateWidget({Key key}) : super(key: key);
@override
_TextInputStateWidgetState createState() => _TextInputStateWidgetState();
}
class _TextInputStateWidgetState extends State<TextInputStateWidget> {
String _tmpValue;
String _entityState;
String _entityDomain;
String _entityId;
int _minLength;
int _maxLength;
FocusNode _focusNode = FocusNode();
bool validValue = false;
@override
void initState() {
super.initState();
_focusNode.addListener(_focusListener);
}
void setNewState(newValue, domain, entityId) {
if (validate(newValue, _minLength, _maxLength)) {
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
{"value": "$newValue"}));
} else {
setState(() {
_tmpValue = _entityState;
});
}
}
bool validate(newValue, minLength, maxLength) {
if (newValue is String) {
validValue = (newValue.length >= minLength) &&
(maxLength == -1 ||
(newValue.length <= maxLength));
} else {
validValue = true;
}
return validValue;
}
void _focusListener() {
if (!_focusNode.hasFocus && (_tmpValue != _entityState)) {
setNewState(_tmpValue, _entityDomain, _entityId);
}
}
@override
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final TextEntity entity = entityModel.entityWrapper.entity;
_entityState = entity.state;
_entityDomain = entity.domain;
_entityId = entity.entityId;
_minLength = entity.valueMinLength;
_maxLength = entity.valueMaxLength;
if (!_focusNode.hasFocus && (_tmpValue != entity.state)) {
_tmpValue = entity.state;
}
if (entity.isTextField || entity.isPasswordField) {
return Flexible(
fit: FlexFit.tight,
flex: 2,
//width: Entity.INPUT_WIDTH,
child: TextField(
focusNode: _focusNode,
obscureText: entity.isPasswordField,
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _tmpValue,
selection:
new TextSelection.collapsed(offset: _tmpValue.length)
)
),
onChanged: (value) {
_tmpValue = value;
}),
);
} else {
TheLogger.warning( "Unsupported input mode for ${entity.entityId}");
return SimpleEntityState();
}
}
@override
void dispose() {
_focusNode.removeListener(_focusListener);
_focusNode.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,608 @@
part of 'main.dart';
class HomeAssistant {
String _webSocketAPIEndpoint;
String _password;
String _authType;
bool _useLovelace = false;
IOWebSocketChannel _hassioChannel;
SendMessageQueue _messageQueue;
int _currentMessageId = 0;
int _statesMessageId = 0;
int _servicesMessageId = 0;
int _subscriptionMessageId = 0;
int _configMessageId = 0;
int _userInfoMessageId = 0;
int _lovelaceMessageId = 0;
EntityCollection entities;
HomeAssistantUI ui;
Map _instanceConfig = {};
String _userName;
Map _rawLovelaceData;
Completer _fetchCompleter;
Completer _statesCompleter;
Completer _servicesCompleter;
Completer _lovelaceCompleter;
Completer _configCompleter;
Completer _connectionCompleter;
Completer _userInfoCompleter;
Timer _connectionTimer;
Timer _fetchTimer;
bool autoReconnect = false;
StreamSubscription _socketSubscription;
int messageExpirationTime = 30; //seconds
Duration fetchTimeout = Duration(seconds: 30);
Duration connectTimeout = Duration(seconds: 15);
String get locationName {
if (_useLovelace) {
return ui?.title ?? "";
} else {
return _instanceConfig["location_name"] ?? "";
}
}
String get userName => _userName ?? locationName;
String get userAvatarText => userName.length > 0 ? userName[0] : "";
//int get viewsCount => entities.views.length ?? 0;
HomeAssistant() {
entities = EntityCollection();
_messageQueue = SendMessageQueue(messageExpirationTime);
}
void updateSettings(String url, String password, String authType, bool useLovelace) {
_webSocketAPIEndpoint = url;
_password = password;
_authType = authType;
_useLovelace = useLovelace;
TheLogger.debug( "Use lovelace is $_useLovelace");
}
Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
TheLogger.warning("Previous fetch is not complited");
} else {
_fetchCompleter = new Completer();
_fetchTimer = Timer(fetchTimeout, () {
TheLogger.error( "Data fetching timeout");
disconnect().then((_) {
_completeFetching({
"errorCode": 9,
"errorMessage": "Couldn't get data from server"
});
});
});
_connection().then((r) {
_getData();
}).catchError((e) {
_completeFetching(e);
});
}
return _fetchCompleter.future;
}
disconnect() async {
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
onTimeout: () => TheLogger.debug( "Socket sink closed")
);
await _socketSubscription.cancel();
_hassioChannel = null;
}
}
Future _connection() {
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
TheLogger.debug("Previous connection is not complited");
} else {
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
_connectionCompleter = new Completer();
autoReconnect = false;
disconnect().then((_){
TheLogger.debug( "Socket connecting...");
_connectionTimer = Timer(connectTimeout, () {
TheLogger.error( "Socket connection timeout");
_handleSocketError(null);
});
if (_socketSubscription != null) {
_socketSubscription.cancel();
}
_hassioChannel = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
_socketSubscription = _hassioChannel.stream.listen(
(message) => _handleMessage(message),
cancelOnError: true,
onDone: () => _handleSocketClose(),
onError: (e) => _handleSocketError(e)
);
});
} else {
_completeConnecting(null);
}
}
return _connectionCompleter.future;
}
void _handleSocketClose() {
TheLogger.debug("Socket disconnected. Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
}
}
void _handleSocketError(e) {
TheLogger.error("Socket stream Error: $e");
TheLogger.debug("Automatic reconnect is $autoReconnect");
if (autoReconnect) {
_reconnect();
} else {
disconnect().then((_) {
_completeConnecting({
"errorCode": 1,
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
});
});
}
}
void _reconnect() {
disconnect().then((_) {
_connection().catchError((e){
_completeConnecting(e);
});
});
}
_getData() async {
List<Future> futures = [];
futures.add(_getStates());
if (_useLovelace) {
futures.add(_getLovelace());
}
futures.add(_getConfig());
futures.add(_getServices());
futures.add(_getUserInfo());
try {
await Future.wait(futures);
_createUI();
_completeFetching(null);
} catch (error) {
_completeFetching(error);
}
}
void _completeFetching(error) {
_fetchTimer.cancel();
_completeConnecting(error);
if (!_fetchCompleter.isCompleted) {
if (error != null) {
_fetchCompleter.completeError(error);
} else {
autoReconnect = true;
TheLogger.debug( "Fetch complete successful");
_fetchCompleter.complete();
}
}
}
void _completeConnecting(error) {
_connectionTimer.cancel();
if (!_connectionCompleter.isCompleted) {
if (error != null) {
_connectionCompleter.completeError(error);
} else {
_connectionCompleter.complete();
}
} else if (error != null) {
if (error is Error) {
eventBus.fire(ShowErrorEvent(error.toString(), 12));
} else {
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
}
}
}
_handleMessage(String message) {
var data = json.decode(message);
if (data["type"] == "auth_required") {
_sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}');
} else if (data["type"] == "auth_ok") {
_completeConnecting(null);
_sendSubscribe();
} else if (data["type"] == "auth_invalid") {
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
TheLogger.debug("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
if (data["id"] == _configMessageId) {
_parseConfig(data);
} else if (data["id"] == _statesMessageId) {
_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") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
TheLogger.debug("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
_handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) {
TheLogger.warning("Unhandled event type: ${data["event"]["event_type"]}");
} else {
TheLogger.error("Event is null: $message");
}
} else {
TheLogger.warning("Unknown message type: $message");
}
}
void _sendSubscribe() {
_incrementMessageId();
_subscriptionMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
}
Future _getConfig() {
_configCompleter = new Completer();
_incrementMessageId();
_configMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
return _configCompleter.future;
}
Future _getStates() {
_statesCompleter = new Completer();
_incrementMessageId();
_statesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
return _statesCompleter.future;
}
Future _getLovelace() {
_lovelaceCompleter = new Completer();
_incrementMessageId();
_lovelaceMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false);
return _lovelaceCompleter.future;
}
Future _getUserInfo() {
_userInfoCompleter = new Completer();
_incrementMessageId();
_userInfoMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
return _userInfoCompleter.future;
}
Future _getServices() {
_servicesCompleter = new Completer();
_incrementMessageId();
_servicesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
return _servicesCompleter.future;
}
_incrementMessageId() {
_currentMessageId += 1;
}
void _sendAuthMessageRaw(String message) {
TheLogger.debug( "[Sending] ==> auth request");
_hassioChannel.sink.add(message);
}
_sendMessageRaw(String message, bool queued) {
var sendCompleter = Completer();
if (queued) _messageQueue.add(message);
_connection().then((r) {
_messageQueue.getActualMessages().forEach((message){
TheLogger.debug( "[Sending queued] ==> $message");
_hassioChannel.sink.add(message);
});
if (!queued) {
TheLogger.debug( "[Sending] ==> $message");
_hassioChannel.sink.add(message);
}
sendCompleter.complete();
}).catchError((e){
sendCompleter.completeError(e);
});
return sendCompleter.future;
}
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
_incrementMessageId();
String message = "";
if (entityId != null) {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
if (additionalParams != null) {
additionalParams.forEach((name, value) {
if ((value is double) || (value is int) || (value is List)) {
message += ', "$name" : $value';
} else {
message += ', "$name" : "$value"';
}
});
}
message += '}}';
} else {
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"';
if (additionalParams != null && additionalParams.isNotEmpty) {
message += ', "service_data": {';
bool first = true;
additionalParams.forEach((name, value) {
if (!first) {
message += ', ';
}
if ((value is double) || (value is int) || (value is List)) {
message += '"$name" : $value';
} else {
message += '"$name" : "$value"';
}
first = false;
});
message += '}';
}
message += '}';
}
return _sendMessageRaw(message, true);
}
void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}");
Map data = Map.from(eventData);
entities.updateState(data);
eventBus.fire(new StateChangedEvent(data["entity_id"], null));
}
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() {
TheLogger.debug("--Title: ${_rawLovelaceData["title"]}");
ui.title = _rawLovelaceData["title"];
int viewCounter = 0;
TheLogger.debug("--Views count: ${_rawLovelaceData['views'].length}");
_rawLovelaceData["views"].forEach((rawView){
TheLogger.debug("----view id: ${rawView['id']}");
HAView view = HAView(
count: viewCounter,
id: "${rawView['id']}",
name: rawView['title'],
iconName: rawView['icon']
);
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
ui.views.add(
view
);
viewCounter += 1;
});
}
List<HACard> _createLovelaceCards(List rawCards) {
List<HACard> result = [];
rawCards.forEach((rawCard){
if (rawCard["cards"] != null) {
result.addAll(_createLovelaceCards(rawCard["cards"]));
} else {
HACard card = HACard(
id: "card",
name: rawCard["title"],
type: rawCard['type'],
columnsCount: rawCard['columns'] ?? 4,
showName: rawCard['show_name'] ?? true,
showState: rawCard['show_state'] ?? true,
);
rawCard["entities"]?.forEach((rawEntity) {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
}
} else {
if (entities.isExist(rawEntity["entity"])) {
Entity e = entities.get(rawEntity["entity"]);
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(
EntityWrapper(
entity: e,
displayName: rawEntity["name"],
icon: rawEntity["icon"],
tapAction: tapAction,
holdAction: holdAction,
tapActionService: rawEntity["service"],
tapActionServiceData: rawEntity["service_data"] ?? {"entity_id": e.entityId}
)
);
}
}
});
if (rawCard["entity"] != null) {
var en = rawCard["entity"];
if (en is String) {
if (entities.isExist(en)) {
card.linkedEntity = EntityWrapper(entity: entities.get(en));
}
} else {
if (entities.isExist(en["entity"])) {
card.linkedEntity = EntityWrapper(
entity: entities.get(en["entity"]),
icon: en["icon"],
displayName: en["name"]
);
}
}
}
result.add(card);
}
});
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() {
ui = HomeAssistantUI();
if ((_useLovelace) && (_rawLovelaceData != null)) {
TheLogger.debug("Creating Lovelace UI");
_parseLovelace();
} else {
TheLogger.debug("Creating group-based UI");
int viewCounter = 0;
if (!entities.hasDefaultView) {
HAView view = HAView(
count: viewCounter,
id: "group.default_view",
name: "Home",
childEntities: entities.filterEntitiesForDefaultView()
);
ui.views.add(
view
);
viewCounter += 1;
}
entities.viewEntities.forEach((viewEntity) {
HAView view = HAView(
count: viewCounter,
id: viewEntity.entityId,
name: viewEntity.displayName,
childEntities: viewEntity.childEntities
);
view.linkedEntity = viewEntity;
ui.views.add(
view
);
viewCounter += 1;
});
}
}
Widget buildViews(BuildContext context, bool lovelace) {
return ui.build(context);
}
Future<List> getHistory(String entityId) async {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
TheLogger.debug("[Sending] ==> $url");
http.Response historyResponse;
if (_authType == "access_token") {
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_password",
"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);
if (history is List) {
TheLogger.debug( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];
}
}
}
class SendMessageQueue {
int _messageTimeout;
List<HAMessage> _queue = [];
SendMessageQueue(this._messageTimeout);
void add(String message) {
_queue.add(HAMessage(_messageTimeout, message));
}
List<String> getActualMessages() {
_queue.removeWhere((item) => item.isExpired());
List<String> result = [];
_queue.forEach((haMessage){
result.add(haMessage.message);
});
this.clear();
return result;
}
void clear() {
_queue.clear();
}
}
class HAMessage {
DateTime _timeStamp;
int _messageTimeout;
String message;
HAMessage(this._messageTimeout, this.message) {
_timeStamp = DateTime.now();
}
bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
}
}

View File

@ -10,11 +10,7 @@ class LogViewPage extends StatefulWidget {
}
class _LogViewPageState extends State<LogViewPage> {
String _hassioDomain = "";
String _hassioPort = "8123";
String _hassioPassword = "";
String _socketProtocol = "wss";
String _authType = "access_token";
String _logData;
@override
void initState() {
@ -23,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> {
}
_loadLog() async {
//
_logData = TheLogger.getLog();
}
@override
@ -36,12 +32,19 @@ class _LogViewPageState extends State<LogViewPage> {
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text(widget.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.content_copy),
onPressed: () {
Clipboard.setData(new ClipboardData(text: _logData));
},
)
],
),
body: TextField(
maxLines: null,
controller: TextEditingController(
text: TheLogger.getLog()
text: _logData
),
)
);

File diff suppressed because it is too large Load Diff

View File

@ -1,362 +1,5 @@
part of 'main.dart';
class StateChangedEvent {
String entityId;
StateChangedEvent(this.entityId);
}
class SettingsChangedEvent {
bool reconnect;
SettingsChangedEvent(this.reconnect);
}
class HassioDataModel {
String _hassioAPIEndpoint;
String _hassioPassword;
String _hassioAuthType;
IOWebSocketChannel _hassioChannel;
int _currentMessageId = 0;
int _statesMessageId = 0;
int _servicesMessageId = 0;
int _subscriptionMessageId = 0;
int _configMessageId = 0;
Map _entitiesData = {};
Map _servicesData = {};
Map _uiStructure = {};
Map _instanceConfig = {};
Completer _fetchCompleter;
Completer _statesCompleter;
Completer _servicesCompleter;
Completer _configCompleter;
Timer _fetchingTimer;
List _topBadgeDomains = ["alarm_control_panel", "binary_sensor", "device_tracker", "updater", "sun", "timer", "sensor"];
Map get entities => _entitiesData;
Map get services => _servicesData;
Map get uiStructure => _uiStructure;
Map get instanceConfig => _instanceConfig;
HassioDataModel(String url, String password, String authType) {
_hassioAPIEndpoint = url;
_hassioPassword = password;
_hassioAuthType = authType;
}
Future fetch() {
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
TheLogger.log("Warning","Previous fetch is not complited");
} else {
//TODO: Fetch timeout timer. Should be removed after #21 fix
_fetchingTimer = Timer(Duration(seconds: 15), () {
closeConnection();
_fetchCompleter.completeError({"errorCode" : 1,"errorMessage": "Connection timeout"});
});
_fetchCompleter = new Completer();
_reConnectSocket().then((r) {
_getData();
}).catchError((e) {
_finishFetching(e);
});
}
return _fetchCompleter.future;
}
closeConnection() {
if (_hassioChannel?.closeCode == null) {
_hassioChannel?.sink?.close();
}
_hassioChannel = null;
}
Future _reConnectSocket() {
var _connectionCompleter = new Completer();
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
TheLogger.log("Debug","Socket connecting...");
_hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint);
_hassioChannel.stream.handleError((e) {
TheLogger.log("Error","Unhandled socket error: ${e.toString()}");
});
_hassioChannel.stream.listen((message) =>
_handleMessage(_connectionCompleter, message));
} else {
_connectionCompleter.complete();
}
return _connectionCompleter.future;
}
_getData() {
_getConfig().then((result) {
_getStates().then((result) {
_getServices().then((result) {
_finishFetching(null);
}).catchError((e) {
_finishFetching(e);
});
}).catchError((e) {
_finishFetching(e);
});
}).catchError((e) {
_finishFetching(e);
});
}
_finishFetching(error) {
_fetchingTimer.cancel();
if (error != null) {
_fetchCompleter.completeError(error);
} else {
_fetchCompleter.complete();
}
}
_handleMessage(Completer connectionCompleter, String message) {
var data = json.decode(message);
TheLogger.log("Debug","[Received] => Message type: ${data['type']}");
if (data["type"] == "auth_required") {
_sendMessageRaw('{"type": "auth","$_hassioAuthType": "$_hassioPassword"}');
} else if (data["type"] == "auth_ok") {
_sendSubscribe();
connectionCompleter.complete();
} else if (data["type"] == "auth_invalid") {
connectionCompleter.completeError({"errorCode": 6, "errorMessage": "${data["message"]}"});
} else if (data["type"] == "result") {
if (data["id"] == _configMessageId) {
_parseConfig(data);
} else if (data["id"] == _statesMessageId) {
_parseEntities(data);
} else if (data["id"] == _servicesMessageId) {
_parseServices(data);
} else if (data["id"] == _currentMessageId) {
TheLogger.log("Debug","Request id:$_currentMessageId was successful");
}
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
_handleEntityStateChange(data["event"]["data"]);
} else if (data["event"] != null) {
TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}");
} else {
TheLogger.log("Error","Event is null: $message");
}
} else {
TheLogger.log("Warning","Unknown message type: $message");
}
}
void _sendSubscribe() {
_incrementMessageId();
_subscriptionMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}');
}
Future _getConfig() {
_configCompleter = new Completer();
_incrementMessageId();
_configMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}');
return _configCompleter.future;
}
Future _getStates() {
_statesCompleter = new Completer();
_incrementMessageId();
_statesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}');
return _statesCompleter.future;
}
Future _getServices() {
_servicesCompleter = new Completer();
_incrementMessageId();
_servicesMessageId = _currentMessageId;
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}');
return _servicesCompleter.future;
}
_incrementMessageId() {
_currentMessageId += 1;
}
_sendMessageRaw(String message) {
if (message.indexOf('"type": "auth"') > 0) {
TheLogger.log("Debug", "[Sending] ==> auth request");
} else {
TheLogger.log("Debug", "[Sending] ==> $message");
}
_hassioChannel.sink.add(message);
}
void _handleEntityStateChange(Map eventData) {
TheLogger.log("Debug", "Parsing new state for ${eventData['entity_id']}");
if (eventData["new_state"] == null) {
TheLogger.log("Error", "No new_state found");
} else {
var parsedEntityData = _parseEntity(eventData["new_state"]);
String entityId = parsedEntityData["entity_id"];
if (_entitiesData[entityId] == null) {
_entitiesData[entityId] = parsedEntityData;
} else {
_entitiesData[entityId].addAll(parsedEntityData);
}
eventBus.fire(new StateChangedEvent(eventData["entity_id"]));
}
}
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 _parseServices(response) {
if (response["success"] == false) {
_servicesCompleter.completeError({"errorCode": 4, "errorMessage": response["error"]["message"]});
return;
}
try {
Map data = response["result"];
Map result = {};
TheLogger.log("Debug","Parsing ${data.length} Home Assistant service domains");
data.forEach((domain, services) {
result[domain] = Map.from(services);
services.forEach((serviceName, serviceData) {
if (_entitiesData["$domain.$serviceName"] != null) {
result[domain].remove(serviceName);
}
});
});
_servicesData = result;
_servicesCompleter.complete();
} catch (e) {
//TODO hadle it properly
TheLogger.log("Error","Error parsing services. But they are not used :-)");
_servicesCompleter.complete();
}
}
void _parseEntities(response) async {
_entitiesData.clear();
_uiStructure.clear();
if (response["success"] == false) {
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
return;
}
List data = response["result"];
TheLogger.log("Debug","Parsing ${data.length} Home Assistant entities");
List<String> uiGroups = [];
data.forEach((entity) {
try {
var composedEntity = _parseEntity(entity);
if (composedEntity["attributes"] != null) {
if ((composedEntity["domain"] == "group") &&
(composedEntity["attributes"]["view"] == true)) {
uiGroups.add(composedEntity["entity_id"]);
}
}
_entitiesData[entity["entity_id"]] = composedEntity;
} catch (error) {
TheLogger.log("Error","Error parsing entity: ${entity['entity_id']}");
}
});
//Gethering information for UI
TheLogger.log("Debug","Gethering views");
int viewCounter = 0;
uiGroups.forEach((viewId) { //Each view
try {
Map viewGroupStructure = {};
viewCounter += 1;
var viewGroup = _entitiesData[viewId];
if (viewGroup != null) {
viewGroupStructure["groups"] = {};
viewGroupStructure["state"] = "on";
viewGroupStructure["entity_id"] = viewGroup["entity_id"];
viewGroupStructure["badges"] = {"children": []};
viewGroupStructure["attributes"] = viewGroup["attributes"] != null ? {
"icon": viewGroup["attributes"]["icon"]
} : {"icon": "none"};
viewGroup["attributes"]["entity_id"].forEach((
entityId) { //Each entity or group in view
Map newGroup = {};
String domain = _entitiesData[entityId]["domain"];
if (domain != "group") {
if (_topBadgeDomains.contains(domain)) {
viewGroupStructure["badges"]["children"].add(entityId);
} else {
String autoGroupID = "$domain.$domain$viewCounter";
if (viewGroupStructure["groups"]["$autoGroupID"] == null) {
newGroup["entity_id"] = "$domain.$domain$viewCounter";
newGroup["friendly_name"] = "$domain";
newGroup["children"] = [];
newGroup["children"].add(entityId);
viewGroupStructure["groups"]["$autoGroupID"] =
Map.from(newGroup);
} else {
viewGroupStructure["groups"]["$autoGroupID"]["children"].add(
entityId);
}
}
} else {
newGroup["entity_id"] = entityId;
newGroup["friendly_name"] =
(_entitiesData[entityId]['attributes'] != null)
? (_entitiesData[entityId]['attributes']['friendly_name'] ??
"")
: "";
newGroup["children"] = List<String>();
_entitiesData[entityId]["attributes"]["entity_id"].forEach((
groupedEntityId) {
newGroup["children"].add(groupedEntityId);
});
viewGroupStructure["groups"]["$entityId"] = Map.from(newGroup);
}
});
}
_uiStructure[viewId.split(".")[1]] = viewGroupStructure;
} catch (error) {
TheLogger.log("Error","Error parsing view: $viewId");
}
});
_statesCompleter.complete();
}
Map _parseEntity(rawData) {
var composedEntity = Map.from(rawData);
String entityDomain = rawData["entity_id"].split(".")[0];
composedEntity["display_name"] = "${rawData["attributes"]!=null ? rawData["attributes"]["friendly_name"] ?? rawData["attributes"]["name"] : "_"}";
composedEntity["domain"] = entityDomain;
return composedEntity;
}
Future callService(String domain, String service, String entity_id) {
var sendCompleter = Completer();
//TODO: Send service call timeout timer. Should be removed after #21 fix
Timer _sendTimer = Timer(Duration(seconds: 7), () {
sendCompleter.completeError({"errorCode" : 8,"errorMessage": "Connection timeout"});
});
_reConnectSocket().then((r) {
_incrementMessageId();
_sendMessageRaw('{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entity_id"}}');
_sendTimer.cancel();
sendCompleter.complete();
}).catchError((e){
_sendTimer.cancel();
sendCompleter.completeError(e);
});
return sendCompleter.future;
}
}
class MaterialDesignIcons {
static Map _defaultIconsByDomains = {
"light": "mdi:lightbulb",
@ -371,7 +14,17 @@ class MaterialDesignIcons {
"input_number": "mdi:ray-vertex",
"input_select": "mdi:format-list-bulleted",
"input_text": "mdi:textbox",
"sun": "mdi:white-balance-sunny"
"sun": "mdi:white-balance-sunny",
"scene": "mdi:google-pages",
"media_player": "mdi:cast",
"climate": "mdi:thermostat",
"cover.open": "mdi:window-open",
"cover.closed": "mdi:window-closed",
"cover.closing": "mdi:window-open",
"cover.opening": "mdi:window-open",
"lock.locked": "mdi:lock",
"lock.unlocked": "mdi:lock-open",
"fan": "mdi:fan"
};
static Map _defaultIconsByDeviceClass = {
@ -423,7 +76,14 @@ class MaterialDesignIcons {
//"sensor.illuminance": "mdi:",
"sensor.temperature": "mdi:thermometer",
//"cover.window": "mdi:",
//"cover.garage": "mdi:",
"cover.garage.closed": "mdi:garage",
"cover.garage.open": "mdi:garage-open",
"cover.garage.opening": "mdi:garage-open",
"cover.garage.closing": "mdi:garage-open",
"cover.window.open": "mdi:window-open",
"cover.window.closed": "mdi:window-closed",
"cover.window.closing": "mdi:window-open",
"cover.window.opening": "mdi:window-open",
};
static Map _iconsDataMap = {
"mdi:access-point": 0xf002,
@ -3223,35 +2883,35 @@ class MaterialDesignIcons {
"mdi:blank": 0xf68c
};
static Widget createIconFromEntityData(Map data, double size, Color color) {
if ((data["attributes"] != null) && (data["attributes"]["entity_picture"] != null)) {
static Widget createIconWidgetFromEntityData(EntityWrapper data, double size, Color color) {
if (data == null) {
return null;
}
if (data.entity.entityPicture != null) {
if (homeAssistantWebHost != null) {
return CircleAvatar(
radius: size/2,
backgroundColor: Colors.white,
backgroundImage: CachedNetworkImageProvider(
"$homeAssistantWebHost${data["attributes"]["entity_picture"]}",
"$homeAssistantWebHost${data.entity.entityPicture}",
),
);
} else {
return Container(width: 0.0, height: 0.0);
}
} else {
String iconName = data["attributes"] != null
? data["attributes"]["icon"]
: null;
String iconName = data.icon;
int iconCode = 0;
if (iconName != null) {
if (iconName.length > 0) {
iconCode = getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data["entity_id"],
data["attributes"] != null
? data["attributes"]["device_class"]
: null, data["state"]); //
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
return Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
);
}
}
@ -3270,7 +2930,7 @@ class MaterialDesignIcons {
static int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
String domain = entityId.split(".")[0];
String iconNameByDomain = _defaultIconsByDomains[domain];
String iconNameByDomain = _defaultIconsByDomains["$domain.$state"] ?? _defaultIconsByDomains["$domain"];
String iconNameByDeviceClass;
if (deviceClass != null) {
iconNameByDeviceClass = _defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? _defaultIconsByDeviceClass["$domain.$deviceClass"];

220
lib/settings.page.dart Normal file
View File

@ -0,0 +1,220 @@
part of 'main.dart';
class ConnectionSettingsPage extends StatefulWidget {
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
final String title;
@override
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
}
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = "";
String _newHassioDomain = "";
String _hassioPort = "";
String _newHassioPort = "";
String _hassioPassword = "";
String _newHassioPassword = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _authType = "access_token";
String _newAuthType = "access_token";
bool _useLovelace = false;
bool _newUseLovelace = false;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
_authType = _newAuthType = prefs.getString("hassio-auth-type") ?? 'access_token';
try {
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? false;
} catch (e) {
_useLovelace = _newUseLovelace = false;
}
});
}
bool _checkConfigChanged() {
return ((_newHassioPassword != _hassioPassword) ||
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newAuthType != _authType) ||
(_newUseLovelace != _useLovelace));
}
_saveSettings() async {
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
_newHassioDomain = _newHassioDomain.split("//")[1];
}
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("hassio-domain", _newHassioDomain);
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-password", _newHassioPassword);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setString("hassio-auth-type", _newAuthType);
prefs.setBool("use-lovelace", _newUseLovelace);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text(widget.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: (){
if (_checkConfigChanged()) {
TheLogger.debug("Settings changed. Saving...");
_saveSettings().then((r) {
Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true));
});
} else {
TheLogger.debug("Settings was not changed");
Navigator.pop(context);
}
}
)
],
),
body: ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text(
"Connection settings",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
new Row(
children: [
Text("Use ssl (HTTPS)"),
Switch(
value: (_newSocketProtocol == "wss"),
onChanged: (value) {
setState(() {
_newSocketProtocol = value ? "wss" : "ws";
});
},
)
],
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioDomain,
selection:
new TextSelection.collapsed(offset: _newHassioDomain.length)
)
),
onChanged: (value) {
_newHassioDomain = value;
}
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant port (default is 8123)"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioPort,
selection:
new TextSelection.collapsed(offset: _newHassioPort.length)
)
),
onChanged: (value) {
_newHassioPort = value;
}
),
new Row(
mainAxisSize: MainAxisSize.min,
children: [
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(
decoration: InputDecoration(
labelText: _newAuthType == "access_token" ? "Access token" : "API password"
),
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _newHassioPassword,
selection:
new TextSelection.collapsed(offset: _newHassioPassword.length)
)
),
onChanged: (value) {
_newHassioPassword = value;
}
),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Text(
"UI",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
),
new Row(
children: [
Text("Use Lovelace UI"),
Switch(
value: _newUseLovelace,
onChanged: (value) {
setState(() {
_newUseLovelace = value;
});
},
)
],
),
],
),
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -1,135 +0,0 @@
part of 'main.dart';
class ConnectionSettingsPage extends StatefulWidget {
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
final String title;
@override
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
}
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = "";
String _hassioPort = "8123";
String _hassioPassword = "";
String _socketProtocol = "wss";
String _authType = "access_token";
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_hassioDomain = prefs.getString("hassio-domain");
_hassioPort = prefs.getString("hassio-port") ?? '8123';
_hassioPassword = prefs.getString("hassio-password");
_socketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
_authType = prefs.getString("hassio-auth-type") ?? 'access_token';
});
}
_saveSettings() async {
if (_hassioDomain.indexOf("http") == 0 && _hassioDomain.indexOf("//") > 0) {
_hassioDomain = _hassioDomain.split("//")[1];
}
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("hassio-domain", _hassioDomain);
prefs.setString("hassio-port", _hassioPort);
prefs.setString("hassio-password", _hassioPassword);
prefs.setString("hassio-protocol", _socketProtocol);
prefs.setString("hassio-res-protocol", _socketProtocol == "wss" ? "https" : "http");
prefs.setString("hassio-auth-type", _authType);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
_saveSettings().then((r){
Navigator.pop(context);
});
eventBus.fire(SettingsChangedEvent(true));
}),
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text(widget.title),
),
body: ListView(
padding: const EdgeInsets.all(20.0),
children: <Widget>[
new Row(
children: [
Text("HTTPS"),
Switch(
value: (_socketProtocol == "wss"),
onChanged: (value) {
setState(() {
_socketProtocol = value ? "wss" : "ws";
});
_saveSettings();
},
)
],
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
),
controller: TextEditingController(
text: _hassioDomain
),
onChanged: (value) {
_hassioDomain = value;
_saveSettings();
},
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant port"
),
controller: TextEditingController(
text: _hassioPort
),
onChanged: (value) {
_hassioPort = value;
_saveSettings();
},
),
new Row(
children: [
Text("Login with access token (HA >= 0.78.0)"),
Switch(
value: (_authType == "access_token"),
onChanged: (value) {
setState(() {
_authType = value ? "access_token" : "api_password";
});
_saveSettings();
},
)
],
),
new TextField(
decoration: InputDecoration(
labelText: _authType == "access_token" ? "Access token" : "API password"
),
controller: TextEditingController(
text: _hassioPassword
),
onChanged: (value) {
_hassioPassword = value;
_saveSettings();
},
)
],
),
);
}
}

View File

@ -0,0 +1,77 @@
part of '../main.dart';
class HACard {
List<EntityWrapper> entities = [];
EntityWrapper linkedEntity;
String name;
String id;
String type;
bool showName;
bool showState;
int columnsCount;
HACard({
this.name,
this.id,
this.linkedEntity,
this.columnsCount: 4,
this.showName: true,
this.showState: true,
@required this.type
});
Widget build(BuildContext context) {
switch (type) {
case CardType.entities: {
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,
);
}
}
}
}
}

View File

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

27
lib/ui_class/ui.dart Normal file
View File

@ -0,0 +1,27 @@
part of '../main.dart';
class HomeAssistantUI {
List<HAView> views;
String title;
HomeAssistantUI() {
views = [];
}
Widget build(BuildContext context) {
return TabBarView(
children: _buildViews(context)
);
}
List<Widget> _buildViews(BuildContext context) {
List<Widget> result = [];
views.forEach((view) {
result.add(
view.build(context)
);
});
return result;
}
}

View File

@ -0,0 +1,114 @@
part of '../main.dart';
class HAView {
List<HACard> cards = [];
List<Entity> badges = [];
Entity linkedEntity;
String name;
String id;
String iconName;
int count;
HAView({
this.name,
this.id,
this.count,
this.iconName,
List<Entity> childEntities
}) {
if (childEntities != null) {
_fillView(childEntities);
}
}
void _fillView(List<Entity> childEntities) {
List<HACard> autoGeneratedCards = [];
badges.addAll(childEntities.where((entity){ return entity.isBadge;}));
childEntities.where((entity){ return entity.domain == "media_player";}).forEach((e){
HACard card = HACard(
name: e.displayName,
id: e.entityId,
linkedEntity: EntityWrapper(entity: e),
type: "media-control"
);
cards.add(card);
});
childEntities.where((e){return (!e.isBadge && e.domain != "media_player");}).forEach((entity) {
if (!entity.isGroup) {
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) {
HACard card = HACard(
id: groupIdToAdd,
name: entity.domain,
type: "entities"
);
card.entities.add(EntityWrapper(entity: entity));
autoGeneratedCards.add(card);
} else {
autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(EntityWrapper(entity: entity));
}
} else {
HACard card = HACard(
name: entity.displayName,
id: entity.entityId,
linkedEntity: EntityWrapper(entity: entity),
type: "entities"
);
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
HACard mediaCard = HACard(
name: entity.displayName,
id: entity.entityId,
linkedEntity: EntityWrapper(entity: entity),
type: "media-control"
);
cards.add(mediaCard);
});
cards.add(card);
}
});
cards.addAll(autoGeneratedCards);
}
Widget buildTab() {
if (linkedEntity == null) {
if (iconName != null) {
return
Tab(
icon:
Icon(
MaterialDesignIcons.createIconDataFromIconName(
iconName ?? "mdi:home-assistant"),
size: 24.0,
)
);
} else {
return
Tab(
text: name.toUpperCase(),
);
}
} else {
if (linkedEntity.icon != null && linkedEntity.icon.length > 0) {
return Tab(
icon: Icon(
MaterialDesignIcons.createIconDataFromIconName(
linkedEntity.icon),
size: 24.0,
)
);
} else {
return Tab(
text: linkedEntity.displayName.toUpperCase(),
);
}
}
}
Widget build(BuildContext context) {
return ViewWidget(
view: this,
);
}
}

View File

@ -0,0 +1,25 @@
part of '../main.dart';
class CardHeaderWidget extends StatelessWidget {
final String name;
const CardHeaderWidget({Key key, this.name}) : super(key: key);
@override
Widget build(BuildContext context) {
var result;
if ((name != null) && (name.trim().length > 0)) {
result = new ListTile(
title: Text("$name",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
);
} else {
result = new Container(width: 0.0, height: 0.0);
}
return result;
}
}

View File

@ -0,0 +1,43 @@
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

@ -0,0 +1,52 @@
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

@ -0,0 +1,29 @@
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

@ -0,0 +1,53 @@
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;
}
}

96
lib/ui_widgets/view.dart Normal file
View File

@ -0,0 +1,96 @@
part of '../main.dart';
class ViewWidget extends StatefulWidget {
final HAView view;
const ViewWidget({
Key key,
this.view
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return ViewWidgetState();
}
}
class ViewWidgetState extends State<ViewWidget> {
StreamSubscription _refreshDataSubscription;
Completer _refreshCompleter;
@override
void initState() {
super.initState();
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
_refreshCompleter.complete();
}
});
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
color: Colors.amber,
child: ListView(
padding: EdgeInsets.all(0.0),
physics: const AlwaysScrollableScrollPhysics(),
children: _buildChildren(context),
),
onRefresh: () => _refreshData(),
);
}
List<Widget> _buildChildren(BuildContext context) {
List<Widget> result = [];
if (widget.view.badges.isNotEmpty) {
result.insert(0,
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 1.0,
children: _buildBadges(context),
)
);
}
widget.view.cards.forEach((HACard card){
result.add(
card.build(context)
);
});
return result;
}
List<Widget> _buildBadges(BuildContext context) {
List<Widget> result = [];
widget.view.badges.forEach((Entity entity) {
if (!entity.isHidden) {
result.add(entity.buildBadgeWidget(context));
}
});
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
void dispose() {
_refreshDataSubscription.cancel();
super.dispose();
}
}

99
lib/utils.class.dart Normal file
View File

@ -0,0 +1,99 @@
part of 'main.dart';
class TheLogger {
static List<String> _log = [];
static String getLog() {
String res = '';
_log.forEach((line) {
res += "$line\n";
});
return res;
}
static bool get isInDebugMode {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
static void error(String message) {
_writeToLog("Error", message);
}
static void warning(String message) {
_writeToLog("Warning", message);
}
static void debug(String message) {
_writeToLog("Debug", message);
}
static void _writeToLog(String level, String message) {
if (isInDebugMode) {
debugPrint('$message');
}
DateTime t = DateTime.now();
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
if (_log.length > 100) {
_log.removeAt(0);
}
}
}
class HAUtils {
static void launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
TheLogger.error( "Could not launch $url");
}
}
}
class StateChangedEvent {
String entityId;
String newState;
StateChangedEvent(this.entityId, this.newState);
}
class SettingsChangedEvent {
bool reconnect;
SettingsChangedEvent(this.reconnect);
}
class RefreshDataEvent {
RefreshDataEvent();
}
class RefreshDataFinishedEvent {
RefreshDataFinishedEvent();
}
class ServiceCallEvent {
String domain;
String service;
String entityId;
Map<String, dynamic> additionalParams;
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
}
class ShowEntityPageEvent {
Entity entity;
ShowEntityPageEvent(this.entity);
}
class ShowErrorEvent {
String text;
int errorCode;
ShowErrorEvent(this.text, this.errorCode);
}

View File

@ -1,13 +1,6 @@
# Generated by pub
# See https://www.dartlang.org/tools/pub/glossary#lockfile
packages:
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.32.4"
archive:
dependency: transitive
description:
@ -21,7 +14,7 @@ packages:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0"
version: "1.5.1"
async:
dependency: transitive
description:
@ -42,7 +35,7 @@ packages:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
version: "0.5.0+1"
charcode:
dependency: transitive
description:
@ -50,6 +43,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
charts_common:
dependency: transitive
description:
name: charts_common
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
charts_flutter:
dependency: "direct main"
description:
name: charts_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
collection:
dependency: transitive
description:
@ -71,13 +78,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.5"
dart_config:
dependency: transitive
description:
@ -87,6 +87,13 @@ packages:
url: "https://github.com/MarkOSullivan94/dart_config.git"
source: git
version: "0.5.0"
date_format:
dependency: "direct main"
description:
name: date_format
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
event_bus:
dependency: "direct main"
description:
@ -105,9 +112,16 @@ packages:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
flutter_launcher_icons:
version: "0.2.0+1"
flutter_colorpicker:
dependency: "direct main"
description:
name: flutter_colorpicker
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
url: "https://pub.dartlang.org"
@ -118,41 +132,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
front_end:
dependency: transitive
description:
name: front_end
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.7"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.3+3"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.3+17"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "0.12.0"
http_parser:
dependency: transitive
description:
@ -167,34 +153,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
io:
intl:
dependency: transitive
description:
name: io
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+1"
json_rpc_2:
dependency: transitive
description:
name: json_rpc_2
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
kernel:
dependency: transitive
description:
name: kernel
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
version: "0.15.7"
logging:
dependency: transitive
description:
@ -216,48 +181,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.6+2"
multi_server_socket:
dependency: transitive
description:
name: multi_server_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
package_info:
dependency: "direct main"
description:
name: package_info
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.2"
package_resolver:
dependency: transitive
description:
name: package_resolver
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
path:
dependency: transitive
description:
@ -278,21 +201,7 @@ packages:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
plugin:
dependency: transitive
description:
name: plugin
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+3"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.6"
version: "2.0.2"
progress_indicators:
dependency: "direct main"
description:
@ -300,74 +209,25 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.2"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0+1"
version: "2.0.1"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.3+3"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.8"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2+4"
version: "0.4.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.5"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.7"
source_span:
dependency: transitive
description:
@ -410,13 +270,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
test:
test_api:
dependency: transitive
description:
name: test
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "0.2.1"
typed_data:
dependency: transitive
description:
@ -424,13 +284,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
utf:
dependency: transitive
url_launcher:
dependency: "direct main"
description:
name: utf
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.0+5"
version: "4.0.1"
uuid:
dependency: transitive
description:
@ -445,22 +305,8 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
vm_service_client:
dependency: transitive
description:
name: vm_service_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.6"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+10"
web_socket_channel:
dependency: transitive
dependency: "direct main"
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
@ -472,7 +318,7 @@ packages:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.1"
version: "3.2.3"
yaml:
dependency: transitive
description:
@ -481,5 +327,5 @@ packages:
source: hosted
version: "2.1.15"
sdks:
dart: ">=2.0.0 <=2.1.0-dev.3.1.flutter-760a9690c2"
flutter: ">=0.1.4 <2.0.0"
dart: ">=2.0.0 <=2.1.0-dev.9.3.flutter-9c07fb64c4"
flutter: ">=0.5.6 <2.0.0"

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.1.1-alpha
version: 0.3.10+73
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -9,12 +9,15 @@ environment:
dependencies:
flutter:
sdk: flutter
web_socket_channel: any
shared_preferences: any
progress_indicators: ^0.1.2
event_bus: ^1.0.1
package_info: ^0.3.2
flutter_launcher_icons: ^0.6.1
cached_network_image: ^0.4.1
progress_indicators: any
event_bus: any
cached_network_image: any
url_launcher: any
date_format: any
flutter_colorpicker: any
charts_flutter: any
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
@ -23,6 +26,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: any
flutter_icons:
android: true

View File

@ -12,7 +12,7 @@ import 'package:hass_client/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(new HassClientApp());
await tester.pumpWidget(new HAClientApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);