Compare commits

...

638 Commits

Author SHA1 Message Date
139533d2ca 0.7.2 2019-11-01 14:00:13 +00:00
889682f771 Resolves #491 Lovelace badges parse issue 2019-11-01 13:54:35 +00:00
f16c98057f Location fixes 2019-11-01 13:44:51 +00:00
26ec807c25 Resolves #490 Prevent fused coarse location 2019-10-30 16:54:25 +00:00
45af6cbe3c Fix play_media call 2019-10-30 15:04:23 +00:00
5dd9cde12d Entity page fixes 2019-10-30 14:25:30 +00:00
472fb1d367 Merge pull request #489 from estevez-dev/hotfix/0.7.1
Hotfix/0.7.1
2019-10-29 19:47:33 +02:00
8b372fbc0b Merge branch 'master' into hotfix/0.7.1 2019-10-29 19:47:24 +02:00
40d72eb6e1 0.7.1 2019-10-29 17:46:16 +00:00
ced008a7c1 Resolves #486 Fix for very small screens 2019-10-29 17:44:18 +00:00
d1f652282a Merge pull request #488 from estevez-dev/pre-release/0.7.1
Pre release/0.7.1
2019-10-29 19:35:10 +02:00
f656528d5b 711 2019-10-28 20:42:05 +00:00
bcdb2a648c Fix entity page linking after app resumed 2019-10-28 20:40:30 +00:00
8a78745aa7 State change event log 2019-10-28 20:34:43 +00:00
2a3eaabbe4 Remove some logs 2019-10-28 20:32:23 +00:00
bcd175fbfb Fix service calls 2019-10-28 20:31:45 +00:00
f9f013636d 710 2019-10-28 18:02:03 +00:00
b34cc97080 Show entity page on main page 2019-10-28 17:59:47 +00:00
327f623ef7 Remove HA url from main manu 2019-10-28 16:39:47 +00:00
4d0877e5ae Remove Profile link 2019-10-28 16:36:20 +00:00
0eac217399 Set minimum location update intervhals to 15 minute 2019-10-28 16:28:01 +00:00
9c42ad687d 0.7.0 2019-10-28 10:43:10 +00:00
5cda98da46 whats new key update 2019-10-24 19:48:06 +00:00
958f545f65 Link to location tracking documentation 2019-10-24 19:41:58 +00:00
44165993b4 Integration setting improvement 2019-10-24 19:18:27 +00:00
283ae6cfd4 Integration settings improvements 2019-10-24 18:58:48 +00:00
4068b295bd Battery level for device_tracker 2019-10-24 18:34:38 +00:00
e36b33dcec Update README.md 2019-10-24 14:09:42 +03:00
4b12912697 build 704 2019-10-23 18:24:10 +00:00
49a21967cc Integration settings 2019-10-23 18:22:52 +00:00
cf36406f2a Update README.md 2019-10-22 22:05:15 +03:00
872ad044f1 Update README.md 2019-10-22 22:00:47 +03:00
345301c03a Merge pull request #481 from estevez-dev/pre-release/702
Pre release/702
2019-10-22 21:57:43 +03:00
117923413d Do not use static debug key for CI 2019-10-22 18:48:28 +00:00
24ccbc58c4 703 2019-10-22 18:43:40 +00:00
89c91b4b01 Location tracking improvements 2 2019-10-22 18:42:30 +00:00
4494da1f4f 702 2019-10-21 18:26:12 +00:00
c263542c54 Location tracking improvements 2019-10-21 18:22:25 +00:00
c70f52a73d Merge pull request #480 from estevez-dev/pre-release/0.7.0
build 701
2019-10-21 00:35:22 +03:00
423813d6fb Update README.md 2019-10-21 00:33:53 +03:00
ec6a86f4b0 build 701 2019-10-21 00:13:34 +03:00
64cf18cb23 Debug key 2019-10-20 20:04:53 +00:00
e0e064bc67 build number 700 2019-10-20 18:28:07 +00:00
5cee6cbd9c Remove auth code from logs 2019-10-20 18:22:51 +00:00
43659b26f7 Fix workmanager init 2019-10-20 18:21:51 +00:00
98e15ad429 Resolves #360 Update material design icons to version 4.5.95 2019-10-20 21:14:27 +03:00
90728cdf8b WIP #260 Update MDI parser to 4.5.95 2019-10-20 21:02:57 +03:00
d1ec4f36cc Resolves #49 Location tracking 2019-10-20 17:54:29 +00:00
079070071e Remove webview plugin 2019-10-20 17:18:23 +00:00
520fd6bc38 Migrate athentication from webview to deep linking 2019-10-20 16:45:44 +00:00
085aead36b Add files via upload 2019-10-20 14:30:59 +03:00
fcbaf298cc Delete google-services.json 2019-10-20 14:30:45 +03:00
eedc0c9b22 More gitignore 2019-10-20 10:51:53 +00:00
f70c1e12ff Update gitignore 2019-10-20 10:46:38 +00:00
ec094a4362 Merge pull request #478 from estevez-dev/gitpod
Gitpod configuration
2019-10-18 21:14:11 +03:00
11646c840e Merge branch 'master' into gitpod 2019-10-18 21:13:58 +03:00
86987c57c9 Gitpod configuration 2019-10-18 18:11:06 +00:00
e4d6e842f5 Gitpod configuration 2019-10-18 16:44:01 +00:00
cfe4dd1c59 Vacuum state colors update 2019-10-16 19:39:14 +03:00
3387ab2850 Resolves #416 Vacuum support 2019-10-16 19:34:29 +03:00
abd23e27ea WIP #416 Vacuum support 2019-10-14 15:02:49 +03:00
2f110b20bb Resolves #365 Fix missed group entities 2019-10-14 13:39:00 +03:00
f88e6f9b61 Fix light controls inconsistance 2019-10-14 12:51:20 +03:00
2836973dca Fix light controls issues 2019-10-09 20:45:46 +03:00
a4477e9f83 Resolves #472 entity-filter fix 2019-09-30 21:21:16 +03:00
96fa7ece25 Resolves #444 connection fix 2019-09-30 21:11:37 +03:00
b84caa4cc3 Resolves #469 fix zero position when player paused 2019-09-30 21:05:14 +03:00
49c212632e Fix app restore when on entity page 2019-09-30 20:58:04 +03:00
92165aa7ed Remove intents 2019-09-30 20:30:52 +03:00
cbbdb754aa Packages update 2019-09-26 22:46:52 +03:00
7e3fe0608d Deep links native handler 2019-09-26 13:31:09 +03:00
781f39f281 Deep linking manifest preparation 2019-09-26 13:24:27 +03:00
bfb80f6f8c Resolves #457 Don't send media to unavailable players 2019-09-20 16:51:57 +03:00
801b8f9288 Resolves #459 Send media to the same player 2019-09-20 16:47:02 +03:00
b988fcfcdd Resolves #461 Hide media switch buttons if nothing playing 2019-09-20 16:42:50 +03:00
dff6457cb2 Resolves #462 seek bar for idle players 2019-09-20 16:39:21 +03:00
f50f68f318 Fix video with no duration 2019-09-20 16:15:26 +03:00
c869ad41d9 Packadges update 2019-09-18 21:42:46 +03:00
cd41f9a236 Fix 'Switch to' button 2019-09-18 21:41:02 +03:00
1dbe162bf0 Merge pull request #467 from estevez-dev/release/0.6.7
Release/0.6.7
2019-09-18 21:35:55 +03:00
1a52203bd7 Merge branch 'master' into release/0.6.7 2019-09-18 21:35:46 +03:00
753df3c724 v.0.6.7 2019-09-18 21:18:24 +03:00
dc62a08da3 Fix issue with unnamed view 2019-09-18 21:14:21 +03:00
0c26aff498 672 2019-09-15 20:56:23 +03:00
6323f8f2e6 Whats new url with app version 2019-09-15 20:55:28 +03:00
885c0b1316 Fix stop player when switching to another 2019-09-15 20:27:49 +03:00
14958d9165 Whats new page 2019-09-15 20:23:03 +03:00
bf6a52e0b9 Improve media switching 2019-09-15 18:38:02 +03:00
72aad5cc16 Turn off source player when swicthing media 2019-09-15 17:38:29 +03:00
340e8569cc Switch media to another player 2019-09-15 17:29:49 +03:00
8fc7d0b61e Fix entity state non updated on entity page 2019-09-15 14:34:00 +03:00
5dcb27ada7 Fix no duration crash on media player 2019-09-15 11:10:19 +03:00
db1a076132 Fix media popup menu 2019-09-15 10:48:35 +03:00
6707201e23 Media player controls improvements 2019-09-15 10:39:08 +03:00
b8b92171a8 Media player seek 2019-09-15 01:50:03 +03:00
3dd7069292 Resolves #450 Quick access to active media players 2019-09-15 00:49:49 +03:00
7177419472 Call service with POST instead of waiting for socket 2019-09-14 20:08:10 +03:00
c37313cf07 Some refactoring 2019-09-14 19:53:39 +03:00
a65f42d0fd Hide entity history and attributes under expandepble card 2019-09-14 19:37:52 +03:00
78dd7df686 Entity class refactoring 2019-09-14 19:12:11 +03:00
2ea7d9440c Entity class refactoring 2019-09-14 19:07:21 +03:00
abdcd49368 Fix window resize crash on Chrome OS 2019-09-14 18:54:31 +03:00
6da7a5ab90 Resolves #224 Main UI tablet support 2019-09-14 18:32:44 +03:00
20ffe03139 View widget improvements 2019-09-14 12:31:50 +03:00
a71213c589 Merge pull request #452 from estevez-dev/feature/tablet_ui
Feature/tablet ui
2019-09-14 12:20:13 +03:00
d61103ac42 WIP #224 Dynamic multi column view 2019-09-14 12:18:37 +03:00
298a64b7ae Packages update 2019-09-14 12:18:37 +03:00
9e2c673966 Delete lovelace-card-implementation-request.md 2019-09-14 12:18:37 +03:00
092469d668 Update issue templates 2019-09-14 12:18:37 +03:00
bcf3dab0e2 Update issue templates 2019-09-14 12:18:37 +03:00
7ecfc8a9ff Update issue templates 2019-09-14 12:18:37 +03:00
ecf0a696f7 Create no-response.yml 2019-09-14 12:18:37 +03:00
dc5db28e01 Packages update 2019-09-12 17:08:40 +03:00
555f305c22 Delete lovelace-card-implementation-request.md 2019-09-12 14:07:24 +03:00
76bf07cfcd Update issue templates 2019-09-12 14:06:44 +03:00
c4663576d1 Update issue templates 2019-09-12 14:05:45 +03:00
a64aa73aae Update issue templates 2019-09-12 14:00:16 +03:00
a3a60dd707 Create no-response.yml 2019-09-12 13:36:37 +03:00
9c28b0085b Tablet UI in progress 2019-09-10 15:40:49 +03:00
d5baabdd53 Project structure changes 2019-09-09 18:50:35 +03:00
56a333a852 v.0.6.6 2019-09-09 14:25:27 +03:00
c5922368de Add whars new user message 2019-09-09 14:24:54 +03:00
8c2316a51a Resolves #446 Fix conditional crads 2019-09-09 13:36:33 +03:00
e2e6c015de Fix state color for paused media_player 2019-09-09 12:28:32 +03:00
0a6ff4586d Share media url to HA CLient to play on media_player 2019-09-09 12:25:13 +03:00
fc228d85ae Disable wip card 2019-09-08 19:06:41 +03:00
61823cb43b Merge pull request #445 from estevez-dev/feature/light_card_support
WIP #212 Light card support
2019-09-08 19:05:18 +03:00
127e0b8182 WIP #212 Light card support 2019-09-08 19:04:12 +03:00
38c37fa212 Launch camera view in Chrome custom tab 2019-09-07 19:26:00 +03:00
dfaf2a2924 Project structure change 2019-09-07 18:23:04 +03:00
c90c40c046 Resolves #443 Lovelace view as panel support 2019-09-07 17:58:00 +03:00
d2049b726a Resolves #348 Button entity refactoring 2019-09-07 17:27:23 +03:00
6508f109f7 Minor gauge fixes 2019-09-07 17:04:40 +03:00
37e63637a7 Resolves #348 Glance card improvements 2019-09-07 16:46:41 +03:00
6650c5c145 Resolves #208 Gauge card 2019-09-07 15:47:09 +03:00
9160dbf7f2 0.6.5 2019-09-05 15:43:13 +03:00
243fcd7c49 Resolves #430 Trim any leading and trailing whitespace in address, port or token 2019-09-05 00:51:29 +03:00
c114bcfb35 Resolves #426 Autofill port if not set 2019-09-05 00:48:20 +03:00
83defb08f1 Resolves #425 Paste option for text fields 2019-09-05 00:43:45 +03:00
57ebdbbe85 Main page in separate file 2019-09-05 00:25:03 +03:00
c6aceed623 utils separated 2019-09-05 00:17:08 +03:00
ba4c88ec5d Startup user messages fix 2019-09-05 00:09:40 +03:00
ee1685e981 Refix #439 2019-09-04 23:42:19 +03:00
996fbf7bba Login error handling improvements 2019-09-04 23:40:37 +03:00
56cd8963d7 Fix mobile_app error help url 2019-09-04 23:13:11 +03:00
5759aad0cb Remove unused plugin 2019-09-04 23:09:17 +03:00
02717332f7 Revert all rash decisions 2019-09-04 22:46:14 +03:00
8d1b159f56 User error messages 2019-09-04 22:03:52 +03:00
fb335e1100 WIP: user messages 2019-09-04 15:10:25 +03:00
5f0bc83d67 Resolves #439 Fix negative and zero columns issue for glance card 2019-09-03 23:58:28 +03:00
6a8cee2cc2 Login errors handling improvements 2019-09-03 23:44:03 +03:00
0d2f1cf9aa No mobile_app error handling 2019-09-03 23:35:33 +03:00
8efeb3da8a Error messages refactored 2019-09-03 23:25:39 +03:00
620aa3b8d8 Disabling location tracking 2019-09-03 20:13:23 +03:00
ab5bf3b807 WIP #49 Catch http errors inside location isolate 2019-09-02 21:21:38 +03:00
6663bcad72 Resolves #339 Authenticated webview for panels and config sections 2019-09-02 21:08:20 +03:00
113cd29f74 Resolves #431 Add default card type if not specified 2019-09-02 19:36:20 +03:00
f2fdfb0a32 WIP #339 Open unsupported Panels as authenticared webviews 2019-09-02 19:05:49 +03:00
691e48a36b 0.6.5-alpha2 2019-09-01 23:20:29 +03:00
2036cc117f Fix support app popup buttons 2019-09-01 23:19:26 +03:00
389d28a1e1 Location manager optimizations 2019-09-01 23:12:43 +03:00
27e6198d83 Remove test data send 2019-09-01 22:31:46 +03:00
de762a4878 Resolves #401 Fix login restart and several login views opened 2019-09-01 22:27:56 +03:00
e8efefe25d Remove test data send 2019-09-01 22:01:27 +03:00
21f3e8985a 0.6.5-alpha1 2019-09-01 00:30:27 +03:00
622543d405 Fix notification handlers return type 2019-09-01 00:14:54 +03:00
abdc0fc1c8 Remove dead code 2019-09-01 00:12:16 +03:00
1ecb839042 Add user messages 2019-08-31 23:55:32 +03:00
cece4d1e16 Add location tracking switch disabled by default 2019-08-31 23:09:30 +03:00
623634cb6e Send location on app resume 2019-08-31 22:37:55 +03:00
f9c37f5084 Refactor ConnectionManager and DeviceInfoManager 2019-08-31 22:10:07 +03:00
3e12f4f8a4 Create MobileAppIntegrationManager 2019-08-31 22:06:52 +03:00
b07ff6fe71 WIP #49 Add location update interval settings 2019-08-30 21:45:34 +03:00
5a3b57c28e Update location permissions 2019-08-30 19:56:22 +03:00
e858eee83b Merge pull request #396 from koying/history_tweak
History tweaks
2019-08-30 19:48:04 +03:00
73f00d3bd7 Merge pull request #428 from estevez-dev/gelocator_plugin
WIP #49 - geolocator plugin testing
2019-08-30 19:46:25 +03:00
eea59cf11b WIP #49 - geolocator plugin testing 2019-08-30 16:12:03 +03:00
61b459ed8a WIP #49 2019-08-30 15:51:39 +03:00
dca8c309aa WIP #49 Location tracking service and test alarm manager 2019-08-30 15:04:51 +03:00
be53500104 WIP #49 2019-08-29 12:57:24 +03:00
bc1a791608 WIP #49 2019-08-28 19:23:24 +03:00
b112ff980a Update README.md 2019-08-28 00:08:17 +03:00
7beab9ae93 0.6.4 2019-08-27 20:06:18 +03:00
8c0d1f90a3 Resolves #422 Fix noSuchMethod for group-based UI 2019-08-27 20:05:33 +03:00
05c05ba768 Possibly resolves #422 2019-08-27 12:05:33 +03:00
67e885e76a Update subscribtions 2019-08-26 19:42:26 +03:00
594bce0b8d Update packages 2019-08-26 19:00:17 +03:00
7f6569e0db 0.6.3 2019-08-26 18:56:06 +03:00
1c829c8364 Remove sensitive information from log 2019-08-26 18:55:12 +03:00
7ca4b02e6d Resolves#413 Handling removed mobile_app integration 2019-08-26 18:30:49 +03:00
fadfefd836 Resolves #421 Manual long-lived token 2019-08-26 18:04:40 +03:00
37155901ef Resolves #205, Resolves #417 Condotional cards support 2019-08-26 17:03:28 +03:00
fbbb96409d Project structure change in progress 2019-08-24 21:22:32 +03:00
5126c54914 Resolves #412 Add 'Support app development' option 2019-08-24 20:39:25 +03:00
916d0b7e3c WIP #412 2019-08-24 15:22:23 +03:00
0815840a9c WIP #412 2019-08-23 14:13:58 +03:00
bc237796b2 0.6.2 2019-08-21 21:30:57 +03:00
7f44800f64 Fix mobile_app registration and update 2019-08-21 21:30:11 +03:00
85ac746e9d 0.6.1 2019-08-21 18:49:56 +03:00
8215175098 Resolves #409 Remove slesh from the end of HA address 2019-08-21 18:48:53 +03:00
39ee8b1799 Resolves #410 os_version removed from mobile_app registration 2019-08-21 18:40:16 +03:00
c76d3d68c8 Resolves #406 Minimum light brightness is set to 1 isntead of 0 2019-08-21 18:28:07 +03:00
cde257922b 0.6.0 2019-08-21 09:40:18 +03:00
be0c9d3372 App bundle test 2019-08-20 11:22:47 +03:00
66cd7ea307 0.6.0-alpha3 2019-08-16 15:05:43 +03:00
b704ce6984 0.6.0-alpha3 2019-08-16 14:01:10 +03:00
247c856a41 Resolves #397 Add default icon for device_tracker 2019-08-16 13:44:29 +03:00
9afaebfa12 Resolves #401 Climate support fixes 2019-08-16 13:29:41 +03:00
929abea5d3 Login and mobile app registration improvements 2019-08-16 12:32:36 +03:00
13102a6b04 CHG: [history] wrap around 2019-06-27 18:25:36 +02:00
57c3083f9f CHG: [history widget] select last measurement initally 2019-06-27 18:25:24 +02:00
5c31ddd00f Resolves #345 Add default icon for Remote 2019-06-23 16:19:28 +03:00
8f55be187d Resolves #324 devider fix, entity card padding fix 2019-06-23 16:08:12 +03:00
1fe82d8b0d Resolves #334 Fix plug device_class icons 2019-06-23 15:27:55 +03:00
cbc56a8105 Resolves #336 Replace 'unknown' state with '-'. Show displayState for badges 2019-06-23 15:24:08 +03:00
b63cddfa46 Resolves #330 Add Help menu item 2019-06-23 15:15:33 +03:00
91db82f730 Resolves #331 Menu item text change 2019-06-23 15:11:04 +03:00
0c4d1b78ff Resolves #323 fix widget padding for entity page 2019-06-23 15:09:18 +03:00
5af2fd0562 Resolves #376 Dynamic font size on badges 2019-06-23 14:53:11 +03:00
2375543ebf Fix camera stream open 2019-06-23 14:36:15 +03:00
de187f3ed5 Update mdi array builder script 2019-06-21 21:30:26 +03:00
9266ffacf3 Update Material Design Icons font to 3.6.95 2019-06-21 21:28:37 +03:00
3c0ca5d16d Resolve #382 VIew camera in chrome custom tab 2019-06-21 21:01:53 +03:00
caabf25260 WIP #382 Open camera stream in CHrome custom tab 2019-06-21 14:29:56 +03:00
0af2afbb80 Add links to web version of COnfiguration secrtions 2019-06-21 13:33:28 +03:00
12d226509d App registration improvements 2019-06-21 13:21:30 +03:00
3417c38426 Resolves #386 2019-06-21 12:53:03 +03:00
c7fc5afbb8 Resolves #389 Improve app registration checking 2019-06-21 12:39:58 +03:00
11f565a9dc Resolves #388 2019-06-21 12:05:55 +03:00
53240faac3 Fix automatic OAuth window open issue 2019-06-16 22:57:50 +03:00
95d4878785 Resolves #48 Native notifications 2019-06-16 20:08:50 +03:00
ef15026203 Fix authentication process. App register in background 2019-06-16 16:32:55 +03:00
ad6355503b WIP #48 Show dialog on app registration 2019-06-16 00:23:11 +03:00
491c2b0dc0 WIP #48 Notifications with mobile_app component 2019-06-16 00:08:13 +03:00
5b99ade088 Resolves #318 add mobile_app integration 2019-06-15 18:07:11 +03:00
e1d9d9f304 Stop connection init if settings is empty 2019-06-15 14:36:11 +03:00
209ccd4f7f New error class 2019-04-19 21:43:52 +03:00
5a8a207f2e minor fix 2019-04-19 14:40:05 +03:00
19c85d9c16 Don't handle state change if fetch is in progress 2019-04-19 14:38:02 +03:00
a916ddfa50 Resolves #364, Resolves #363 Connection issues 2019-04-19 14:07:44 +03:00
8c1ad9c7f9 Fix login button 2019-04-05 14:07:03 +03:00
93af1eca7e Resolves #355 Add login button on empty screen 2019-04-05 13:39:54 +03:00
cabf836fa3 WIP #355 Disconnect when logout 2019-04-05 13:06:14 +03:00
15b3d31a6f Resolves #353 Show error if connection drops 2019-04-05 12:23:31 +03:00
9b98689012 Fix connection error handling 2019-04-05 12:08:32 +03:00
84ebd0c33c Resolves #352 Fix panels clear after logout 2019-04-05 11:59:13 +03:00
ccd7774931 Resolves #350 Fix displayed hostname 2019-04-05 11:57:58 +03:00
b2773635f5 Connection improvements 2019-04-05 11:48:41 +03:00
8b046b7313 Merge branch '0.6.0-alpha1-1' 2019-04-04 22:25:19 +03:00
885a516676 alpha2 2019-04-04 22:12:08 +03:00
921b0e09b0 Merge branch 'terms_and_privacy' into 0.6.0-alpha1-1 2019-04-04 22:10:29 +03:00
277c67fc6f Add padding for links in About dialog 2019-04-04 21:54:41 +03:00
2a01ff8a03 Bump version in UI 2019-04-04 21:51:05 +03:00
b246b7bc1d 0.5.3 and new build numbers 2019-04-04 21:44:16 +03:00
e1868b9a14 Add privacy polici and terms and conditions links 2019-04-04 21:43:23 +03:00
125f3ac16c Resolves #327 Timer duration parsing error 2019-04-04 21:38:23 +03:00
be502b5668 Discord icon fix 2019-04-04 21:38:05 +03:00
6f33fdca9f New app icon 2019-04-04 21:37:41 +03:00
a7cda2a35e WIP #48 Notifications 2019-03-30 00:29:52 +02:00
102b10ade0 WIP #48 Notifications 2019-03-29 13:09:34 +02:00
4e96b9adbb Build 101 2019-03-29 11:16:04 +02:00
b9581d3762 Resolves #347, Resolves #346 Connection and reconnection 2019-03-29 11:04:43 +02:00
7c010359c3 Resolves #340 Connection refactoring 2019-03-26 00:18:30 +02:00
4a75243994 WIP #340 Refactor getting data and error handling 2019-03-22 14:04:20 +02:00
d29d7e5b3b WIP #340 2019-03-21 16:55:25 +02:00
5ebd25e0d1 Resolves #59 Storing token in secure storage 2019-03-21 14:25:05 +02:00
b7d5a53e86 Resolves #341 Add logout 2019-03-21 14:08:07 +02:00
20d3498bfd WIP #341 Logout 2019-03-20 23:38:57 +02:00
67d7bb45f5 Resolves #338 OAuth with Home Assistant 2019-03-20 23:05:25 +02:00
6a03105d01 WIP 2019-03-20 19:01:30 +02:00
5ae580ecf1 Chachesd HomeAssistance instance for every view in app 2019-03-20 12:48:00 +02:00
0efef33e53 Fix CleartextTraffic issue. WIP #338 2019-03-19 23:20:57 +02:00
ccb88884a7 Settings loading refactored. WIP #338 2019-03-19 23:07:40 +02:00
d70ba0a55a WIP #48 2019-03-18 23:37:45 +02:00
5140840d3a Resolves #327 Timer duration parsing error 2019-03-14 16:39:37 +02:00
14759fd3c9 Discord icon fix 2019-03-14 14:35:30 +02:00
fed35be517 New app icon 2019-03-14 14:07:36 +02:00
db77cc43aa Version 0.5.0 2019-03-13 22:42:03 +02:00
b2269cc96d Resolves #293 Fix updater icon 2019-03-13 22:40:54 +02:00
8b28bb2e9e Resolves #314 card icon priority 2019-03-13 22:12:01 +02:00
fb456878bc Resolves #258 Timer support 2019-03-13 21:33:58 +02:00
8b961ebd69 Resolves #83 Calendar support 2019-03-13 20:07:44 +02:00
9bd3a41cf5 Resolves #140 Scenes 2019-03-13 18:06:43 +02:00
491ae55a2a Resolves #299, Resolves #234 Fix entity picture url issue 2019-03-13 17:48:49 +02:00
e1d2981782 Add 'Open Web UI' menu link 2019-03-13 17:25:08 +02:00
74572168ae Resolves #116 Add Iframe panel support 2019-03-13 17:23:23 +02:00
92d0b5c055 Migrate to AndroidX 2019-03-13 17:05:15 +02:00
3504d3276c Resolves #11 Add Panels fetching 2019-03-13 16:39:23 +02:00
736b38b64c Some UI improvements for #245 2019-03-13 14:08:54 +02:00
cb118b599a Resolves #245 Add special row elements support for entities card 2019-03-13 00:56:57 +02:00
a08a056cff Resolves #254 Missed entities 2019-03-12 23:35:33 +02:00
0ef2ebfe31 Fix 'Paste color' button background when saved color is null 2019-03-10 23:49:05 +02:00
4f4ac3b574 Resolves #310 Add assumed state for locks 2019-03-10 23:41:14 +02:00
7064cb0e30 Resolves #272 Add 'Copy color' and 'Past color' 2019-03-10 23:28:23 +02:00
91a99e17e0 Resolves #320 Fix eEntity_picture size 2019-03-10 22:50:39 +02:00
2e9b7d20b9 Fix broken icons 2019-03-10 19:28:11 +02:00
b8aa808de4 Update Material Design Icons to 3.5.95 2019-03-09 13:26:45 +00:00
2cfa92a42b Reverts #308 2019-03-06 16:50:30 +00:00
146efef72d Gradle config for Chrome OS build 2019-03-06 16:42:05 +00:00
8c9804e16f WIP #308 2019-03-02 20:13:24 +02:00
a4736bfb5a Message handling improvements 2019-03-02 18:00:25 +02:00
15c54df629 Update README.md 2019-02-26 11:31:39 +02:00
32ffef21e9 Update README.md 2019-02-26 11:31:08 +02:00
848d3cb510 Update README.md 2019-02-26 10:45:25 +02:00
8a4caeebba Update README.md 2019-02-26 10:43:47 +02:00
aa923f0fba Update README.md 2019-02-26 10:39:09 +02:00
4d8f50ddd5 Update README.md 2019-02-26 10:33:34 +02:00
fe06b21a6c Update README.md 2019-02-26 10:30:08 +02:00
efed7fb1b5 Update README.md 2019-02-26 10:23:03 +02:00
df2cbb7d13 Resolves #313 Fix missed mute button for media_player 2019-02-22 15:39:53 +02:00
03edaa9ca2 Resolves #168 Fix error when entity view closed before history loaded 2019-02-22 15:33:10 +02:00
1a7457abf9 Resolves #311 Rebuild tabs only if views count changes 2019-02-22 15:28:11 +02:00
00889b13e0 Resolves #312 Add white value control for light 2019-02-22 15:15:27 +02:00
0615073ec4 Get color from rgb_color if there is no hsv_color attribute 2019-02-22 14:20:01 +02:00
eb7d17d147 WIP #308 Move entity icon generation into EntityIcon widget 2019-02-21 16:32:55 +02:00
24f80feeee Resolves #187 Fix crash on view count changes 2019-02-21 15:35:58 +02:00
4b6dda5a9c version 0.4.4 2019-02-20 18:54:54 +02:00
4099fa0c83 WIP #302 fix SVG size 2019-02-20 18:50:58 +02:00
76057e8797 WIP #302 simple SVG support 2019-02-20 17:55:56 +02:00
538d3603dc Resolves #306 Improve camera connection 2019-02-20 16:39:57 +02:00
bc0e72ca52 version 0.4.3 2019-02-20 13:58:30 +02:00
f25a47beb2 Add camera stream reconnect on closing 2019-02-20 13:57:25 +02:00
cc3c6b0087 Resolves #307 Support different frame bounderies for MJPEG stream 2019-02-20 12:06:03 +02:00
6cf80c0bfd version 0.4.2 2019-02-19 19:22:40 +02:00
8ce9bdb7a5 Resolves #303, Resolves #304 Fix wrong camera stream url 2019-02-19 19:21:52 +02:00
31e50150b1 Resolves #263 Fix error when supported_features is null 2019-02-17 13:52:24 +02:00
e359150d97 Version 0.4.0 2019-02-16 22:05:43 +02:00
93680c981c Resolves #283 Add possibility to restrat HA 2019-02-16 22:04:49 +02:00
e06b66c523 Resolves #259 target_temp_step support for climate 2019-02-16 20:44:41 +02:00
3dea844e1e Resolves #290 Hide pin inputs if code_format is null 2019-02-16 20:33:56 +02:00
62b1af30e0 Resolves #291 some padding issues 2019-02-16 19:59:39 +02:00
e006c4e403 build number 2019-02-10 22:36:30 +02:00
983573388e Remove pull-to-refresh. Add new menu in header. 2019-02-10 22:33:46 +02:00
bdd1dc7e17 Hide light additional controls if state=unavailable 2019-02-10 19:06:07 +02:00
9c1970ee14 build number 2019-02-10 18:26:48 +02:00
d0e0bf3571 Current color for color picker fix 2019-02-10 18:24:54 +02:00
b399357517 Back to old version format 2019-02-10 17:23:57 +02:00
0290cd3a32 Resolves #273 New color picker 2019-02-10 17:15:52 +02:00
d8a1d03179 v.3.14.87 2019-02-09 02:21:20 +02:00
216fad3cb9 Fix entity page padding 2019-02-09 02:21:20 +02:00
fead6ea348 Resolves #143 Camera support 2019-02-09 02:21:20 +02:00
8814687be6 WIP #143 Initial not optimized MJPEG streaming 2019-02-09 02:21:20 +02:00
71c0e2caa0 Change version numeration 2019-02-09 02:21:20 +02:00
1531c41542 Create CODE_OF_CONDUCT.md 2019-02-01 12:55:25 +02:00
bc90d013e8 Build 86 2019-02-01 11:52:09 +02:00
2adfaca0c4 Resolves #286 2019-02-01 11:51:35 +02:00
6cc1a37d9d Resolves #285 2019-02-01 11:49:27 +02:00
4bb616b327 Build number 2019-01-31 21:14:20 +02:00
38219618ba version 0.3.14 2019-01-31 21:05:11 +02:00
6774b53758 Disable unfinished camera support 2019-01-31 20:57:33 +02:00
29a94c882f WIP MJPEG stream handling 2019-01-31 01:04:13 +02:00
5897fa3a99 WIP #143 2019-01-30 00:25:41 +02:00
7af92c2dc9 WIP #143 Camera support 2019-01-29 22:03:08 +02:00
1094177a42 Resolves #282 Trigger button for alarm 2019-01-29 18:51:28 +02:00
5e814e8109 Resolves #204 Alarm panel card support 2019-01-29 15:00:15 +02:00
24c7675fa4 Resolves #142 Alarm control panel support 2019-01-29 11:54:26 +02:00
dc3ca38c78 WIP #142 Alarm control panel 2019-01-28 16:48:49 +02:00
96b528e055 Update README.md 2019-01-28 15:05:19 +02:00
3858036631 Resolves #139 Trigger for automations 2019-01-25 23:48:31 +02:00
19d42ceeb3 Fix names null 2019-01-25 23:30:23 +02:00
a2836a3603 Resolves #257 2019-01-25 23:08:12 +02:00
2a45758a6d Resolves #268 Badges for Lovelace UI 2019-01-25 22:55:41 +02:00
dc1bf4d878 WIP #266 fix icons 2019-01-25 22:45:54 +02:00
e82ba60c4e WIP #266 Card parsing proper error handling and toString for some fields 2019-01-25 22:41:26 +02:00
09199d30e8 Resolves #274 Use Lovelace UI by default 2019-01-25 22:29:16 +02:00
724d32dbe2 Resolves #277 Remove legacy password support 2019-01-25 22:27:13 +02:00
949c8ee44e Resolves #264 Throttle for sending thermostat temperature 2019-01-25 22:19:11 +02:00
1a446d34c7 Resolves #262 Cler 2019-01-25 22:19:11 +02:00
22a5847285 Resolves #270 Current light effect for lights 2019-01-24 12:26:38 +02:00
1c8f770f10 Resolves #121 Markdown card support 2019-01-23 23:34:45 +02:00
be5ea55f6b Fix light color controls appearence issue 2018-12-29 17:44:11 +02:00
c65ade9827 Fix icons for entity button 2018-12-25 11:48:37 +02:00
d3c1422b9e Version 0.3.13 2018-12-15 14:37:55 +02:00
b6ac9f985f Entity state by device class 2018-12-15 14:37:00 +02:00
a59de4b6dc Resolves #255 Refresh UI for newly appeared entity 2018-12-15 14:09:37 +02:00
f507d5df0c Resolves #242 2018-12-14 19:37:49 +02:00
f77e46de37 Version 0.3.12 2018-12-14 17:04:52 +02:00
cda17b1217 Resolves #232 2018-12-14 17:03:18 +02:00
be560769ef Resolves #243 2018-12-14 16:57:11 +02:00
3815800e32 Resolves #253 removing trash characters from state string 2018-12-14 16:48:35 +02:00
a3226311a2 Fix default tap actions 2018-12-14 16:31:41 +02:00
79669243c2 Remove some logging 2018-12-14 16:01:13 +02:00
fdc81f6ea4 Resolves #237 2018-12-14 15:59:47 +02:00
7fe44459e7 Resolves #252, Resolves #249 2018-12-14 14:28:23 +02:00
a8500d44e1 Version 0.3.11 2018-12-13 23:42:34 +02:00
b4d4c5abec Relates to #248 old format support 2018-12-13 23:40:20 +02:00
c19a3f272a Resolves #248 tap_action parsing fix 2018-12-13 23:37:54 +02:00
b264534858 Add files via upload 2018-12-12 10:43:26 +02:00
ab53f77f9e Add files via upload 2018-12-12 10:40:50 +02:00
c73956720c Create empty 2018-12-12 10:39:42 +02:00
051041e794 build number 2018-12-07 23:00:20 +02:00
5c83be9fee Resolves #207 Entity filter card support 2018-12-07 22:04:14 +02:00
4bece42693 build number 2018-11-29 21:46:47 +02:00
4ae107fe4c Resolves #230 Vertical stack card 2018-11-29 21:45:46 +02:00
9523ed2562 Build number 2018-11-25 20:45:22 +02:00
9c403480e2 Resolves #120 Horizontal Stack Cards 2018-11-25 20:44:19 +02:00
20b1b90e39 Resolves #206 Entity button with tap and hold events 2018-11-25 18:09:06 +02:00
5633e30448 WIP #206 Entity button card 2018-11-25 17:33:33 +02:00
4492fb9f0c Build number 2018-11-24 17:30:02 +02:00
36410752e4 Fix app version 2018-11-24 17:29:33 +02:00
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
23d3d1839f Merge pull request #71 from estevez-dev/release/0.1.1-alpha
Release/0.1.1 alpha
2018-09-24 00:24:13 +03:00
176 changed files with 18975 additions and 4624 deletions

41
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,41 @@
---
name: Bug report
about: Create a report to help improve HA Client
title: ''
labels: ''
assignees: ''
---
<!--
Please provide as much information as possible.
-->
**HA Client version:** <!-- Main app menu => About HA Client -->
**Home Assistant version:** <!-- 0.94.1 for example -->
**Device name:** <!-- Pixel 2 for example -->
**Android version:** <!-- 8.1 for example -->
**Connection type:** <!-- For example "Local IP" or "Remote UI" or "Own domain"-->
**Login type:** <!-- For example "HA Login" or "Manual token"-->
**Description**
<!--
Describe your issue here
-->
**Screenshots**
<!--
Please provide screenshots if it is a UI issue. Also you can attach screenshot from Home Assistant web UI as an expected result
-->
**Logs**
<!--
Right after issue reproduced go to app menu and tap "Log". Copy log with a "Copy" button in the upper-right corner and post it below
-->
```
[Replace this text with your logs]
```

View File

@ -0,0 +1,12 @@
---
name: Entity support request
about: Suggest to add support of any entity type
title: ''
labels: ENTITY, feature/improvement
assignees: ''
---
**Entity type:**
**Link to documentation:**

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for HA Client if it is not a card or entity support
title: ''
labels: feature/improvement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,12 @@
---
name: Lovelace Card support request
about: Suggest to add any Lovelace card support
title: ''
labels: CARD, feature/improvement
assignees: ''
---
**Card name:**
**Link to card repository or web page:**

11
.github/no-response.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 14
# Label requiring a response
responseRequiredLabel: more info needed
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. If the issue still relevant
feel free to reopen this issue and add more information, or report a new one.

10
.gitignore vendored
View File

@ -9,5 +9,13 @@ build/
.flutter-plugins
.idea/
.vscode/
.theia/
.project/
.settings/
key.properties
flutter_export_environment.sh
key.properties
premium_features_manager.class.dart
pubspec.lock

12
.gitpod.dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM gitpod/workspace-full:latest
ENV ANDROID_HOME=/workspace/android-sdk \
FLUTTER_ROOT=/workspace/flutter \
FLUTTER_HOME=/workspace/flutter
USER root
RUN apt-get update && \
apt-get -y install build-essential libkrb5-dev gcc make gradle openjdk-8-jdk && \
apt-get clean && \
apt-get -y autoremove

27
.gitpod.yml Normal file
View File

@ -0,0 +1,27 @@
image:
file: .gitpod.dockerfile
tasks:
- before: |
export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
mkdir -p /home/gitpod/.android
touch /home/gitpod/.android/repositories.cfg
init: |
echo "Installing Flutter SDK..."
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.9.1+hotfix.4-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
echo "Installing Android SDK..."
mkdir -p /workspace/android-sdk && cd /workspace/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip
/workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
echo "Init Flutter..."
cd /workspace/ha_client
flutter upgrade
flutter doctor --android-licenses
flutter pub get
command: |
flutter pub upgrade
echo "Ready to go!"
flutter doctor
vscode:
extensions:
- Dart-Code.dart-code@3.5.0-beta.1:Wg2nTABftVR/Dry4tqeY1w==
- Dart-Code.flutter@3.5.0:/kOacEWdiDRLyN/idUiM4A==

76
CODE_OF_CONDUCT.md Normal file
View File

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

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,15 @@
# Android client for Home Assistant
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
# HA Client
## Native Android client for Home Assistant
### With notifications and Lovelace UI support
Home Assistant Android client using Flutter and Dart.
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn)
#### Pre-release CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
#### Beta CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)

1
android/.gitignore vendored
View File

@ -8,3 +8,4 @@
/build
/captures
GeneratedPluginRegistrant.java
.project/

17
android/.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>android</name>
<comment>Project android created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

6
android/app/.classpath Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

23
android/app/.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View File

@ -29,7 +29,12 @@ def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
compileSdkVersion 27
compileSdkVersion 28
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
disable 'InvalidPackage'
@ -38,13 +43,21 @@ android {
defaultConfig {
applicationId "com.keyboardcrumbs.haclient"
minSdkVersion 21
targetSdkVersion 27
versionCode 19
versionName "0.1.1-alpha"
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
signingConfigs {
if (!System.getenv()["CI"]) {
debug {
keyAlias keystoreProperties['debugKeyAlias']
keyPassword keystoreProperties['debugKeyPassword']
storeFile file(keystoreProperties['debugStoreFile'])
storePassword keystoreProperties['debugStorePassword']
}
}
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
@ -65,7 +78,10 @@ flutter {
}
dependencies {
implementation 'com.google.firebase:firebase-core:16.0.8'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
apply plugin: 'com.google.gms.google-services'

View File

@ -0,0 +1,64 @@
{
"project_info": {
"project_number": "441874387819",
"firebase_url": "https://ha-client-c73c4.firebaseio.com",
"project_id": "ha-client-c73c4",
"storage_bucket": "ha-client-c73c4.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45",
"android_client_info": {
"package_name": "com.keyboardcrumbs.haclient"
}
},
"oauth_client": [
{
"client_id": "441874387819-uqmkibhf361828od1982o2jhl0n3m0ov.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.keyboardcrumbs.haclient",
"certificate_hash": "bebe4d970fbebf0bff2c93244fdc7fcbcefb3470"
}
},
{
"client_id": "441874387819-5q7vmimci4s2jl3v0ncugv1ocp4m48nb.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.keyboardcrumbs.haclient",
"certificate_hash": "0ea12348468be44bc2aa5792ee7e8924c633da81"
}
},
{
"client_id": "441874387819-joi8plo5345ebt8i1dug27u2aenv5tg7.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.keyboardcrumbs.haclient",
"certificate_hash": "fcbc805d965ccf6a4d5417398d191edc9c9890b0"
}
},
{
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -1,11 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keyboardcrumbs.hassclient">
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
<!-- The INTERNET permission is required for development. Specifically,
flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
@ -13,9 +21,15 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:name=".Application"
android:label="HA Client"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="ha_notify" />
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
@ -26,14 +40,41 @@
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
defined in @style/LaunchTheme).
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
android:value="true" />-->
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="haclient"
android:host="auth" />
</intent-filter>
</activity>
<service
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<receiver
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
android:exported="false"/>
<receiver
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
</intent-filter>
</receiver>
</application>
</manifest>
</manifest>

View File

@ -0,0 +1,20 @@
package com.keyboardcrumbs.hassclient;
import io.flutter.app.FlutterApplication;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
import io.flutter.plugins.GeneratedPluginRegistrant;
import be.tramckrijte.workmanager.WorkmanagerPlugin;
public class Application extends FlutterApplication implements PluginRegistrantCallback {
@Override
public void onCreate() {
super.onCreate();
WorkmanagerPlugin.setPluginRegistrantCallback(this);
}
@Override
public void registerWith(PluginRegistry registry) {
GeneratedPluginRegistrant.registerWith(registry);
}
}

View File

@ -3,8 +3,10 @@ package com.keyboardcrumbs.hassclient;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.share.FlutterShareReceiverActivity;
public class MainActivity extends FlutterActivity {
public class MainActivity extends FlutterShareReceiverActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -5,7 +5,8 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0'
}
}

View File

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

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

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

1232
android/hs_err_pid766.log Normal file

File diff suppressed because it is too large Load Diff

16
assets/js/externalAuth.js Normal file
View File

@ -0,0 +1,16 @@
window.externalApp = {};
window.externalApp.getExternalAuth = function(options) {
console.log("Starting external auth");
var options = JSON.parse(options);
if (options && options.callback) {
var responseData = {
access_token: "[token]",
expires_in: 1800
};
console.log("Waiting for callback to be added");
setTimeout(function(){
console.log("Calling a callback");
window[options.callback](true, responseData);
}, 500);
}
};

1
docs/empty Normal file
View File

@ -0,0 +1 @@

BIN
docs/ha_access_tokens.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/ha_profile-300x247.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/settings-869x1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

41
flutter_01.log Normal file
View File

@ -0,0 +1,41 @@
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
## command
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
## exception
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/.packages, isolateId: isolates/68989666}}, details: Isolate must be runnable before this request is made.}}
```
null```
## flutter doctor
```
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
• Engine revision fee001c93f
• Dart version 2.4.0
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Android SDK at /home/estevez/Android/Sdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-29, build-tools 29.0.2
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
• All Android licenses accepted.
[✓] Android Studio (version 3.5)
• Android Studio at /home/estevez/bin/android-studio
• Flutter plugin version 38.2.3
• Dart plugin version 191.8423
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
[✓] Connected device (1 available)
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
• No issues found!
```

41
flutter_02.log Normal file
View File

@ -0,0 +1,41 @@
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
## command
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
## exception
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/.packages, isolateId: isolates/289688365}}, details: Isolate must be runnable before this request is made.}}
```
null```
## flutter doctor
```
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
• Engine revision fee001c93f
• Dart version 2.4.0
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Android SDK at /home/estevez/Android/Sdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-29, build-tools 29.0.2
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
• All Android licenses accepted.
[✓] Android Studio (version 3.5)
• Android Studio at /home/estevez/bin/android-studio
• Flutter plugin version 38.2.3
• Dart plugin version 191.8423
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
[✓] Connected device (1 available)
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
• No issues found!
```

41
flutter_03.log Normal file
View File

@ -0,0 +1,41 @@
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
## command
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
## exception
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/.packages, isolateId: isolates/866521062}}, details: Isolate must be runnable before this request is made.}}
```
null```
## flutter doctor
```
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
• Engine revision fee001c93f
• Dart version 2.4.0
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Android SDK at /home/estevez/Android/Sdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-29, build-tools 29.0.2
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
• All Android licenses accepted.
[✓] Android Studio (version 3.5)
• Android Studio at /home/estevez/bin/android-studio
• Flutter plugin version 38.2.3
• Dart plugin version 191.8423
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
[✓] Connected device (1 available)
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
• No issues found!
```

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 24 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

64
lib/cards/card.class.dart Normal file
View File

@ -0,0 +1,64 @@
part of '../main.dart';
class HACard {
List<EntityWrapper> entities = [];
List<HACard> childCards = [];
EntityWrapper linkedEntityWrapper;
String name;
String id;
String type;
bool showName;
bool showState;
bool showEmpty;
int columnsCount;
List stateFilter;
List states;
List conditions;
String content;
String unit;
int min;
int max;
Map severity;
HACard({
this.name,
this.id,
this.linkedEntityWrapper,
this.columnsCount: 4,
this.showName: true,
this.showState: true,
this.stateFilter: const [],
this.showEmpty: true,
this.content,
this.states,
this.conditions: const [],
this.unit,
this.min,
this.max,
this.severity,
@required this.type
}) {
if (this.columnsCount <= 0) {
this.columnsCount = 4;
}
}
List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) {
if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) {
return false;
}
if (stateFilter.isNotEmpty) {
return stateFilter.contains(entityWrapper.entity.state);
}
return true;
}).toList();
}
Widget build(BuildContext context) {
return CardWidget(
card: this,
);
}
}

361
lib/cards/card_widget.dart Normal file
View File

@ -0,0 +1,361 @@
part of '../main.dart';
class CardWidget extends StatelessWidget {
final HACard card;
const CardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.linkedEntityWrapper!= null) {
if (card.linkedEntityWrapper.entity.isHidden) {
return Container(width: 0.0, height: 0.0,);
}
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: MissedEntityWidget(),
handleTap: false,
);
}
}
if (card.conditions.isNotEmpty) {
bool showCardByConditions = true;
for (var condition in card.conditions) {
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
if (conditionEntity != null &&
((condition['state'] != null && conditionEntity.state != condition['state']) ||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
) {
showCardByConditions = false;
break;
}
}
if (!showCardByConditions) {
return Container(width: 0.0, height: 0.0,);
}
}
switch (card.type) {
case CardType.ENTITIES: {
return _buildEntitiesCard(context);
}
case CardType.GLANCE: {
return _buildGlanceCard(context);
}
case CardType.MEDIA_CONTROL: {
return _buildMediaControlsCard(context);
}
case CardType.ENTITY_BUTTON: {
return _buildEntityButtonCard(context);
}
case CardType.GAUGE: {
return _buildGaugeCard(context);
}
/* case CardType.LIGHT: {
return _buildLightCard(context);
}*/
case CardType.MARKDOWN: {
return _buildMarkdownCard(context);
}
case CardType.ALARM_PANEL: {
return _buildAlarmPanelCard(context);
}
case CardType.HORIZONTAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
children.add(
Flexible(
fit: FlexFit.tight,
child: card.build(context),
)
);
}
});
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
case CardType.VERTICAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
children.add(
card.build(context)
);
});
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
default: {
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
return _buildEntitiesCard(context);
} else {
return _buildUnsupportedCard(context);
}
}
}
}
Widget _buildEntitiesCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeader(name: card.name));
entitiesToShow.forEach((EntityWrapper entity) {
body.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
child: entity.entity.buildDefaultWidget(context)
),
));
});
return Card(
child: Padding(
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
child: Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildMarkdownCard(BuildContext context) {
if (card.content == null) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeader(name: card.name));
body.add(MarkdownBody(data: card.content));
return Card(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildAlarmPanelCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeader(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle(
color: Colors.grey
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
EntityIcon(
size: 50.0,
),
Container(
width: 26.0,
child: IconButton(
padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
)
)
]
),
));
body.add(
AlarmControlPanelControlsWidget(
extended: true,
states: card.states,
)
);
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: body
)
)
);
}
Widget _buildGlanceCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> rows = [];
rows.add(CardHeader(name: card.name));
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
rows.add(
Padding(
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
child: FractionallySizedBox(
widthFactor: 1,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
List<Widget> buttons = [];
double buttonWidth = constraints.maxWidth / columnsCount;
entitiesToShow.forEach((EntityWrapper entity) {
buttons.add(
SizedBox(
width: buttonWidth,
child: EntityModel(
entityWrapper: entity,
child: GlanceCardEntityContainer(
showName: card.showName,
showState: card.showState,
),
handleTap: true
),
)
);
});
return Wrap(
//spacing: 5.0,
//alignment: WrapAlignment.spaceEvenly,
runSpacing: Sizes.doubleRowPadding,
children: buttons,
);
}
),
),
)
);
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: rows
)
);
}
Widget _buildMediaControlsCard(BuildContext context) {
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: MediaPlayerWidget()
)
);
}
Widget _buildEntityButtonCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName.toUpperCase();
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: EntityButtonCardBody(),
handleTap: true
)
);
}
Widget _buildGaugeCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name ??
card.linkedEntityWrapper.displayName;
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
card.linkedEntityWrapper.unitOfMeasurement;
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: GaugeCardBody(
min: card.min,
max: card.max,
severity: card.severity,
),
handleTap: true
)
);
}
Widget _buildLightCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name ??
card.linkedEntityWrapper.displayName;
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: LightCardBody(
min: card.min,
max: card.max,
severity: card.severity,
),
handleTap: true
)
);
}
Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeader(name: card.name ?? ""));
List<Widget> result = [];
if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: true,
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
),
)
]);
} else {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
),
]);
}
body.addAll(result);
return Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: body
)
);
}
}

View File

@ -0,0 +1,29 @@
part of '../../main.dart';
class CardHeader extends StatelessWidget {
final String name;
final Widget trailing;
final Widget subtitle;
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
@override
Widget build(BuildContext context) {
var result;
if ((name != null) && (name.trim().length > 0)) {
result = new ListTile(
trailing: trailing,
subtitle: subtitle,
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,51 @@
part of '../../main.dart';
class EntityButtonCardBody extends StatelessWidget {
EntityButtonCardBody({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: FractionallySizedBox(
widthFactor: 1,
child: Column(
children: <Widget>[
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
size: constraints.maxWidth / 2.5,
);
}
),
_buildName()
],
),
),
);
}
Widget _buildName() {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
wordsWrap: true,
textAlign: TextAlign.center,
fontSize: Sizes.nameFontSize,
);
}
}

View File

@ -0,0 +1,153 @@
part of '../../main.dart';
class GaugeCardBody extends StatefulWidget {
final int min;
final int max;
final Map severity;
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
_GaugeCardBodyState createState() => _GaugeCardBodyState();
}
class _GaugeCardBodyState extends State<GaugeCardBody> {
List<charts.Series> seriesList;
List<charts.Series<GaugeSegment, String>> _createData(double value) {
double fixedValue;
if (value > widget.max) {
fixedValue = widget.max.toDouble();
} else if (value < widget.min) {
fixedValue = widget.min.toDouble();
} else {
fixedValue = value;
}
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
Color mainColor;
if (widget.severity != null) {
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
mainColor = Colors.red;
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
mainColor = Colors.amber;
} else {
mainColor = Colors.green;
}
} else {
mainColor = Colors.green;
}
final data = [
GaugeSegment('Main', toShow, mainColor),
GaugeSegment('Rest', 100 - toShow, Colors.black45),
];
return [
charts.Series<GaugeSegment, String>(
id: 'Segments',
domainFn: (GaugeSegment segment, _) => segment.segment,
measureFn: (GaugeSegment segment, _) => segment.value,
colorFn: (GaugeSegment segment, _) => segment.color,
// Set a label accessor to control the text of the arc label.
labelAccessorFn: (GaugeSegment segment, _) =>
segment.segment == 'Main' ? '${segment.value}' : null,
data: data,
)
];
}
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: AspectRatio(
aspectRatio: 1.5,
child: Stack(
fit: StackFit.expand,
overflow: Overflow.clip,
children: [
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double verticalOffset;
if(constraints.maxWidth > 150.0) {
verticalOffset = 0.2;
} else if (constraints.maxWidth > 100.0) {
verticalOffset = 0.3;
} else {
verticalOffset = 0.3;
}
return FractionallySizedBox(
heightFactor: 2,
widthFactor: 1,
alignment: FractionalOffset(0,verticalOffset),
child: charts.PieChart(
_createData(entityWrapper.entity.doubleState),
animate: false,
defaultRenderer: charts.ArcRendererConfig(
arcRatio: 0.4,
startAngle: pi,
arcLength: pi,
),
),
);
}
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: 2*fontSize),
child: SimpleEntityState(
//textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
bold: true,
textAlign: TextAlign.center,
padding: EdgeInsets.all(0.0),
fontSize: fontSize,
//padding: EdgeInsets.only(top: Sizes.rowPadding),
),
);
}
),
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: fontSize),
child: EntityName(
fontSize: fontSize,
maxLines: 1,
padding: EdgeInsets.all(0.0),
textAlign: TextAlign.center,
textOverflow: TextOverflow.ellipsis,
),
);
}
),
)
]
)
),
);
}
}
class GaugeSegment {
final String segment;
final double value;
final charts.Color color;
GaugeSegment(this.segment, this.value, Color color)
: this.color = charts.Color(
r: color.red, g: color.green, b: color.blue, a: color.alpha);
}

View File

@ -0,0 +1,85 @@
part of '../../main.dart';
class GlanceCardEntityContainer extends StatelessWidget {
final bool showName;
final bool showState;
final bool nameInTheBottom;
final double iconSize;
final double nameFontSize;
final bool wordsWrapInName;
GlanceCardEntityContainer({
Key key,
@required this.showName,
@required this.showState,
this.nameInTheBottom: false,
this.iconSize: Sizes.iconSize,
this.nameFontSize: Sizes.smallFontSize,
this.wordsWrapInName: false
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = [];
if (!nameInTheBottom) {
if (showName) {
result.add(_buildName());
}
} else {
if (showState) {
result.add(_buildState());
}
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
)
);
if (!nameInTheBottom) {
if (showState) {
result.add(_buildState());
}
} else {
result.add(_buildName());
}
return Center(
child: InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
),
);
}
Widget _buildName() {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: wordsWrapInName,
textAlign: TextAlign.center,
fontSize: nameFontSize,
);
}
Widget _buildState() {
return SimpleEntityState(
textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
padding: EdgeInsets.only(top: Sizes.rowPadding),
);
}
}

View File

@ -0,0 +1,90 @@
part of '../../main.dart';
class LightCardBody extends StatefulWidget {
final int min;
final int max;
final Map severity;
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
_LightCardBodyState createState() => _LightCardBodyState();
}
class _LightCardBodyState extends State<LightCardBody> {
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
LightEntity entity = entityWrapper.entity;
Logger.d("Light brightness: ${entity.brightness}");
return FractionallySizedBox(
widthFactor: 0.5,
child: Container(
//color: Colors.redAccent,
child: SingleCircularSlider(
255,
entity.brightness ?? 0,
baseColor: Colors.white,
handlerColor: Colors.blue[200],
selectionColor: Colors.blue[100],
),
),
);
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: AspectRatio(
aspectRatio: 1.5,
child: Stack(
fit: StackFit.expand,
overflow: Overflow.clip,
children: [
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: 2*fontSize),
child: SimpleEntityState(
//textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
bold: true,
textAlign: TextAlign.center,
padding: EdgeInsets.all(0.0),
fontSize: fontSize,
//padding: EdgeInsets.only(top: Sizes.rowPadding),
),
);
}
),
),
Align(
alignment: Alignment.bottomCenter,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSize = constraints.maxHeight / 7;
return Padding(
padding: EdgeInsets.only(bottom: fontSize),
child: EntityName(
fontSize: fontSize,
maxLines: 1,
padding: EdgeInsets.all(0.0),
textAlign: TextAlign.center,
textOverflow: TextOverflow.ellipsis,
),
);
}
),
)
]
)
),
);
}
}

125
lib/const.dart Normal file
View File

@ -0,0 +1,125 @@
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';
static const active = 'active';
static const cleaning = 'cleaning';
static const docked = 'docked';
static const returning = 'returning';
static const error = 'error';
}
class EntityUIAction {
static const moreInfo = 'more-info';
static const toggle = 'toggle';
static const callService = 'call-service';
static const navigate = 'navigate';
static const none = 'none';
String tapAction = EntityUIAction.moreInfo;
String tapNavigationPath;
String tapService;
Map<String, dynamic> tapServiceData;
String holdAction = EntityUIAction.none;
String holdNavigationPath;
String holdService;
Map<String, dynamic> holdServiceData;
EntityUIAction({rawEntityData}) {
if (rawEntityData != null) {
if (rawEntityData["tap_action"] != null) {
if (rawEntityData["tap_action"] is String) {
tapAction = rawEntityData["tap_action"];
} else {
tapAction =
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
tapService = rawEntityData["tap_action"]["service"];
tapServiceData = rawEntityData["tap_action"]["service_data"];
}
}
if (rawEntityData["hold_action"] != null) {
if (rawEntityData["hold_action"] is String) {
holdAction = rawEntityData["hold_action"];
} else {
holdAction =
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
holdService = rawEntityData["hold_action"]["service"];
holdServiceData = rawEntityData["hold_action"]["service_data"];
}
}
}
}
}
class CardType {
static const HORIZONTAL_STACK = "horizontal-stack";
static const VERTICAL_STACK = "vertical-stack";
static const ENTITIES = "entities";
static const GLANCE = "glance";
static const MEDIA_CONTROL = "media-control";
static const WEATHER_FORECAST = "weather-forecast";
static const THERMOSTAT = "thermostat";
static const SENSOR = "sensor";
static const PLANT_STATUS = "plant-status";
static const PICTURE_ENTITY = "picture-entity";
static const PICTURE_ELEMENTS = "picture-elements";
static const PICTURE = "picture";
static const MAP = "map";
static const IFRAME = "iframe";
static const GAUGE = "gauge";
static const ENTITY_BUTTON = "entity-button";
static const CONDITIONAL = "conditional";
static const ALARM_PANEL = "alarm-panel";
static const MARKDOWN = "markdown";
static const LIGHT = "light";
}
class Sizes {
static const rightWidgetPadding = 10.0;
static const leftWidgetPadding = 10.0;
static const buttonPadding = 4.0;
static const extendedWidgetHeight = 50.0;
static const iconSize = 28.0;
static const largeIconSize = 46.0;
static const stateFontSize = 15.0;
static const nameFontSize = 15.0;
static const smallFontSize = 14.0;
static const largeFontSize = 24.0;
static const inputWidth = 160.0;
static const rowPadding = 10.0;
static const doubleRowPadding = rowPadding*2;
static const minViewColumnWidth = 350;
static const entityPageMaxWidth = 400.0;
static const mainPageScreenSeparatorWidth = 5.0;
static const tabletMinWidth = minViewColumnWidth + entityPageMaxWidth + 5;
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -0,0 +1,145 @@
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.getIconDataFromIconCode(0xf0dc),
size: iconSize,
)
: Icon(
MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
size: iconSize,
);
break;
}
case "camera":
case "media_player":
case "binary_sensor":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
break;
}
case "device_tracker":
case "person":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
);
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
break;
}
default:
{
double stateFontSize;
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
stateFontSize = 18.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
stateFontSize = 15.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
stateFontSize = 10.0;
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
stateFontSize = 8.0;
}
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
badgeIcon = Center(
child: Text(
"${entityModel.entityWrapper.entity.displayState}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: stateFontSize),
),
);
break;
}
}
Widget onBadgeText;
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
onBadgeText = Container(width: 0.0, height: 0.0);
} else {
onBadgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$onBadgeTextValue",
style: 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(entity: entityModel.entityWrapper.entity)));
}
}

View File

@ -0,0 +1,16 @@
part of '../../main.dart';
class ButtonEntity extends Entity {
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
@override
Widget _buildStatePart(BuildContext context) {
return FlatServiceButton(
entityId: entityId,
serviceDomain: domain,
serviceName: 'turn_on',
text: domain == "scene" ? "ACTIVATE" : "EXECUTE",
);
}
}

View File

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

View File

@ -0,0 +1,58 @@
part of '../../../main.dart';
class CameraStreamView extends StatefulWidget {
CameraStreamView({Key key}) : super(key: key);
@override
_CameraStreamViewState createState() => _CameraStreamViewState();
}
class _CameraStreamViewState extends State<CameraStreamView> {
@override
void initState() {
super.initState();
}
CameraEntity _entity;
bool started = false;
String streamUrl = "";
launchStream() {
Launcher.launchURLInCustomTab(
context: context,
url: streamUrl
);
}
@override
Widget build(BuildContext context) {
if (!started) {
_entity = EntityModel
.of(context)
.entityWrapper
.entity;
started = true;
}
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
.entityId}?token=${_entity.attributes['access_token']}';
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.all(20.0),
child: IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
iconSize: 50.0,
onPressed: () => launchStream(),
)
)
],
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -0,0 +1,114 @@
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_RANGE = 2;
static const SUPPORT_TARGET_HUMIDITY = 4;
static const SUPPORT_FAN_MODE = 8;
static const SUPPORT_PRESET_MODE = 16;
static const SUPPORT_SWING_MODE = 32;
static const SUPPORT_AUX_HEAT = 64;
//static const SUPPORT_OPERATION_MODE = 16;
//static const SUPPORT_HOLD_MODE = 256;
//static const SUPPORT_AWAY_MODE = 1024;
//static const SUPPORT_ON_OFF = 4096;
ClimateEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportTargetTemperature => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
bool get supportTargetTemperatureRange => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE);
bool get supportTargetHumidity => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
bool get supportFanMode =>
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
ClimateEntity.SUPPORT_FAN_MODE);
bool get supportSwingMode =>
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE);
bool get supportPresetMode =>
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
ClimateEntity.SUPPORT_PRESET_MODE);
bool get supportAuxHeat =>
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
ClimateEntity.SUPPORT_AUX_HEAT);
List<String> get hvacModes => attributes["hvac_modes"] != null
? (attributes["hvac_modes"] as List).cast<String>()
: null;
List<String> get fanModes => attributes["fan_modes"] != null
? (attributes["fan_modes"] as List).cast<String>()
: null;
List<String> get presetModes => attributes["preset_modes"] != null
? (attributes["preset_modes"] as List).cast<String>()
: null;
List<String> get swingModes => attributes["swing_modes"] != null
? (attributes["swing_modes"] as List).cast<String>()
: null;
double get temperature => _getDoubleAttributeValue('temperature');
double get currentTemperature => _getDoubleAttributeValue('current_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');
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
String get hvacAction => attributes['hvac_action'];
String get fanMode => attributes['fan_mode'];
String get presetMode => attributes['preset_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";
@override
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
if (supportTargetTemperature) {
historyConfig.numericAttributesToShow.add("temperature");
}
if (supportTargetTemperatureRange) {
historyConfig.numericAttributesToShow.add("target_temp_high");
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,421 @@
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;
Timer _tempThrottleTimer;
Timer _targetTempThrottleTimer;
double _tmpTemperature = 0.0;
double _tmpTargetLow = 0.0;
double _tmpTargetHigh = 0.0;
double _tmpTargetHumidity = 0.0;
String _tmpHVACMode;
String _tmpFanMode;
String _tmpSwingMode;
String _tmpPresetMode;
//bool _tmpIsOff = false;
bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
_tmpHVACMode = entity.state;
_tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode;
_tmpPresetMode = entity.presetMode;
//_tmpIsOff = entity.isOff;
_tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity;
_showPending = false;
_changedHere = false;
}
void _temperatureUp(ClimateEntity entity) {
_tmpTemperature = ((_tmpTemperature + entity.temperatureStep) <= entity.maxTemp) ? _tmpTemperature + entity.temperatureStep : entity.maxTemp;
_setTemperature(entity);
}
void _temperatureDown(ClimateEntity entity) {
_tmpTemperature = ((_tmpTemperature - entity.temperatureStep) >= entity.minTemp) ? _tmpTemperature - entity.temperatureStep : entity.minTemp;
_setTemperature(entity);
}
void _targetLowUp(ClimateEntity entity) {
_tmpTargetLow = ((_tmpTargetLow + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetLow + entity.temperatureStep : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetLowDown(ClimateEntity entity) {
_tmpTargetLow = ((_tmpTargetLow - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetLow - entity.temperatureStep : entity.minTemp;
_setTargetTemp(entity);
}
void _targetHighUp(ClimateEntity entity) {
_tmpTargetHigh = ((_tmpTargetHigh + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetHigh + entity.temperatureStep : entity.maxTemp;
_setTargetTemp(entity);
}
void _targetHighDown(ClimateEntity entity) {
_tmpTargetHigh = ((_tmpTargetHigh - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetHigh - entity.temperatureStep : entity.minTemp;
_setTargetTemp(entity);
}
void _setTemperature(ClimateEntity entity) {
if (_tempThrottleTimer!=null) {
_tempThrottleTimer.cancel();
}
setState(() {
_changedHere = true;
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
});
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() {
_changedHere = true;
ConnectionManager().callService(
entity.domain,
"set_temperature",
entity.entityId,
{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
);
_resetStateTimer(entity);
});
});
}
void _setTargetTemp(ClimateEntity entity) {
if (_targetTempThrottleTimer!=null) {
_targetTempThrottleTimer.cancel();
}
setState(() {
_changedHere = true;
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
});
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() {
_changedHere = true;
ConnectionManager().callService(
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;
ConnectionManager().callService(entity.domain, "set_humidity", entity.entityId, {"humidity": "$_tmpTargetHumidity"});
_resetStateTimer(entity);
});
}
void _setHVACMode(ClimateEntity entity, value) {
setState(() {
_tmpHVACMode = value;
_changedHere = true;
ConnectionManager().callService(entity.domain, "set_hvac_mode", entity.entityId, {"hvac_mode": "$_tmpHVACMode"});
_resetStateTimer(entity);
});
}
void _setSwingMode(ClimateEntity entity, value) {
setState(() {
_tmpSwingMode = value;
_changedHere = true;
ConnectionManager().callService(entity.domain, "set_swing_mode", entity.entityId, {"swing_mode": "$_tmpSwingMode"});
_resetStateTimer(entity);
});
}
void _setFanMode(ClimateEntity entity, value) {
setState(() {
_tmpFanMode = value;
_changedHere = true;
ConnectionManager().callService(entity.domain, "set_fan_mode", entity.entityId, {"fan_mode": "$_tmpFanMode"});
_resetStateTimer(entity);
});
}
void _setPresetMode(ClimateEntity entity, value) {
setState(() {
_tmpPresetMode = value;
_changedHere = true;
ConnectionManager().callService(entity.domain, "set_preset_mode", entity.entityId, {"preset_mode": "$_tmpPresetMode"});
_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;
ConnectionManager().callService(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 || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
_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),
_buildPresetModeControl(entity),
_buildAuxHeatControl(entity)
],
),
);
}
Widget _buildPresetModeControl(ClimateEntity entity) {
if (entity.supportPresetMode) {
return ModeSelectorWidget(
options: entity.presetModes,
onChange: (mode) => _setPresetMode(entity, mode),
caption: "Preset",
value: _tmpPresetMode,
);
} 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.hvacModes != null) {
return ModeSelectorWidget(
onChange: (mode) => _setHVACMode(entity, mode),
options: entity.hvacModes,
caption: "Operation",
value: _tmpHVACMode,
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildFanControl(ClimateEntity entity) {
if (entity.supportFanMode) {
return ModeSelectorWidget(
options: entity.fanModes,
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.swingModes,
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,
onDec: () => _temperatureDown(entity),
onInc: () => _temperatureUp(entity),
)
],
);
} else {
return Container(width: 0.0, height: 0.0,);
}
}
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
List<Widget> controls = [];
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
controls.addAll(<Widget>[
TemperatureControlWidget(
value: _tmpTargetLow,
fontColor: _showPending ? Colors.red : Colors.black,
onDec: () => _targetLowDown(entity),
onInc: () => _targetLowUp(entity),
),
Expanded(
child: Container(height: 10.0),
)
]);
}
if ((entity.supportTargetTemperatureRange) && (entity.targetHigh != null)) {
controls.add(
TemperatureControlWidget(
value: _tmpTargetHigh,
fontColor: _showPending ? Colors.red : Colors.black,
onDec: () => _targetHighDown(entity),
onInc: () => _targetHighUp(entity),
)
);
}
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();
}
}

View File

@ -0,0 +1,58 @@
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.supportTargetTemperatureRange) &&
(entity.targetLow != null) &&
(entity.targetHigh != null)) {
targetTemp = "${entity.targetLow} - ${entity.targetHigh}";
}
String displayState = '';
if (entity.hvacAction != null) {
displayState = "${entity.hvacAction} (${entity.displayState})";
} else {
displayState = "${entity.displayState}";
}
if (entity.presetMode != null) {
displayState += " - ${entity.presetMode}";
}
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("$displayState",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: Sizes.stateFontSize,
)),
Text(" $targetTemp",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
))
],
),
entity.currentTemperature != null ?
Text("Currently: ${entity.currentTemperature}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
color: Colors.black45)
) :
Container(height: 0.0,)
],
));
}
}

View File

@ -0,0 +1,64 @@
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 onChange;
final EdgeInsets padding;
ModeSelectorWidget({
Key key,
@required this.caption,
@required this.options,
this.value,
@required this.onChange,
this.captionFontSize,
this.valueFontSize,
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: padding,
child: 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),
),
),
)
],
)
],
),
);
}
}

View File

@ -0,0 +1,53 @@
part of '../../../main.dart';
class ModeSwitchWidget extends StatelessWidget {
final String caption;
final onChange;
final double captionFontSize;
final bool value;
final bool expanded;
final EdgeInsets padding;
ModeSwitchWidget({
Key key,
@required this.caption,
@required this.onChange,
this.captionFontSize,
this.value,
this.expanded: true,
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: this.padding,
child: 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,50 @@
part of '../../../main.dart';
class TemperatureControlWidget extends StatelessWidget {
final double value;
final double fontSize;
final Color fontColor;
final onInc;
final onDec;
TemperatureControlWidget(
{Key key,
@required this.value,
@required this.onInc,
@required this.onDec,
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.getIconDataFromIconName(
'mdi:chevron-up')),
iconSize: 30.0,
onPressed: () => onInc(),
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
'mdi:chevron-down')),
iconSize: 30.0,
onPressed: () => onDec(),
)
],
)
],
);
}
}

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;
CoverEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportOpen => ((supportedFeatures &
CoverEntity.SUPPORT_OPEN) ==
CoverEntity.SUPPORT_OPEN);
bool get supportClose => ((supportedFeatures &
CoverEntity.SUPPORT_CLOSE) ==
CoverEntity.SUPPORT_CLOSE);
bool get supportSetPosition => ((supportedFeatures &
CoverEntity.SUPPORT_SET_POSITION) ==
CoverEntity.SUPPORT_SET_POSITION);
bool get supportStop => ((supportedFeatures &
CoverEntity.SUPPORT_STOP) ==
CoverEntity.SUPPORT_STOP);
bool get supportOpenTilt => ((supportedFeatures &
CoverEntity.SUPPORT_OPEN_TILT) ==
CoverEntity.SUPPORT_OPEN_TILT);
bool get supportCloseTilt => ((supportedFeatures &
CoverEntity.SUPPORT_CLOSE_TILT) ==
CoverEntity.SUPPORT_CLOSE_TILT);
bool get supportStopTilt => ((supportedFeatures &
CoverEntity.SUPPORT_STOP_TILT) ==
CoverEntity.SUPPORT_STOP_TILT);
bool get supportSetTiltPosition => ((supportedFeatures &
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;
@override
Widget _buildStatePart(BuildContext context) {
return CoverStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return CoverControlWidget();
}
}

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;
ConnectionManager().callService(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()});
});
}
void _setNewTiltPosition(CoverEntity entity, double position) {
setState(() {
_tmpTiltPosition = position.roundToDouble();
_changedHere = true;
ConnectionManager().callService(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) {
ConnectionManager().callService(
entity.domain, "open_cover_tilt", entity.entityId, null);
}
void _close(CoverEntity entity) {
ConnectionManager().callService(
entity.domain, "close_cover_tilt", entity.entityId, null);
}
void _stop(CoverEntity entity) {
ConnectionManager().callService(
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.getIconDataFromIconName(
"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.getIconDataFromIconName("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.getIconDataFromIconName(
"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,65 @@
part of '../../../main.dart';
class CoverStateWidget extends StatelessWidget {
void _open(CoverEntity entity) {
ConnectionManager().callService(
entity.domain, "open_cover", entity.entityId, null);
}
void _close(CoverEntity entity) {
ConnectionManager().callService(
entity.domain, "close_cover", entity.entityId, null);
}
void _stop(CoverEntity entity) {
ConnectionManager().callService(
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.getIconDataFromIconName("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.getIconDataFromIconName("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.getIconDataFromIconName("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,41 @@
part of '../../main.dart';
class DateTimeEntity extends Entity {
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
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();
@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(Map newValue) {
ConnectionManager().callService(domain, "set_datetime", entityId, newValue);
}
}

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 {
Logger.w( "${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,70 @@
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);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
return Divider(
color: Colors.black45,
);
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Divider(
color: Colors.black45,
),
Text(
"${entityModel.entityWrapper.entity.displayName}",
style: TextStyle(color: Colors.blue),
)
],
);
}
Widget result = Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
EntityIcon(),
Flexible(
fit: FlexFit.tight,
flex: 3,
child: EntityName(
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
),
),
state
],
);
if (entityModel.handleTap) {
return InkWell(
onLongPress: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleHold();
}
},
onTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleTap();
}
},
child: result,
);
} else {
return result;
}
}
}

View File

@ -0,0 +1,257 @@
part of '../main.dart';
class StatelessEntityType {
static const NONE = 0;
static const MISSED = 1;
static const DIVIDER = 2;
static const SECTION = 3;
static const CALL_SERVICE = 4;
static const WEBLINK = 5;
}
class Entity {
static List badgeDomains = [
"alarm_control_panel",
"binary_sensor",
"device_tracker",
"updater",
"sun",
"timer",
"sensor"
];
static Map StateByDeviceClass = {
"battery.on": "Low",
"battery.off": "Normal",
"cold.on": "Cold",
"cold.off": "Normal",
"connectivity.on": "Connected",
"connectivity.off": "Disconnected",
"door.on": "Open",
"door.off": "Closed",
"garage_door.on": "Open",
"garage_door.off": "Closed",
"gas.on": "Detected",
"gas.off": "Clear",
"heat.on": "Hot",
"heat.off": "Normal",
"light.on": "Detected",
"lignt.off": "No light",
"lock.on": "Unlocked",
"lock.off": "Locked",
"moisture.on": "Wet",
"moisture.off": "Dry",
"motion.on": "Detected",
"motion.off": "Clear",
"moving.on": "Moving",
"moving.off": "Stopped",
"occupancy.on": "Occupied",
"occupancy.off": "Clear",
"opening.on": "Open",
"opening.off": "Closed",
"plug.on": "Plugged in",
"plug.off": "Unplugged",
"power.on": "Powered",
"power.off": "No power",
"presence.on": "Home",
"presence.off": "Away",
"problem.on": "Problem",
"problem.off": "OK",
"safety.on": "Unsafe",
"safety.off": "Safe",
"smoke.on": "Detected",
"smoke.off": "Clear",
"sound.on": "Detected",
"sound.off": "Clear",
"vibration.on": "Detected",
"vibration.off": "Clear",
"window.on": "Open",
"window.off": "Closed"
};
Map attributes;
String domain;
String entityId;
String entityPicture;
String state;
String displayState;
DateTime _lastUpdated;
int statelessType = 0;
List<Entity> childEntities = [];
String deviceClass;
EntityHistoryConfig historyConfig = EntityHistoryConfig(
chartType: EntityHistoryWidgetType.simple
);
String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
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 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;
int get supportedFeatures => attributes["supported_features"] ?? 0;
String _getEntityPictureUrl(String webHost) {
String result = attributes["entity_picture"];
if (result == null) return result;
if (!result.startsWith("http")) {
if (result.startsWith("/")) {
result = "$webHost$result";
} else {
result = "$webHost/$result";
}
}
return result;
}
Entity(Map rawData, String webHost) {
update(rawData, webHost);
}
Entity.missed(String entityId) {
statelessType = StatelessEntityType.MISSED;
attributes = {"hidden": false};
this.entityId = entityId;
}
Entity.divider() {
statelessType = StatelessEntityType.DIVIDER;
attributes = {"hidden": false};
}
Entity.section(String label) {
statelessType = StatelessEntityType.SECTION;
attributes = {"hidden": false, "friendly_name": "$label"};
}
Entity.callService({String icon, String name, String service, String actionName}) {
statelessType = StatelessEntityType.CALL_SERVICE;
entityId = service;
displayState = actionName?.toUpperCase() ?? "RUN";
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
}
Entity.weblink({String url, String name, String icon}) {
statelessType = StatelessEntityType.WEBLINK;
entityId = "custom.custom"; //TODO wtf??
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
}
void update(Map rawData, String webHost) {
attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
}
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 _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 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,77 @@
part of '../main.dart';
class EntityColor {
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
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.active: Colors.amber,
EntityState.playing: Colors.amber,
EntityState.paused: Colors.amber,
"above_horizon": Colors.amber,
EntityState.home: Colors.amber,
EntityState.open: Colors.amber,
EntityState.cleaning: Colors.amber,
EntityState.returning: Colors.amber,
EntityState.off: defaultStateColor,
EntityState.closed: defaultStateColor,
"below_horizon": defaultStateColor,
"default": defaultStateColor,
EntityState.idle: defaultStateColor,
"heat": Colors.redAccent,
"cool": Colors.lightBlue,
EntityState.unavailable: Colors.black26,
EntityState.unknown: Colors.black26,
EntityState.alarm_disarmed: Colors.green,
EntityState.alarm_armed_away: Colors.redAccent,
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
EntityState.alarm_armed_home: Colors.redAccent,
EntityState.alarm_armed_night: Colors.redAccent,
EntityState.alarm_triggered: Colors.redAccent,
EntityState.alarm_arming: Colors.amber,
EntityState.alarm_disarming: Colors.amber,
EntityState.alarm_pending: Colors.amber,
};
static Color stateColor(String state) {
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,74 @@
part of '../main.dart';
class EntityIcon extends StatelessWidget {
final EdgeInsetsGeometry padding;
final double size;
final Color color;
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
String domain = entityId.split(".")[0];
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
String iconNameByDeviceClass;
if (deviceClass != null) {
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
}
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
if (iconName != null) {
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
} else {
return 0;
}
}
Widget buildIcon(EntityWrapper data, Color color) {
if (data == null) {
return null;
}
if (data.entityPicture != null) {
return Container(
height: size+12,
width: size+12,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit:BoxFit.cover,
image: CachedNetworkImageProvider(
"${data.entityPicture}"
),
)
),
);
}
String iconName = data.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
return Padding(
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
)
);
}
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: buildIcon(
entityWrapper,
color ?? EntityColor.stateColor(entityWrapper.entity.state)
),
);
}
}

View File

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

View File

@ -0,0 +1,33 @@
part of '../main.dart';
class EntityName extends StatelessWidget {
final EdgeInsetsGeometry padding;
final TextOverflow textOverflow;
final bool wordsWrap;
final double fontSize;
final TextAlign textAlign;
final int maxLines;
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
TextStyle textStyle = TextStyle(fontSize: fontSize);
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
}
return Padding(
padding: padding,
child: Text(
"${entityWrapper.displayName}",
overflow: textOverflow,
softWrap: wordsWrap,
maxLines: maxLines,
style: textStyle,
textAlign: textAlign,
),
);
}
}

View File

@ -0,0 +1,70 @@
part of '../main.dart';
class EntityPageLayout extends StatelessWidget {
final bool showClose;
final Entity entity;
EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key);
@override
Widget build(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: entity),
child: ListView(
padding: EdgeInsets.all(0),
children: <Widget>[
showClose ?
Container(
color: Colors.blue[300],
height: 40,
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8),
child: Text(
entity.displayName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 22
),
),
),
),
IconButton(
padding: EdgeInsets.all(0),
icon: Icon(Icons.close),
color: Colors.white,
iconSize: 36.0,
onPressed: () {
eventBus.fire(ShowEntityPageEvent());
},
)
],
),
) :
Container(height: 0, width: 0,),
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),
),
LastUpdatedWidget(),
Divider(),
entity._buildAdditionalControlsForPage(context),
Divider(),
SpoilerCard(
title: "State history",
body: EntityHistoryWidget(),
),
SpoilerCard(
title: "Attributes",
body: EntityAttributesList(),
),
]
),
handleTap: false,
);
}
}

View File

@ -0,0 +1,112 @@
part of '../main.dart';
class EntityWrapper {
String displayName;
String icon;
String unitOfMeasurement;
String entityPicture;
EntityUIAction uiAction;
Entity entity;
EntityWrapper({
this.entity,
String icon,
String displayName,
this.uiAction
}) {
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
this.icon = icon ?? entity.icon;
if (icon == null) {
entityPicture = entity.entityPicture;
}
this.displayName = displayName ?? entity.displayName;
if (uiAction == null) {
uiAction = EntityUIAction();
}
unitOfMeasurement = entity.unitOfMeasurement;
}
}
void handleTap() {
switch (uiAction.tapAction) {
case EntityUIAction.toggle: {
ConnectionManager().callService("homeassistant", "toggle", entity.entityId, null);
break;
}
case EntityUIAction.callService: {
if (uiAction.tapService != null) {
ConnectionManager().callService(uiAction.tapService.split(".")[0],
uiAction.tapService.split(".")[1], null,
uiAction.tapServiceData);
}
break;
}
case EntityUIAction.none: {
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.tapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
Launcher.launchURL(uiAction.tapService);
}
break;
}
default: {
break;
}
}
}
void handleHold() {
switch (uiAction.holdAction) {
case EntityUIAction.toggle: {
ConnectionManager().callService("homeassistant", "toggle", entity.entityId, null);
break;
}
case EntityUIAction.callService: {
if (uiAction.holdService != null) {
ConnectionManager().callService(uiAction.holdService.split(".")[0],
uiAction.holdService.split(".")[1], null,
uiAction.holdServiceData);
}
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
Launcher.launchURL(uiAction.holdService);
}
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, String webHost) : super(rawData, webHost);
bool get supportSetSpeed => ((supportedFeatures &
FanEntity.SUPPORT_SET_SPEED) ==
FanEntity.SUPPORT_SET_SPEED);
bool get supportOscillate => ((supportedFeatures &
FanEntity.SUPPORT_OSCILLATE) ==
FanEntity.SUPPORT_OSCILLATE);
bool get supportDirection => ((supportedFeatures &
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,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;
ConnectionManager().callService(
"fan", "oscillate", entity.entityId,
{"oscillating": oscillate});
});
}
void _setDirection(FanEntity entity, bool forward) {
setState(() {
_tmpDirectionForward = forward;
_changedHere = true;
ConnectionManager().callService(
"fan", "set_direction", entity.entityId,
{"direction": forward ? "forward" : "reverse"});
});
}
void _setSpeed(FanEntity entity, String value) {
setState(() {
_tmpSpeed = value;
_changedHere = true;
ConnectionManager().callService(
"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,41 @@
part of '../main.dart';
class FlatServiceButton extends StatelessWidget {
final String serviceDomain;
final String serviceName;
final String entityId;
final String text;
final double fontSize;
FlatServiceButton({
Key key,
@required this.serviceDomain,
@required this.serviceName,
@required this.entityId,
@required this.text,
this.fontSize: Sizes.stateFontSize
}) : super(key: key);
void _setNewState() {
ConnectionManager().callService(serviceDomain, serviceName, entityId, null);
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: fontSize*2.5,
child: FlatButton(
onPressed: (() {
_setNewState();
}),
child: Text(
text,
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: fontSize, color: Colors.blue),
),
)
);
}
}

View File

@ -0,0 +1,44 @@
part of '../../main.dart';
class GroupEntity extends Entity {
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
String mutualDomain;
bool switchable = false;
GroupEntity(Map rawData, String webHost) : super(rawData, webHost);
@override
Widget _buildStatePart(BuildContext context) {
if (switchable) {
return SwitchStateWidget(
domainForService: "homeassistant",
);
} else {
return super._buildStatePart(context);
}
}
@override
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
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,79 @@
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 => ((supportedFeatures &
LightEntity.SUPPORT_BRIGHTNESS) ==
LightEntity.SUPPORT_BRIGHTNESS);
bool get supportColorTemp => ((supportedFeatures &
LightEntity.SUPPORT_COLOR_TEMP) ==
LightEntity.SUPPORT_COLOR_TEMP);
bool get supportEffect => ((supportedFeatures &
LightEntity.SUPPORT_EFFECT) ==
LightEntity.SUPPORT_EFFECT);
bool get supportFlash => ((supportedFeatures &
LightEntity.SUPPORT_FLASH) ==
LightEntity.SUPPORT_FLASH);
bool get supportColor => ((supportedFeatures &
LightEntity.SUPPORT_COLOR) ==
LightEntity.SUPPORT_COLOR);
bool get supportTransition => ((supportedFeatures &
LightEntity.SUPPORT_TRANSITION) ==
LightEntity.SUPPORT_TRANSITION);
bool get supportWhiteValue => ((supportedFeatures &
LightEntity.SUPPORT_WHITE_VALUE) ==
LightEntity.SUPPORT_WHITE_VALUE);
int get brightness => _getIntAttributeValue("brightness");
int get whiteValue => _getIntAttributeValue("white_value");
String get effect => attributes["effect"];
int get colorTemp => _getIntAttributeValue("color_temp");
double get maxMireds => _getDoubleAttributeValue("max_mireds");
double get minMireds => _getDoubleAttributeValue("min_mireds");
HSVColor get color => _getColor();
bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0));
List<String> get effectList => getStringListAttributeValue("effect_list");
LightEntity(Map rawData, String webHost) : super(rawData, webHost);
HSVColor _getColor() {
List hs = attributes["hs_color"];
List rgb = attributes["rgb_color"];
try {
if (hs != null && hs.isNotEmpty) {
double sat = hs[1]/100;
String ssat = sat.toStringAsFixed(2);
return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0);
} else if (rgb != null && rgb.isNotEmpty) {
return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2]));
} else {
return null;
}
} catch (e) {
return null;
}
}
@override
Widget _buildStatePart(BuildContext context) {
return SwitchStateWidget();
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
if (!isAdditionalControls || state == EntityState.unavailable) {
return Container(height: 0.0, width: 0.0);
} else {
return LightControlsWidget();
}
}
}

View File

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

View File

@ -0,0 +1,246 @@
part of '../../../main.dart';
class LightControlsWidget extends StatefulWidget {
@override
_LightControlsWidgetState createState() => _LightControlsWidgetState();
}
class _LightControlsWidgetState extends State<LightControlsWidget> {
int _tmpBrightness;
int _tmpWhiteValue;
int _tmpColorTemp = 0;
HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0);
bool _changedHere = false;
String _tmpEffect;
void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 1;
_tmpWhiteValue = entity.whiteValue ?? 0;
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
_tmpColor = entity.color ?? _tmpColor;
_tmpEffect = entity.effect;
}
void _setBrightness(LightEntity entity, double value) {
setState(() {
_tmpBrightness = value.round();
_changedHere = true;
ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId,
{"brightness": _tmpBrightness});
});
}
void _setWhiteValue(LightEntity entity, double value) {
setState(() {
_tmpWhiteValue = value.round();
_changedHere = true;
ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId,
{"white_value": _tmpWhiteValue});
});
}
void _setColorTemp(LightEntity entity, double value) {
setState(() {
_tmpColorTemp = value.round();
_changedHere = true;
ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId,
{"color_temp": _tmpColorTemp});
});
}
void _setColor(LightEntity entity, HSVColor color) {
setState(() {
_tmpColor = color;
_changedHere = true;
Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
ConnectionManager().callService(
entity.domain, "turn_on", entity.entityId,
{"hs_color": [color.hue, color.saturation*100]});
});
}
void _setEffect(LightEntity entity, String value) {
setState(() {
_tmpEffect = value;
_changedHere = true;
if (_tmpEffect != null) {
ConnectionManager().callService(
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),
_buildWhiteValueControl(entity),
_buildColorTempControl(entity),
_buildColorControl(entity),
_buildEffectControl(entity)
],
);
}
Widget _buildBrightnessControl(LightEntity entity) {
if (entity.supportBrightness) {
double val;
if (_tmpBrightness != null) {
if (_tmpBrightness > 255) {
val = 255;
} else if (_tmpBrightness < 1) {
val = 1;
} else {
val = _tmpBrightness.toDouble();
}
} else {
val = 1;
}
return UniversalSlider(
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpBrightness = value.round();
});
},
min: 1.0,
max: 255.0,
onChangeEnd: (value) => _setBrightness(entity, value),
value: val,
leading: Icon(Icons.brightness_5),
title: "Brightness",
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildWhiteValueControl(LightEntity entity) {
if ((entity.supportWhiteValue) && (_tmpWhiteValue != null)) {
return UniversalSlider(
onChanged: (value) {
setState(() {
_changedHere = true;
_tmpWhiteValue = value.round();
});
},
min: 0.0,
max: 255.0,
onChangeEnd: (value) => _setWhiteValue(entity, value),
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
title: "White",
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildColorTempControl(LightEntity entity) {
if (entity.supportColorTemp) {
double val;
if (_tmpColorTemp != null) {
if (_tmpColorTemp > entity.maxMireds) {
val = entity.maxMireds;
} else if (_tmpColorTemp < entity.minMireds) {
val = entity.minMireds;
} else {
val = _tmpColorTemp.toDouble();
}
} else {
val = entity.minMireds;
}
return UniversalSlider(
title: "Color temperature",
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
value: val,
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) {
HSVColor savedColor = HomeAssistant().savedColor;
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
LightColorPicker(
color: _tmpColor,
onColorSelected: (color) => _setColor(entity, color),
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
color: _tmpColor.toColor(),
child: Text('Copy color'),
onPressed: _tmpColor == null ? null : () {
setState(() {
HomeAssistant().savedColor = _tmpColor;
});
},
),
FlatButton(
color: savedColor?.toColor() ?? Colors.transparent,
child: Text('Paste color'),
onPressed: savedColor == null ? null : () {
_setColor(entity, savedColor);
},
)
],
)
],
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
Widget _buildEffectControl(LightEntity entity) {
if ((entity.supportEffect) && (entity.effectList != null)) {
List<String> list = List.from(entity.effectList);
if (_tmpEffect!= null && !list.contains(_tmpEffect)) {
list.insert(0, _tmpEffect);
}
return ModeSelectorWidget(
onChange: (effect) => _setEffect(entity, effect),
caption: "Effect",
options: list,
value: _tmpEffect
);
} else {
return Container(width: 0.0, height: 0.0);
}
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More