Compare commits

...

435 Commits

Author SHA1 Message Date
6afbd37d71 892 2020-04-15 15:46:06 +03:00
0e8869878f 0.8.4 2020-04-15 12:06:06 +00:00
7c2cfe3215 MobileApp integration force update 2020-04-15 11:59:40 +00:00
c376c0e952 Minor fix 2020-04-14 21:08:10 +00:00
da5f663396 Gauge card optimizations 2020-04-14 18:39:21 +00:00
0e92418a33 Handle missed entity on Entity page 2020-04-14 18:15:28 +00:00
2eef7cfe5e Improve Horizontal and Vertical stack building 2020-04-13 19:24:37 +00:00
de4e0bfb3a Add new Button card support 2020-04-13 18:17:14 +00:00
8bf2d31e72 Bring back separate entity page 2020-04-13 18:15:14 +00:00
2125c46143 Gauge style improvements 2020-04-13 14:01:17 +00:00
5402eb84df Some default icons update 2020-04-13 13:13:49 +00:00
ad5aa0898f Set header toggle default to false 2020-04-13 13:05:59 +00:00
040d40b614 PayPal donate button 2020-04-11 19:26:25 +00:00
8e58f22c56 Merge pull request #533 from estevez-dev/pre-release/889
890
2020-04-11 21:16:12 +03:00
c91695fbe5 890 2020-04-11 21:15:08 +03:00
c43741da49 889 2020-04-11 20:28:35 +03:00
f2563a0397 Fix startup crash 2020-04-11 20:25:54 +03:00
fba4017819 0.8.3 2020-04-11 16:55:50 +00:00
5f23e108a1 Settings page and theme selection 2020-04-11 16:09:35 +00:00
68d14bd13d Fallback to MJPEG camera stream if streaming component is missing 2020-04-11 13:13:20 +00:00
022622522f Fix section text color for dark theme 2020-04-11 12:48:36 +00:00
89513ca4e5 Secrets config for CI/CD 2020-04-09 17:10:21 +00:00
a934ee2335 Hide zha panel 2020-04-09 16:25:35 +00:00
49aeea634f 0.8.2 2020-04-09 16:24:37 +00:00
e18b9ebe14 Replace VideoPlayer with web player 2020-04-08 16:48:13 +00:00
08ee3f3d80 Fix inactive gauge color 2020-04-07 20:30:39 +00:00
62d07bf8b9 Gauge card refactoring 2020-04-07 20:20:57 +00:00
ab398cbdc3 Remove debugg banner 2020-04-06 21:43:38 +00:00
007d12719c WIP #102 Moving all colors to theme 2020-04-06 21:39:16 +00:00
524d195800 WIP #102 Colors from theme 2020-04-06 20:03:41 +00:00
405de64249 Fix double events handling 2020-04-04 22:34:54 +00:00
f53554702e WIP Themes: new dark theme 2020-04-04 22:08:50 +00:00
379e1a4a7e WIP Themes: State colors from themes 2020-04-04 21:39:12 +00:00
d6f7096055 WIP Themes: New light theme 2020-04-04 20:54:32 +00:00
37c721e4f6 WIP Themes: Entity page heade color 2020-04-04 16:19:25 +00:00
d94235ef6d WIP Themes: Dark theme fonts 2020-04-04 16:13:12 +00:00
eb4184713f WIP Themes: badges colors 2020-04-04 15:44:06 +00:00
a0a0cb4612 WIP Themes: Make all fonts depend on theme 2020-04-04 15:13:55 +00:00
f448a20784 WIP Themes: primary colors fix 2020-04-04 13:36:32 +00:00
36eff26862 WIP themes: fix sudhead style 2020-04-04 13:27:23 +00:00
5b2a1163b9 Merge pull request #529 from estevez-dev/hotfix/0.8.1
Fix for older HA versions
2020-04-04 16:22:24 +03:00
e627a8b963 Fix for older HA versions 2020-04-04 13:17:01 +00:00
4432124e8c WIP Themes: font size standartization 2020-04-04 12:47:40 +00:00
b8ba3c59e9 WIP Themes: climate controls and entity name 2020-04-04 12:00:15 +00:00
c40a496b6b Replace Spectrum with Discord 2020-04-04 10:12:21 +00:00
a7c3b46061 Update pubspec.yaml 2020-04-03 17:25:59 +03:00
dfbaaeb06b 0.8.0 2020-04-03 13:32:23 +00:00
f6ab20c6e8 Add whats new manu item 2020-04-03 13:29:02 +00:00
7625099d74 Disable background location log 2020-04-03 13:17:24 +00:00
32c8e76855 Fix for non-string climate mode values 2020-04-03 13:07:26 +00:00
0aa2c974d5 Disable crashlytics in debug mode 2020-04-03 12:43:48 +00:00
9524c8587b Update WHats new link 2020-04-03 12:36:16 +00:00
c075db8b1a 883 2020-04-02 22:05:36 +00:00
d0b7cc1929 Initial UI generation if no lovelace config 2020-04-02 22:04:56 +00:00
d8df32f140 Resolves #524 Geolocator update 2020-04-02 21:16:34 +00:00
293b5e0242 zha_map WIP: get inital data 2020-04-01 17:04:32 +00:00
2f517a3ad5 ui build refactoring 1 2020-03-21 23:11:00 +00:00
56d8e389db Do not update plugins on each gitpod start 2020-03-21 20:45:01 +00:00
1377843350 882 2020-03-21 14:06:58 +00:00
8e31eaf8bb Fix dashboard switching and dashboard icons 2020-03-20 10:14:14 +00:00
5ced01463f Resolves #526 Subscribe to Lovelace update events 2020-03-19 23:16:59 +00:00
a3548455eb Resolves #525 Support Lovelace dashboards 2020-03-19 22:59:53 +00:00
c40fceea4f add log output on location error in background 2020-03-15 17:51:01 +00:00
6ad3938a91 Resolves #369 Get actual entity data in entity wrapper 2020-03-15 16:28:45 +00:00
bc642f81ad Fix caching 2020-03-15 15:26:03 +00:00
14ce608bbb Add data caching 2020-03-15 13:47:51 +00:00
c4c67747c5 Remove services requesting 2020-03-15 12:34:16 +00:00
5b3ceecb0e Update crashlytics 2020-03-15 12:27:32 +00:00
bf53e4b9df Add fit option for EntityPicture widget 2020-03-14 18:23:39 +00:00
7e09d92fdf Add double_tap_action support 2020-03-14 18:12:11 +00:00
1ba9106d0b Show entity picture while camera stream loading 2020-03-14 17:56:07 +00:00
d727a29991 881 2020-03-12 21:20:46 +00:00
c5d617477f Fix state filter when state is number 2020-03-12 21:19:39 +00:00
244a1984cc Resolves #512 Support all state_filter options 2020-03-12 21:16:07 +00:00
b00b745f27 Camera fullscreen view 2020-03-12 12:22:38 +00:00
959ff21b9b Fix checking log file size if it doesnt exist 2020-03-09 15:01:53 +00:00
e6a7fd2dfe workmanager 0.2.2, geolocator 5.3.0, crashlytics 0.1.3 2020-03-09 14:57:29 +00:00
216276e5f3 Limit log file size to 5MB. Background location fixes 2020-03-09 14:53:12 +00:00
3e6229cf3e Add log file for background service 2020-03-09 13:11:16 +00:00
fc4cb80b74 Fix background task execution 2020-03-05 09:08:47 +00:00
b907ff1e82 Location isolate return 2020-03-04 17:16:16 +00:00
7536a52771 Clear camera viewer 2020-03-04 16:59:08 +00:00
73a8c111d1 Camera stream controls 2020-02-21 15:36:03 +00:00
86a19eeec2 Camera stream web view improvements 2020-02-21 11:23:39 +00:00
fba4459977 Camera stream view fixes 2020-02-20 22:42:19 +00:00
06f994a827 Camera stream aspect ratio calculations 2020-02-20 21:48:22 +00:00
35d8607484 Resolves #370 Camera stream support 2020-02-20 14:33:03 +00:00
2f4c06e9b5 Resolves #420 Camera view on entity page 2020-02-20 11:18:09 +00:00
92e008a380 Resolves #522 Fix temperature change for thermostat 2020-02-19 14:20:44 +00:00
14c272af92 Resolves #501 Entities card header toggle 2020-02-19 10:17:08 +00:00
710de9f2b8 Resolves #514 Media player volume buttons placement 2020-02-19 09:27:01 +00:00
d9ad3b3083 Location tracking... 2020-02-17 19:23:00 +00:00
b2686cb105 Update README.md 2020-02-17 16:39:30 +02:00
959e89de2b Remove web UI presentation option 2020-02-17 13:50:24 +00:00
6e448d3458 JS interface improvements 2020-02-12 22:42:43 +00:00
6695756727 Embedded webview 2020-02-12 21:13:49 +00:00
ed732e9b77 Remove bottom bar for webview 2020-02-12 13:37:52 +00:00
f495a6affc Move to ha-client.app 2020-02-12 13:22:48 +00:00
c8d7e1a95f Update README.md 2020-02-12 12:39:36 +02:00
e1ca2638e3 External bus and configuration opening for Web UI 2020-02-11 22:55:16 +00:00
01226cb9eb WebVIew UI settings 2020-02-11 20:53:29 +00:00
8a80d0c5d1 WebView UI experiment 001 2020-02-11 14:02:32 +00:00
f26f3e87c7 Remove Crashlytics debug 2020-02-11 12:57:31 +00:00
b750417415 Replace Sentry with Crashlytics 2020-02-11 12:06:19 +00:00
2c35dd7c21 Update battery plugin 2020-02-11 11:25:58 +00:00
cff4a4feed Update workmanager and migrate to Flutter embedding v2 2020-02-11 11:22:59 +00:00
62174b0651 Update geolocator plugin 2020-02-11 11:07:13 +00:00
d3ea4210c1 Update local notifications library 2020-02-11 11:03:39 +00:00
1c782bf64d Update firebase library and downgrade gradle 2020-02-11 10:57:15 +00:00
bc96dab339 Update gradle and Google services 2020-02-11 09:52:04 +00:00
0f7179b944 In app purchase update and optimizations 2020-02-11 09:39:11 +00:00
1e3bfa8ff7 Install Java in gitpod with sdkman 2020-02-07 09:46:34 +00:00
2bce86f905 gitpod config 2020-02-07 09:27:23 +00:00
0be00acc3a do 2020-01-30 20:50:42 +00:00
4e61adaeb1 Fixind java version switch 2020-01-30 20:42:29 +00:00
49a8f08153 Update .gitpod.dockerfile 2020-01-30 22:34:10 +02:00
ce15658462 Add jdk version set 2020-01-30 20:28:34 +00:00
16d73ba7dd Update .gitpod.yml 2020-01-30 22:20:54 +02:00
9f3e3c1917 Update .gitpod.dockerfile 2020-01-30 22:20:01 +02:00
f29e382a19 Update .gitpod.yml 2020-01-30 21:53:29 +02:00
073562373a Update .gitpod.dockerfile 2020-01-30 21:43:28 +02:00
4298ebcd66 Bump Flutter version to 1.12.13 2020-01-29 20:13:28 +02:00
a121295bef Update .gitignore 2020-01-29 20:12:46 +02:00
9303e4c0a5 Force https for oauth 2020-01-29 17:38:51 +00:00
831fc98ab1 Change domain for ha-client website 2020-01-29 17:31:16 +00:00
2003005e56 Update README.md 2020-01-29 11:48:32 +02:00
fda8fb7182 Webview for external panels and everything 2020-01-27 21:25:55 +00:00
cf6039b279 Back to webview oauth 2020-01-27 21:12:05 +00:00
41e552dce5 New webview test 2020-01-27 20:06:02 +00:00
90043b5806 Remove text sharing feature 2020-01-27 19:21:21 +00:00
9eb74b5a8d WIP Share media refactoring 2019-12-11 18:24:09 +00:00
9cc60a136b 0.7.7 2019-12-10 22:05:12 +00:00
78eb1e779c Fix state event handling before fetch complete 2019-12-10 22:04:24 +00:00
8db2d8508e Resolves #515 tabs controller issue 2019-12-10 22:00:17 +00:00
3f1ece26ec Remove background task logging and reporting 2019-12-10 21:19:29 +00:00
d1912a44c6 Replace Discord with Spectrum 2019-12-10 19:54:02 +00:00
36a05eb390 Update README.md 2019-12-06 10:58:23 +02:00
4f39ea1ad8 Update README.md 2019-12-04 16:31:03 +02:00
a241cc1d61 Resolves #494 2019-11-29 13:40:51 +00:00
8b4df98cb9 0.7.6 2019-11-29 13:02:22 +00:00
7d30c2f9d5 Wrap empty navigate action 2019-11-29 12:45:59 +00:00
44acabadfe Fix location tracking backfroud task id 2019-11-29 11:27:59 +00:00
6f3a2bb78d Fix timeout handling on socket message send 2019-11-29 11:24:29 +00:00
dd5f8b155d Handle some socket exceptions 2019-11-29 11:21:45 +00:00
cd81fc72fd Fix connection timeout handling 2019-11-29 10:58:24 +00:00
890da650dc Resolves #508 show_name for enriry button card 2019-11-29 10:12:41 +00:00
9897b6a44b Fix show_empty for entity-filter 2019-11-29 10:05:09 +00:00
7969f54d3b Fix MissedPluginException for workmanager 2019-11-28 19:54:22 +00:00
7c18454de3 Fix issue with handling service call exceptions 2019-11-28 19:14:50 +00:00
dcf5efddd1 Parse port and protocol from HA url 2019-11-28 18:57:41 +00:00
a6541134e0 Fix compliting alrady completed future 2019-11-28 18:33:27 +00:00
90504047b4 Resolves #492 Infinity media player progress error 2019-11-28 17:42:48 +00:00
ca1eec6602 Update bug_report.md 2019-11-27 18:44:45 +02:00
edc01d14b7 Resolves #511 2019-11-27 17:21:23 +02:00
6cb5463b13 minor background reporting fix 2019-11-27 14:50:53 +00:00
63a789ebfb 0.7.5 name fix 2019-11-27 12:48:03 +00:00
a0994e9a60 0.7.5 2019-11-27 12:42:09 +00:00
8d1b728194 Background location tracking crash reporting 2019-11-27 12:41:38 +00:00
1a9fec8b98 Senty reporting. Fix background location tracking crash 2019-11-27 12:26:55 +00:00
e634253282 0.7.4 2019-11-26 21:13:59 +00:00
64b23ec7cc Revert flutter_markdown to 0.3.0 2019-11-26 21:13:07 +00:00
afe207a878 Removes foreground location and Resolves #510 2019-11-26 20:56:24 +00:00
4bac0c092f Removes foreground location and Resolves #510 2019-11-26 20:54:36 +00:00
74c8ae35a1 Remove network security config 2019-11-26 20:48:08 +00:00
7856637456 Fix app version display 2019-11-26 16:50:19 +00:00
965f80a6ca 0.7.3 2019-11-14 12:58:56 +02:00
198c2ba49a build 730 2019-11-14 12:58:32 +02:00
4b9ec5ca6e Foreground location updates 2019-11-10 21:53:28 +00:00
5792652619 Experimental location tracking for every 10 or 5 minutes 2019-11-10 14:41:29 +00:00
2c900333a5 WIP #344 Add network security config to allow user certificates 2019-11-10 13:53:25 +00:00
1f782d7cd3 Resolves #498 Handle bool state 2019-11-10 13:48:05 +00:00
89cc1833de Resolves #493 Send media_player calls even if it if unavailable 2019-11-08 20:50:31 +00:00
1262d8c9aa Resolves #484 Fix entity-filter cards 2019-11-08 20:41:51 +00:00
85b0c4f814 Resolves #419 Fallback to states if no lovelace config found 2019-11-08 20:14:34 +00:00
551a8dfa31 Fixx service calls 2019-11-08 19:37:41 +00:00
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
175 changed files with 14698 additions and 7671 deletions

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

@ -0,0 +1,22 @@
---
name: Bug report
about: Create a report to help improve HA Client
title: ''
labels: ''
assignees: ''
---
**HA Client version:** [Main menu -> About HA Client]
**Home Assistant version:**
**Device name:**
**Android version:**
**Description**
[Replace with description]
**Screenshots**
[Replace with screenshots]

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.

11
.gitignore vendored
View File

@ -9,5 +9,14 @@ build/
.flutter-plugins
.idea/
.vscode/
.theia/
.project/
.settings/
key.properties
flutter_export_environment.sh
.flutter-plugins-dependencies
key.properties
.secrets.dart
pubspec.lock

8
.gitpod.dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM gitpod/workspace-full:latest
ENV ANDROID_HOME=/workspace/android-sdk \
FLUTTER_ROOT=/workspace/flutter \
FLUTTER_HOME=/workspace/flutter
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
&& sdk install java 8.0.242.j9-adpt"

26
.gitpod.yml Normal file
View File

@ -0,0 +1,26 @@
image:
file: .gitpod.dockerfile
tasks:
- before: |
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/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.12.13+hotfix.7-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: |
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==

Binary file not shown.

Binary file not shown.

View File

View File

@ -1,12 +1,16 @@
[![flutter](https://somegeeky.website/assets/badges/flutter_badge_v3.svg)](https://somegeeky.website/badges/flutter) [![dart](https://somegeeky.website/assets/badges/dart_badge_v3.svg)](https://somegeeky.website/badges/dart)
# HA Client
## Native Android client for Home Assistant
### With Lovelace UI support
### With notifications and Lovelace UI support
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
Visit [ha-client.app](http://ha-client.app/) for more info.
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
Discuss it on [Discord](https://discord.gg/nd6FZQ) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
#### 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

@ -50,6 +50,14 @@ android {
}
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']
@ -70,10 +78,11 @@ flutter {
}
dependencies {
implementation 'com.google.firebase:firebase-core:16.0.8'
implementation 'com.google.firebase:firebase-analytics:17.2.2'
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: 'io.fabric'
apply plugin: 'com.google.gms.google-services'

View File

@ -14,6 +14,30 @@
}
},
"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
@ -25,15 +49,13 @@
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
"other_platform_oauth_client": [
{
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}

View File

@ -1,11 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keyboardcrumbs.hassclient">
<!-- 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-feature android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
@ -13,10 +17,18 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="HA Client"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="ha_notify" />
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
@ -24,13 +36,12 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- 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). -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
@ -40,5 +51,20 @@
<category android:name="android.intent.category.LAUNCHER"/>
</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>

View File

@ -1,13 +1,15 @@
package com.keyboardcrumbs.hassclient;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
}
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

View File

@ -5,4 +5,7 @@
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>

View File

@ -2,11 +2,15 @@ buildscript {
repositories {
google()
jcenter()
maven {
url 'https://maven.fabric.io/public'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'io.fabric.tools:gradle:1.26.1'
}
}
@ -14,6 +18,9 @@ allprojects {
repositories {
google()
jcenter()
maven {
url 'https://maven.fabric.io/public'
}
}
}

View File

@ -2,4 +2,5 @@ org.gradle.jvmargs=-Xmx2g
org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=true
android.enableR8=true

1232
android/hs_err_pid766.log Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
include ':app'

View File

@ -0,0 +1,61 @@
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<style>
body {
padding: 0;
margin: 0;
widows: 100%;
height: 100%;
}
video {
width: 100%;
}
</style>
<script>
var messageChannel = '{{message_channel}}';
</script>
</head>
<body>
<video id="screen" width="100%" controls></video>
<script>
if (Hls.isSupported()) {
var video = document.getElementById('screen');
var hls = new Hls();
hls.on(Hls.Events.ERROR, function (event, data) {
if (data.fatal) {
switch(data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
// try to recover network error
console.log("fatal network error encountered, try to recover");
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.log("fatal media error encountered, try to recover");
hls.recoverMediaError();
break;
default:
// cannot recover
hls.destroy();
break;
}
}
});
// bind them together
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
console.log("video and hls.js are now bound together !");
hls.loadSource("{{stream_url}}");
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
console.log("manifest loaded, found " + data.levels.length + " quality level");
video.play();
video.onloadedmetadata = function() {
window[messageChannel].postMessage(document.body.clientWidth / video.offsetHeight);
};
});
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,28 @@
<html>
<head>
<style>
body {
padding: 0;
margin: 0;
widows: 100%;
height: 100%;
}
img {
width: 100%;
}
</style>
<script>
var messageChannel = '{{message_channel}}';
window.onload = function() {
var img = document.getElementById('screen');
if (img) {
window[messageChannel].postMessage(document.body.clientWidth / img.offsetHeight);
}
};
</script>
</head>
<body>
<img id="screen" src="{{stream_url}}">
</body>
</html>

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

@ -0,0 +1,35 @@
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);
}
};
window.externalApp.externalBus = function(message) {
console.log("External bus message: " + message);
var messageObj = JSON.parse(message);
if (messageObj.type == "config/get") {
var responseData = {
id: messageObj.id,
type: "result",
success: true,
result: {
hasSettingsScreen: true
}
};
setTimeout(function(){
window.externalBus(responseData);
}, 500);
} else if (messageObj.type == "config_screen/show") {
HAClient.postMessage('show-settings');
}
};

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.

View File

@ -1,44 +0,0 @@
part of 'main.dart';
class AuthManager {
static final AuthManager _instance = AuthManager._internal();
factory AuthManager() {
return _instance;
}
AuthManager._internal();
Future getTempToken({String httpWebHost, String oauthUrl}) {
Completer completer = Completer();
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.onUrlChanged.listen((String url) {
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
String authCode = url.split("=")[1];
Logger.d("We have auth code. Getting temporary access token...");
Connection().sendHTTPPost(
host: httpWebHost,
endPoint: "/auth/token",
contentType: "application/x-www-form-urlencoded",
includeAuthHeader: false,
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}"
).then((response) {
Logger.d("Gottemp token");
String tempToken = json.decode(response)['access_token'];
Logger.d("Closing webview...");
flutterWebviewPlugin.close();
completer.complete(tempToken);
}).catchError((e) {
flutterWebviewPlugin.close();
completer.completeError({"errorCode": 61, "errorMessage": "Error getting temp token"});
Logger.e("Error getting temp token: ${e.toString()}");
});
}
});
Logger.d("Launching OAuth...");
eventBus.fire(StartAuthEvent(oauthUrl));
return completer.future;
}
}

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

@ -0,0 +1,123 @@
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;
bool showHeaderToggle;
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.showHeaderToggle: 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 (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
return false;
}
List currentStateFilter;
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
currentStateFilter = entityWrapper.stateFilter;
} else {
currentStateFilter = stateFilter;
}
bool showByFilter = currentStateFilter.isEmpty;
for (var allowedState in currentStateFilter) {
if (allowedState is String && allowedState == entityWrapper.entity.state) {
showByFilter = true;
break;
} else if (allowedState is Map) {
try {
var tmpVal = allowedState['attribute'] != null ? entityWrapper.entity.getAttribute(allowedState['attribute']) : entityWrapper.entity.state;
var valToCompareWith = allowedState['value'];
var valToCompare;
if (valToCompareWith is! String && tmpVal is String) {
valToCompare = double.tryParse(tmpVal);
} else {
valToCompare = tmpVal;
}
if (valToCompare != null) {
bool result;
switch (allowedState['operator']) {
case '<=': { result = valToCompare <= valToCompareWith;}
break;
case '<': { result = valToCompare < valToCompareWith;}
break;
case '>=': { result = valToCompare >= valToCompareWith;}
break;
case '>': { result = valToCompare > valToCompareWith;}
break;
case '!=': { result = valToCompare != valToCompareWith;}
break;
case 'regex': {
RegExp regExp = RegExp(valToCompareWith.toString());
result = regExp.hasMatch(valToCompare.toString());
}
break;
default: {
result = valToCompare == valToCompareWith;
}
}
if (result) {
showByFilter = true;
break;
}
}
} catch (e) {
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState');
Logger.e('$e');
}
}
}
return showByFilter;
}).toList();
}
Widget build(BuildContext context) {
return CardWidget(
card: this,
);
}
}

View File

@ -24,63 +24,82 @@ class CardWidget extends StatelessWidget {
}
}
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: {
case CardType.ENTITIES: {
return _buildEntitiesCard(context);
}
case CardType.glance: {
case CardType.GLANCE: {
return _buildGlanceCard(context);
}
case CardType.mediaControl: {
case CardType.MEDIA_CONTROL: {
return _buildMediaControlsCard(context);
}
case CardType.entityButton: {
case CardType.ENTITY_BUTTON: {
return _buildEntityButtonCard(context);
}
case CardType.markdown: {
case CardType.BUTTON: {
return _buildEntityButtonCard(context);
}
case CardType.GAUGE: {
return _buildGaugeCard(context);
}
/* case CardType.LIGHT: {
return _buildLightCard(context);
}*/
case CardType.MARKDOWN: {
return _buildMarkdownCard(context);
}
case CardType.alarmPanel: {
case CardType.ALARM_PANEL: {
return _buildAlarmPanelCard(context);
}
case CardType.horizontalStack: {
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),
)
);
}
});
children = card.childCards.map((childCard) => Flexible(
fit: FlexFit.tight,
child: childCard.build(context),
)
).toList();
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
case CardType.verticalStack: {
case CardType.VERTICAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
children.add(
card.build(context)
);
});
List<Widget> children = card.childCards.map((childCard) => childCard.build(context)).toList();
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
@ -107,22 +126,49 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name));
Widget headerSwitch;
if (card.showHeaderToggle) {
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
}).map((EntityWrapper en) {
return en.entity.entityId;
}).toList();
headerSwitch = Switch(
value: headerToggleVal,
onChanged: (val) {
if (entitiesToToggle.isNotEmpty) {
ConnectionManager().callService(
domain: "homeassistant",
service: val ? "turn_on" : "turn_off",
entityId: entitiesToToggle
);
}
},
);
}
body.add(
CardHeader(
name: card.name,
trailing: headerSwitch
)
);
entitiesToShow.forEach((EntityWrapper entity) {
if (!entity.entity.isHidden) {
body.add(
Padding(
padding: EdgeInsets.fromLTRB(10.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
child: entity.entity.buildDefaultWidget(context)
),
));
}
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: new Column(mainAxisSize: MainAxisSize.min, children: body)
child: Padding(
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
child: Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
@ -131,7 +177,7 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name));
body.add(CardHeader(name: card.name));
body.add(MarkdownBody(data: card.content));
return Card(
child: Padding(
@ -143,12 +189,9 @@ class CardWidget extends StatelessWidget {
Widget _buildAlarmPanelCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeaderWidget(
body.add(CardHeader(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle(
color: Colors.grey
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
@ -164,7 +207,7 @@ class CardWidget extends StatelessWidget {
alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
)
)
]
@ -195,39 +238,51 @@ class CardWidget extends StatelessWidget {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> rows = [];
rows.add(CardHeaderWidget(name: card.name));
rows.add(CardHeader(name: card.name));
List<Widget> result = [];
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
entitiesToShow.forEach((EntityWrapper entity) {
result.add(
FractionallySizedBox(
widthFactor: 1/columnsCount,
child: EntityModel(
entityWrapper: entity,
child: GlanceEntityContainer(
showName: card.showName,
showState: card.showState,
),
handleTap: true
),
)
);
});
rows.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
child: Wrap(
//alignment: WrapAlignment.spaceAround,
runSpacing: Sizes.rowPadding*2,
children: result,
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: new Column(mainAxisSize: MainAxisSize.min, children: rows)
child: Column(
mainAxisSize: MainAxisSize.min,
children: rows
)
);
}
@ -242,12 +297,48 @@ class CardWidget extends StatelessWidget {
}
Widget _buildEntityButtonCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.overrideName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName.toUpperCase();
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: ButtonEntityContainer(),
child: EntityButtonCardBody(
showName: card.showName,
),
handleTap: true
)
);
}
Widget _buildGaugeCard(BuildContext context) {
card.linkedEntityWrapper.overrideName = card.name ??
card.linkedEntityWrapper.displayName;
card.linkedEntityWrapper.unitOfMeasurementOverride = 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.overrideName = 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
)
);
@ -255,7 +346,11 @@ class CardWidget extends StatelessWidget {
Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeaderWidget(name: card.name ?? ""));
body.add(
CardHeader(
name: card.name ?? ""
)
);
List<Widget> result = [];
if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[
@ -286,4 +381,4 @@ class CardWidget extends StatelessWidget {
);
}
}
}

View File

@ -1,12 +1,12 @@
part of '../main.dart';
part of '../../main.dart';
class CardHeaderWidget extends StatelessWidget {
class CardHeader extends StatelessWidget {
final String name;
final Widget trailing;
final Widget subtitle;
const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -18,7 +18,7 @@ class CardHeaderWidget extends StatelessWidget {
title: Text("$name",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
style: Theme.of(context).textTheme.headline),
);
} else {
result = new Container(width: 0.0, height: 0.0);

View File

@ -0,0 +1,57 @@
part of '../../main.dart';
class EntityButtonCardBody extends StatelessWidget {
final bool showName;
EntityButtonCardBody({
Key key, this.showName: true,
}) : 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(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: FractionallySizedBox(
widthFactor: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
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() {
if (showName) {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
wordsWrap: true,
textAlign: TextAlign.center
);
}
return Container(width: 0, height: 0);
}
}

View File

@ -0,0 +1,177 @@
part of '../../main.dart';
class GaugeCardBody extends StatelessWidget {
final int min;
final int max;
final Map severity;
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
double fixedValue;
double value = entityWrapper.entity.doubleState;
if (value > max) {
fixedValue = max.toDouble();
} else if (value < min) {
fixedValue = min.toDouble();
} else {
fixedValue = value;
}
List<GaugeRange> ranges;
Color currentColor;
if (severity != null && severity["green"] is int && severity["red"] is int && severity["yellow"] is int) {
List<RangeContainer> rangesList = <RangeContainer>[
RangeContainer(severity["green"], HAClientTheme().getGreenGaugeColor()),
RangeContainer(severity["red"], HAClientTheme().getRedGaugeColor()),
RangeContainer(severity["yellow"], HAClientTheme().getYellowGaugeColor())
];
rangesList.sort((current, next) {
if (current.startFrom > next.startFrom) {
return 1;
}
if (current.startFrom < next.startFrom) {
return -1;
}
return 0;
});
if (fixedValue < rangesList[1].startFrom) {
currentColor = rangesList[0].color;
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
currentColor = rangesList[1].color;
} else {
currentColor = rangesList[2].color;
}
ranges = [
GaugeRange(
startValue: rangesList[0].startFrom.toDouble(),
endValue: rangesList[1].startFrom.toDouble(),
color: rangesList[0].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[1].startFrom.toDouble(),
endValue: rangesList[2].startFrom.toDouble(),
color: rangesList[1].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[2].startFrom.toDouble(),
endValue: max.toDouble(),
color: rangesList[2].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
)
];
}
if (ranges == null) {
currentColor = Theme.of(context).primaryColorDark;
ranges = <GaugeRange>[
GaugeRange(
startValue: min.toDouble(),
endValue: max.toDouble(),
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3,
)
];
}
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: AspectRatio(
aspectRatio: 2,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSizeFactor;
if (constraints.maxWidth > 300.0) {
fontSizeFactor = 1.6;
} else if (constraints.maxWidth > 150.0) {
fontSizeFactor = 1;
} else if (constraints.maxWidth > 100.0) {
fontSizeFactor = 0.6;
} else {
fontSizeFactor = 0.4;
}
return SfRadialGauge(
axes: <RadialAxis>[
RadialAxis(
maximum: max.toDouble(),
minimum: min.toDouble(),
showLabels: false,
useRangeColorForAxis: true,
showTicks: false,
canScaleToFit: true,
ranges: ranges,
axisLineStyle: AxisLineStyle(
thickness: 0.3,
thicknessUnit: GaugeSizeUnit.factor,
color: Colors.transparent
),
annotations: <GaugeAnnotation>[
GaugeAnnotation(
angle: -90,
positionFactor: 1.3,
//verticalAlignment: GaugeAlignment.far,
widget: EntityName(
textStyle: Theme.of(context).textTheme.body1.copyWith(
fontSize: Theme.of(context).textTheme.body1.fontSize * fontSizeFactor
),
),
),
GaugeAnnotation(
angle: 180,
positionFactor: 0,
verticalAlignment: GaugeAlignment.center,
widget: SimpleEntityState(
expanded: false,
maxLines: 1,
textAlign: TextAlign.center,
textStyle: Theme.of(context).textTheme.title.copyWith(
fontSize: Theme.of(context).textTheme.title.fontSize * fontSizeFactor,
),
),
)
],
startAngle: 180,
endAngle: 0,
pointers: <GaugePointer>[
RangePointer(
value: fixedValue,
sizeUnit: GaugeSizeUnit.factor,
width: 0.3,
color: currentColor,
enableAnimation: true,
animationType: AnimationType.bounceOut,
)
]
)
],
);
},
),
),
);
}
}
class RangeContainer {
final int startFrom;
Color color;
RangeContainer(this.startFrom, this.color);
}

View File

@ -1,21 +1,19 @@
part of '../main.dart';
part of '../../main.dart';
class GlanceEntityContainer extends StatelessWidget {
class GlanceCardEntityContainer extends StatelessWidget {
final bool showName;
final bool showState;
final bool nameInTheBottom;
final double iconSize;
final double nameFontSize;
final bool wordsWrapInName;
GlanceEntityContainer({
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);
@ -31,7 +29,7 @@ class GlanceEntityContainer extends StatelessWidget {
List<Widget> result = [];
if (!nameInTheBottom) {
if (showName) {
result.add(_buildName());
result.add(_buildName(context));
}
} else {
if (showState) {
@ -39,43 +37,39 @@ class GlanceEntityContainer extends StatelessWidget {
}
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
)
EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
)
);
if (!nameInTheBottom) {
if (showState) {
result.add(_buildState());
}
} else {
result.add(_buildName());
result.add(_buildName(context));
}
return Center(
child: InkResponse(
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
child: Column(
mainAxisSize: MainAxisSize.min,
//mainAxisAlignment: MainAxisAlignment.start,
//crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
),
);
}
Widget _buildName() {
Widget _buildName(BuildContext context) {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: wordsWrapInName,
textAlign: TextAlign.center,
fontSize: nameFontSize,
textStyle: Theme.of(context).textTheme.body1,
);
}

View File

@ -0,0 +1,38 @@
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],
),
),
);
}
}

View File

@ -1,343 +0,0 @@
part of 'main.dart';
class Connection {
static final Connection _instance = Connection._internal();
factory Connection() {
return _instance;
}
Connection._internal();
String displayHostname;
String _webSocketAPIEndpoint;
String httpWebHost;
String _token;
String _tempToken;
String oauthUrl;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
Duration connectTimeout = Duration(seconds: 15);
bool isConnected = false;
var onStateChangeCallback;
IOWebSocketChannel _socket;
int _currentMessageId = 0;
Map<String, Completer> _messageResolver = {};
Future init(onStateChange) async {
Completer completer = Completer();
onStateChangeCallback = onStateChange;
SharedPreferences prefs = await SharedPreferences.getInstance();
String domain = prefs.getString('hassio-domain');
String port = prefs.getString('hassio-port');
displayHostname = "$domain:$port";
_webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket";
httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port";
//_token = prefs.getString('hassio-token');
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
} catch (e) {
Logger.e("Cannt read secure storage. Need to relogin.");
_token = null;
await storage.delete(key: "hacl_llt");
}
if ((domain == null) || (port == null) ||
(domain.length == 0) || (port.length == 0)) {
completer.completeError({"errorCode": 5, "errorMessage": "Check connection settings"});
} else {
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}&redirect_uri=${Uri.encodeComponent('http://ha-client.homemade.systems/service/auth_callback.html')}";
if (_token == null) {
await AuthManager().getTempToken(
httpWebHost: httpWebHost,
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
_tempToken = token;
});
}
_connect().timeout(connectTimeout, onTimeout: () {
_disconnect().then((_) {
completer.completeError(
{"errorCode": 1, "errorMessage": "Connection timeout"});
});
}).then((_) => completer.complete()).catchError((e) {
completer.completeError(e);
});
}
return completer.future;
}
Completer connecting;
Future _connect() async {
if (connecting != null && !connecting.isCompleted) {
Logger.w("");
return connecting.future;
}
connecting = Completer();
await _disconnect();
Logger.d( "Socket connecting...");
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
(message) {
isConnected = true;
var data = json.decode(message);
if (data["type"] == "auth_required") {
Logger.d("[Received] <== ${data.toString()}");
_authenticate().then((_) => connecting.complete()).catchError((e) {
if (!connecting.isCompleted) connecting.completeError(e);
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.complete();
_messageResolver.remove("auth");
if (!connecting.isCompleted) connecting.complete(sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
));
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"});
_messageResolver.remove("auth");
logout().then((_) {
if (!connecting.isCompleted) connecting.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"});
});
} else {
_handleMessage(data);
}
},
cancelOnError: true,
onDone: () => _handleSocketClose(connecting),
onError: (e) => _handleSocketError(e, connecting)
);
return connecting.future;
}
Future _disconnect() async {
Logger.d( "Socket disconnecting...");
await _socketSubscription?.cancel();
await _socket?.sink?.close()?.timeout(Duration(seconds: 4),
onTimeout: () => Logger.d( "Socket sink close timeout")
);
Logger.d( "..Disconnected");
}
_handleMessage(data) {
if (data["type"] == "result") {
if (data["id"] != null && data["success"]) {
Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError({"errorMessage": "${data['error']["message"]}"});
}
_messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]);
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $data");
}
} else {
Logger.d("[Received unhandled] <== ${data.toString()}");
}
}
void _handleSocketClose(Completer connectionCompleter) {
isConnected = false;
Logger.d("Socket disconnected.");
if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError({"errorCode": 82, "errorMessage": "Disconnected"});
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect();
});
});
}
}
void _handleSocketError(e, Completer connectionCompleter) {
isConnected = false;
Logger.e("Socket stream Error: $e");
if (!connectionCompleter.isCompleted) {
connectionCompleter.completeError({"errorCode": 81, "errorMessage": "Unable to connect to Home Assistant"});
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect();
});
});
}
}
Future _authenticate() {
Completer completer = Completer();
if (_token != null) {
Logger.d( "Long-lived token exist");
Logger.d( "[Sending] ==> auth request");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_token"},
auth: true
).then((_) {
completer.complete();
}).catchError((e) => completer.completeError(e));
} else if (_tempToken != null) {
Logger.d("We have temp token. Loging in...");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_tempToken"},
auth: true
).then((_) {
Logger.d("Requesting long-lived token...");
_getLongLivedToken().then((_) {
completer.complete();
}).catchError((e) {
Logger.e("Can't get long-lived token: $e");
throw e;
});
}).catchError((e) => completer.completeError(e));
} else {
completer.completeError({"errorCode": 63, "errorMessage": "General login error"});
}
return completer.future;
}
Future logout() {
_token = null;
_tempToken = null;
final storage = new FlutterSecureStorage();
return storage.delete(key: "hacl_llt");
}
Future _getLongLivedToken() {
Completer completer = Completer();
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
Logger.d("Got long-lived token.");
_token = data;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: "$_token").then((_) {
completer.complete();
}).catchError((e) {
throw e;
});
}).catchError((e) {
logout();
completer.completeError({"errorCode": 63, "errorMessage": "Authentication error: $e"});
});
return completer.future;
}
Future sendSocketMessage({String type, Map additionalData, bool auth: false}) {
Completer _completer = Completer();
Map dataObject = {"type": "$type"};
String callbackName;
if (!auth) {
_incrementMessageId();
dataObject["id"] = _currentMessageId;
callbackName = "$_currentMessageId";
} else {
callbackName = "auth";
}
if (additionalData != null) {
dataObject.addAll(additionalData);
}
_messageResolver[callbackName] = _completer;
String rawMessage = json.encode(dataObject);
Logger.d("[Sending] ==> $rawMessage");
if (!isConnected) {
_connect().timeout(connectTimeout, onTimeout: (){
_completer.completeError({"errorCode": 8, "errorMessage": "No connection to Home Assistant"});
}).then((_) {
_socket.sink.add(rawMessage);
}).catchError((e) {
_completer.completeError(e);
});
} else {
_socket.sink.add(rawMessage);
}
return _completer.future;
}
void _incrementMessageId() {
_currentMessageId += 1;
}
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
Map serviceData = {};
if (entityId != null) {
serviceData["entity_id"] = entityId;
}
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
serviceData.addAll(additionalServiceData);
}
if (serviceData.isNotEmpty)
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
}
Future<List> getHistory(String entityId) async {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> $url");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_token",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
if (history is List) {
Logger.d( "[Received] <== ${history.first.length} history recors");
return history;
} else {
return [];
}
}
Future sendHTTPPost({String host, String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true, String authToken}) async {
Completer completer = Completer();
String url = "$host$endPoint";
Logger.d("[Sending] ==> $url");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $authToken";
}
http.post(
url,
headers: headers,
body: data
).then((response) {
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
if (response.statusCode == 200) {
completer.complete(response.body);
} else {
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
}
}).catchError((e) {
completer.completeError(e);
});
return completer.future;
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of 'main.dart';
class EntityState {
static const on = 'on';
@ -29,6 +29,11 @@ class EntityState {
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 {
@ -46,6 +51,10 @@ class EntityUIAction {
String holdNavigationPath;
String holdService;
Map<String, dynamic> holdServiceData;
String doubleTapAction = EntityUIAction.none;
String doubleTapNavigationPath;
String doubleTapService;
Map<String, dynamic> doubleTapServiceData;
EntityUIAction({rawEntityData}) {
if (rawEntityData != null) {
@ -71,29 +80,62 @@ class EntityUIAction {
holdServiceData = rawEntityData["hold_action"]["service_data"];
}
}
if (rawEntityData["double_tap_action"] != null) {
if (rawEntityData["double_tap_action"] is String) {
doubleTapAction = rawEntityData["double_tap_action"];
} else {
doubleTapAction =
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
doubleTapService = rawEntityData["double_tap_action"]["service"];
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
}
}
}
}
}
class CardType {
static const horizontalStack = "horizontal-stack";
static const verticalStack = "vertical-stack";
static const entities = "entities";
static const glance = "glance";
static const mediaControl = "media-control";
static const weatherForecast = "weather-forecast";
static const thermostat = "thermostat";
static const sensor = "sensor";
static const plantStatus = "plant-status";
static const pictureEntity = "picture-entity";
static const pictureElements = "picture-elements";
static const picture = "picture";
static const map = "map";
static const iframe = "iframe";
static const gauge = "gauge";
static const entityButton = "entity-button";
static const conditional = "conditional";
static const alarmPanel = "alarm-panel";
static const markdown = "markdown";
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 BUTTON = "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;
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class AlarmControlPanelEntity extends Entity {
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class AlarmControlPanelControlsWidget extends StatefulWidget {
@ -25,9 +25,12 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
void _callService(AlarmControlPanelEntity entity, String service) {
eventBus.fire(new ServiceCallEvent(
entity.domain, service, entity.entityId,
{"code": "$code"}));
ConnectionManager().callService(
domain: entity.domain,
service: service,
entityId: entity.entityId,
data: {"code": "$code"}
);
setState(() {
code = "";
});
@ -58,7 +61,11 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
FlatButton(
child: new Text("Yes"),
onPressed: () {
eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null));
ConnectionManager().callService(
domain: entity.domain,
service: "alarm_trigger",
entityId: entity.entityId
);
Navigator.of(context).pop();
},
),
@ -241,7 +248,9 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
FlatButton(
child: Text(
"TRIGGER",
style: TextStyle(color: Colors.redAccent)
style: Theme.of(context).textTheme.subhead.copyWith(
color: Theme.of(context).errorColor
)
),
onPressed: () => _askToTrigger(entity),
)

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class AutomationEntity extends Entity {
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../main.dart';
class BadgeWidget extends StatelessWidget {
@override
@ -7,8 +7,7 @@ class BadgeWidget extends StatelessWidget {
double iconSize = 26.0;
Widget badgeIcon;
String onBadgeTextValue;
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
EntityColor.badgeColors["default"];
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
switch (entityModel.entityWrapper.entity.domain) {
case "sun":
{
@ -30,30 +29,43 @@ class BadgeWidget extends StatelessWidget {
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
color: Theme.of(context).textTheme.body1.color
);
break;
}
case "device_tracker":
case "person":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
color: Theme.of(context).textTheme.body1.color
);
onBadgeTextValue = entityModel.entityWrapper.entity.state;
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
break;
}
default:
{
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
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.state}",
"${entityModel.entityWrapper.entity.displayState}",
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17.0),
style: Theme.of(context).textTheme.body1.copyWith(
fontSize: stateFontSize
)
),
);
break;
@ -66,7 +78,9 @@ class BadgeWidget extends StatelessWidget {
onBadgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$onBadgeTextValue",
style: TextStyle(fontSize: 12.0, color: Colors.white),
style: Theme.of(context).textTheme.overline.copyWith(
color: HAClientTheme().getOnBadgeTextColor()
),
textAlign: TextAlign.center,
softWrap: false,
overflow: TextOverflow.fade),
@ -87,7 +101,7 @@ class BadgeWidget extends StatelessWidget {
decoration: new BoxDecoration(
// Circle shape
shape: BoxShape.circle,
color: Colors.white,
color: Theme.of(context).cardColor,
// The border you want
border: new Border.all(
width: 2.0,
@ -120,7 +134,7 @@ class BadgeWidget extends StatelessWidget {
child: Text(
"${entityModel.entityWrapper.displayName}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.0),
style: Theme.of(context).textTheme.caption,
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
@ -129,6 +143,6 @@ class BadgeWidget extends StatelessWidget {
],
),
onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity)));
eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity)));
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class ButtonEntity extends Entity {
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

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

View File

@ -0,0 +1,191 @@
part of '../../../main.dart';
class CameraStreamView extends StatefulWidget {
final bool withControls;
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
@override
_CameraStreamViewState createState() => _CameraStreamViewState();
}
class _CameraStreamViewState extends State<CameraStreamView> {
CameraEntity _entity;
String _streamUrl = "";
bool _isLoaded = false;
double _aspectRatio = 1.33;
String _webViewHtml;
String _jsMessageChannelName = 'unknown';
Completer _loading;
@override
void initState() {
super.initState();
}
Future _loadResources() {
if (_loading != null && !_loading.isCompleted) {
Logger.d("[Camera Player] Resources loading is not finished yet");
return _loading.future;
}
Logger.d("[Camera Player] Loading resources");
_loading = Completer();
_entity = EntityModel
.of(context)
.entityWrapper
.entity;
if (_entity.supportStream) {
HomeAssistant().getCameraStream(_entity.entityId)
.then((data) {
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
_webViewHtml = Uri.dataFromString(
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8')
).toString();
_loading.complete();
});
})
.catchError((e) {
if (e == 'start_stream_failed') {
Logger.e("[Camera Player] Home Assistant failed starting stream. Forcing MJPEG: $e");
_loadMJPEG().then((_) {
_loading.complete();
});
} else {
_loading.completeError(e);
Logger.e("[Camera Player] Error loading stream: $e");
}
});
} else {
_loadMJPEG().then((_) {
_loading.complete();
});
}
return _loading.future;
}
Future _loadMJPEG() async {
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
.entityId}?token=${_entity.attributes['access_token']}';
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
var file = await rootBundle.loadString('assets/html/cameraView.html');
_webViewHtml = Uri.dataFromString(
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8')
).toString();
}
Widget _buildScreen() {
Widget screenWidget;
if (!_isLoaded) {
screenWidget = Center(
child: EntityPicture(
fit: BoxFit.contain,
)
);
} else {
screenWidget = WebView(
initialUrl: _webViewHtml,
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
debuggingEnabled: Logger.isInDebugMode,
gestureNavigationEnabled: false,
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: {
JavascriptChannel(
name: _jsMessageChannelName,
onMessageReceived: ((message) {
Logger.d('[Camera Player] Message from page: $message');
setState((){
_aspectRatio = double.tryParse(message.message) ?? 1.33;
});
})
)
}
);
}
return AspectRatio(
aspectRatio: _aspectRatio,
child: screenWidget
);
}
Widget _buildControls() {
return Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
iconSize: 40,
color: Theme.of(context).accentColor,
onPressed: _isLoaded ? () {
setState(() {
_isLoaded = false;
});
} : null,
),
Expanded(
child: Container(),
),
IconButton(
icon: Icon(Icons.fullscreen),
iconSize: 40,
color: Theme.of(context).accentColor,
onPressed: _isLoaded ? () {
eventBus.fire(ShowEntityPageEvent());
Navigator.of(context).push(
MaterialPageRoute(
builder: (conext) => FullScreenPage(
child: EntityModel(
child: CameraStreamView(
withControls: false
),
handleTap: false,
entityWrapper: EntityWrapper(
entity: _entity
),
),
),
fullscreenDialog: true
)
).then((_) {
eventBus.fire(ShowEntityPageEvent(entity: _entity));
});
} : null,
)
],
);
}
@override
Widget build(BuildContext context) {
if (!_isLoaded && (_loading == null || _loading.isCompleted)) {
_loadResources().then((_) => setState((){ _isLoaded = true; }));
}
if (widget.withControls) {
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildScreen(),
_buildControls()
],
),
);
} else {
return _buildScreen();
}
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class ClimateEntity extends Entity {
@ -10,71 +10,57 @@ class ClimateEntity extends Entity {
);
static const SUPPORT_TARGET_TEMPERATURE = 1;
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
static const SUPPORT_TARGET_HUMIDITY = 8;
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
static const SUPPORT_FAN_MODE = 64;
static const SUPPORT_OPERATION_MODE = 128;
static const SUPPORT_HOLD_MODE = 256;
static const SUPPORT_SWING_MODE = 512;
static const SUPPORT_AWAY_MODE = 1024;
static const SUPPORT_AUX_HEAT = 2048;
static const SUPPORT_ON_OFF = 4096;
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 supportTargetTemperatureHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
bool get supportTargetTemperatureLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
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 supportTargetHumidityHigh => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
bool get supportTargetHumidityLow => ((supportedFeatures &
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
bool get supportFanMode =>
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
ClimateEntity.SUPPORT_FAN_MODE);
bool get supportOperationMode => ((supportedFeatures &
ClimateEntity.SUPPORT_OPERATION_MODE) ==
ClimateEntity.SUPPORT_OPERATION_MODE);
bool get supportHoldMode =>
((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) ==
ClimateEntity.SUPPORT_HOLD_MODE);
bool get supportSwingMode =>
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
ClimateEntity.SUPPORT_SWING_MODE);
bool get supportAwayMode =>
((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) ==
ClimateEntity.SUPPORT_AWAY_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);
bool get supportOnOff =>
((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) ==
ClimateEntity.SUPPORT_ON_OFF);
List<String> get operationList => attributes["operation_list"] != null
? (attributes["operation_list"] as List).cast<String>()
List<String> get hvacModes => attributes["hvac_modes"] != null
? (attributes["hvac_modes"] as List).cast<String>()
: null;
List<String> get fanList => attributes["fan_list"] != null
? (attributes["fan_list"] as List).cast<String>()
List<String> get fanModes => attributes["fan_modes"] != null
? (attributes["fan_modes"] as List).cast<String>()
: null;
List<String> get swingList => attributes["swing_list"] != null
? (attributes["swing_list"] as List).cast<String>()
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;
@ -83,11 +69,12 @@ class ClimateEntity extends Entity {
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
double get minHumidity => _getDoubleAttributeValue('min_humidity');
double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5;
String get operationMode => attributes['operation_mode'];
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 isOff => state == EntityState.off;
bool get auxHeat => attributes['aux_heat'] == "on";
@override
@ -96,10 +83,8 @@ class ClimateEntity extends Entity {
if (supportTargetTemperature) {
historyConfig.numericAttributesToShow.add("temperature");
}
if (supportTargetTemperatureHigh) {
if (supportTargetTemperatureRange) {
historyConfig.numericAttributesToShow.add("target_temp_high");
}
if (supportTargetTemperatureLow) {
historyConfig.numericAttributesToShow.add("target_temp_low");
}
}

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class ClimateControlWidget extends StatefulWidget {
@ -10,35 +10,35 @@ class ClimateControlWidget extends StatefulWidget {
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _showPending = false;
bool _temperaturePending = 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 _tmpOperationMode;
String _tmpHVACMode;
String _tmpFanMode;
String _tmpSwingMode;
bool _tmpAwayMode = false;
bool _tmpIsOff = false;
String _tmpPresetMode;
//bool _tmpIsOff = false;
bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
_tmpOperationMode = entity.operationMode;
if (!_temperaturePending) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
}
_tmpHVACMode = entity.state;
_tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode;
_tmpAwayMode = entity.awayMode;
_tmpIsOff = entity.isOff;
_tmpPresetMode = entity.presetMode;
//_tmpIsOff = entity.isOff;
_tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity;
_showPending = false;
_changedHere = false;
}
@ -73,36 +73,44 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
void _setTemperature(ClimateEntity entity) {
if (_tempThrottleTimer!=null) {
_tempThrottleTimer.cancel();
}
_tempThrottleTimer?.cancel();
setState(() {
_changedHere = true;
_temperaturePending = true;
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
});
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() {
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
_temperaturePending = false;
ConnectionManager().callService(
domain: entity.domain,
service: "set_temperature",
entityId: entity.entityId,
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
);
});
});
}
void _setTargetTemp(ClimateEntity entity) {
if (_targetTempThrottleTimer!=null) {
_targetTempThrottleTimer.cancel();
}
_targetTempThrottleTimer?.cancel();
setState(() {
_changedHere = true;
_temperaturePending = true;
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
});
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() {
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
_resetStateTimer(entity);
_temperaturePending = false;
ConnectionManager().callService(
domain: entity.domain,
service: "set_temperature",
entityId: entity.entityId,
data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
);
});
});
}
@ -111,17 +119,25 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() {
_tmpTargetHumidity = value.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
_resetStateTimer(entity);
ConnectionManager().callService(
domain: entity.domain,
service: "set_humidity",
entityId: entity.entityId,
data: {"humidity": "$_tmpTargetHumidity"}
);
});
}
void _setOperationMode(ClimateEntity entity, value) {
void _setHVACMode(ClimateEntity entity, value) {
setState(() {
_tmpOperationMode = value;
_tmpHVACMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
_resetStateTimer(entity);
ConnectionManager().callService(
domain: entity.domain,
service: "set_hvac_mode",
entityId: entity.entityId,
data: {"hvac_mode": "$_tmpHVACMode"}
);
});
}
@ -129,8 +145,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() {
_tmpSwingMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
_resetStateTimer(entity);
ConnectionManager().callService(
domain: entity.domain,
service: "set_swing_mode",
entityId: entity.entityId,
data: {"swing_mode": "$_tmpSwingMode"}
);
});
}
@ -138,45 +158,32 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
setState(() {
_tmpFanMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
_resetStateTimer(entity);
ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"});
});
}
void _setAwayMode(ClimateEntity entity, value) {
void _setPresetMode(ClimateEntity entity, value) {
setState(() {
_tmpAwayMode = value;
_tmpPresetMode = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
_resetStateTimer(entity);
ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"});
});
}
void _setOnOf(ClimateEntity entity, value) {
/*void _setOnOf(ClimateEntity entity, value) {
setState(() {
_tmpIsOff = !value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
_resetStateTimer(entity);
});
}
}*/
void _setAuxHeat(ClimateEntity entity, value) {
setState(() {
_tmpAuxHeat = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
_resetStateTimer(entity);
});
}
void _resetStateTimer(ClimateEntity entity) {
if (_resetTimer!=null) {
_resetTimer.cancel();
}
_resetTimer = Timer(Duration(seconds: 3), () {
setState(() {});
_resetVars(entity);
ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"});
});
}
@ -184,11 +191,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity;
Logger.d("[Climate widget build] changed here = $_changedHere");
if (_changedHere) {
_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
//_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
_changedHere = false;
} else {
_resetTimer?.cancel();
_resetVars(entity);
}
return Padding(
@ -196,33 +203,34 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildOnOffControl(entity),
_buildTemperatureControls(entity),
_buildTargetTemperatureControls(entity),
_buildHumidityControls(entity),
_buildOperationControl(entity),
_buildFanControl(entity),
_buildSwingControl(entity),
_buildAwayModeControl(entity),
_buildAuxHeatControl(entity)
//_buildOnOffControl(entity),
_buildTemperatureControls(entity, context),
_buildTargetTemperatureControls(entity, context),
_buildHumidityControls(entity, context),
_buildOperationControl(entity, context),
_buildFanControl(entity, context),
_buildSwingControl(entity, context),
_buildPresetModeControl(entity, context),
_buildAuxHeatControl(entity, context)
],
),
);
}
Widget _buildAwayModeControl(ClimateEntity entity) {
if (entity.supportAwayMode) {
return ModeSwitchWidget(
caption: "Away mode",
onChange: (value) => _setAwayMode(entity, value),
value: _tmpAwayMode,
Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) {
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) {
/*Widget _buildOnOffControl(ClimateEntity entity) {
if (entity.supportOnOff) {
return ModeSwitchWidget(
onChange: (value) => _setOnOf(entity, value),
@ -232,9 +240,9 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
} else {
return Container(height: 0.0, width: 0.0,);
}
}
}*/
Widget _buildAuxHeatControl(ClimateEntity entity) {
Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) {
if (entity.supportAuxHeat ) {
return ModeSwitchWidget(
caption: "Aux heat",
@ -246,23 +254,23 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildOperationControl(ClimateEntity entity) {
if (entity.supportOperationMode) {
Widget _buildOperationControl(ClimateEntity entity, BuildContext context) {
if (entity.hvacModes != null) {
return ModeSelectorWidget(
onChange: (mode) => _setOperationMode(entity, mode),
options: entity.operationList,
onChange: (mode) => _setHVACMode(entity, mode),
options: entity.hvacModes,
caption: "Operation",
value: _tmpOperationMode,
value: _tmpHVACMode,
);
} else {
return Container(height: 0.0, width: 0.0);
}
}
Widget _buildFanControl(ClimateEntity entity) {
Widget _buildFanControl(ClimateEntity entity, BuildContext context) {
if (entity.supportFanMode) {
return ModeSelectorWidget(
options: entity.fanList,
options: entity.fanModes,
onChange: (mode) => _setFanMode(entity, mode),
caption: "Fan mode",
value: _tmpFanMode,
@ -272,11 +280,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildSwingControl(ClimateEntity entity) {
Widget _buildSwingControl(ClimateEntity entity, BuildContext context) {
if (entity.supportSwingMode) {
return ModeSelectorWidget(
onChange: (mode) => _setSwingMode(entity, mode),
options: entity.swingList,
options: entity.swingModes,
value: _tmpSwingMode,
caption: "Swing mode"
);
@ -285,17 +293,15 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildTemperatureControls(ClimateEntity entity) {
Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) {
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
Text("Target temperature", style: Theme.of(context).textTheme.body1),
TemperatureControlWidget(
value: _tmpTemperature,
fontColor: _showPending ? Colors.red : Colors.black,
active: _temperaturePending,
onDec: () => _temperatureDown(entity),
onInc: () => _temperatureUp(entity),
)
@ -306,13 +312,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) {
List<Widget> controls = [];
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
controls.addAll(<Widget>[
TemperatureControlWidget(
value: _tmpTargetLow,
fontColor: _showPending ? Colors.red : Colors.black,
active: _temperaturePending,
onDec: () => _targetLowDown(entity),
onInc: () => _targetLowUp(entity),
),
@ -321,11 +327,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
)
]);
}
if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) {
if ((entity.supportTargetTemperatureRange) && (entity.targetHigh != null)) {
controls.add(
TemperatureControlWidget(
value: _tmpTargetHigh,
fontColor: _showPending ? Colors.red : Colors.black,
active: _temperaturePending,
onDec: () => _targetHighDown(entity),
onInc: () => _targetHighUp(entity),
)
@ -335,9 +341,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature range", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
Text("Target temperature range", style: Theme.of(context).textTheme.body1),
Row(
children: controls,
)
@ -348,13 +352,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildHumidityControls(ClimateEntity entity) {
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) {
List<Widget> result = [];
if (entity.supportTargetHumidity) {
result.addAll(<Widget>[
Text(
"$_tmpTargetHumidity%",
style: TextStyle(fontSize: Sizes.largeFontSize),
style: Theme.of(context).textTheme.display1,
),
Expanded(
child: Slider(
@ -379,9 +383,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Target humidity", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
child: Text("Target humidity", style: Theme.of(context).textTheme.body1),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
@ -403,57 +405,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
@override
void dispose() {
_resetTimer?.cancel();
super.dispose();
}
}
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

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class ClimateStateWidget extends StatelessWidget {
@override
@ -8,13 +8,19 @@ class ClimateStateWidget extends StatelessWidget {
String targetTemp = "-";
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
targetTemp = "${entity.temperature}";
} else if ((entity.supportTargetTemperatureLow) &&
(entity.targetLow != null)) {
targetTemp = "${entity.targetLow}";
if ((entity.supportTargetTemperatureHigh) &&
(entity.targetHigh != null)) {
targetTemp += " - ${entity.targetHigh}";
}
} 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(
@ -25,25 +31,18 @@ class ClimateStateWidget extends StatelessWidget {
children: <Widget>[
Row(
children: <Widget>[
Text("${entity.state}",
Text("$displayState",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: Sizes.stateFontSize,
)),
style: Theme.of(context).textTheme.body2),
Text(" $targetTemp",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
))
style: Theme.of(context).textTheme.body1)
],
),
entity.attributes["current_temperature"] != null ?
Text("Currently: ${entity.attributes["current_temperature"]}",
entity.currentTemperature != null ?
Text("Currently: ${entity.currentTemperature}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
color: Colors.black45)
style: Theme.of(context).textTheme.subtitle
) :
Container(height: 0.0,)
],

View File

@ -1,12 +1,10 @@
part of '../../main.dart';
part of '../../../main.dart';
class ModeSelectorWidget extends StatelessWidget {
final String caption;
final List<String> options;
final List options;
final String value;
final double captionFontSize;
final double valueFontSize;
final onChange;
final EdgeInsets padding;
@ -16,8 +14,6 @@ class ModeSelectorWidget extends StatelessWidget {
@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);
@ -28,9 +24,7 @@ class ModeSelectorWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("$caption", style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
)),
Text("$caption", style: Theme.of(context).textTheme.body1),
Row(
children: <Widget>[
Expanded(
@ -40,15 +34,12 @@ class ModeSelectorWidget extends StatelessWidget {
value: value,
iconSize: 30.0,
isExpanded: true,
style: TextStyle(
fontSize: valueFontSize ?? Sizes.largeFontSize,
color: Colors.black,
),
style: Theme.of(context).textTheme.title,
hint: Text("Select ${caption.toLowerCase()}"),
items: options.map((String value) {
items: options.map((value) {
return new DropdownMenuItem<String>(
value: value,
child: Text(value),
value: '$value',
child: Text('$value'),
);
}).toList(),
onChanged: (mode) => onChange(mode),

View File

@ -1,10 +1,9 @@
part of '../../main.dart';
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;
@ -13,7 +12,6 @@ class ModeSwitchWidget extends StatelessWidget {
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)
@ -25,7 +23,7 @@ class ModeSwitchWidget extends StatelessWidget {
padding: this.padding,
child: Row(
children: <Widget>[
_buildCaption(),
_buildCaption(context),
Switch(
onChanged: (value) => onChange(value),
value: value ?? false,
@ -35,12 +33,10 @@ class ModeSwitchWidget extends StatelessWidget {
);
}
Widget _buildCaption() {
Widget _buildCaption(BuildContext context) {
Widget captionWidget = Text(
"$caption",
style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
),
style: Theme.of(context).textTheme.body1,
);
if (expanded) {
return Expanded(

View File

@ -0,0 +1,47 @@
part of '../../../main.dart';
class TemperatureControlWidget extends StatelessWidget {
final double value;
final bool active;
final onInc;
final onDec;
TemperatureControlWidget(
{Key key,
@required this.value,
@required this.onInc,
@required this.onDec,
//this.fontSize,
this.active: false
})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"$value",
style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1,
),
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

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class CoverEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class CoverControlWidget extends StatefulWidget {
@ -18,7 +18,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
setState(() {
_tmpPosition = position.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
ConnectionManager().callService(domain: entity.domain, service: "set_cover_position", entityId: entity.entityId, data: {"position": _tmpPosition.round()});
});
}
@ -26,7 +26,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
setState(() {
_tmpTiltPosition = position.roundToDouble();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
ConnectionManager().callService(domain: entity.domain, service: "set_cover_tilt_position", entityId: entity.entityId, data: {"tilt_position": _tmpTiltPosition.round()});
});
}
@ -64,9 +64,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
child: Text("Position"),
),
Slider(
value: _tmpPosition,
@ -118,9 +116,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Tilt position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
child: Text("Tilt position"),
));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -135,18 +131,18 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
class CoverTiltControlsWidget extends StatelessWidget {
void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "open_cover_tilt", entity.entityId, null));
ConnectionManager().callService(
domain: entity.domain, service: "open_cover_tilt", entityId: entity.entityId);
}
void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "close_cover_tilt", entity.entityId, null));
ConnectionManager().callService(
domain: entity.domain, service: "close_cover_tilt", entityId: entity.entityId);
}
void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "stop_cover_tilt", entity.entityId, null));
ConnectionManager().callService(
domain: entity.domain, service: "stop_cover_tilt", entityId: entity.entityId);
}
@override

View File

@ -1,19 +1,28 @@
part of '../../main.dart';
part of '../../../main.dart';
class CoverStateWidget extends StatelessWidget {
void _open(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "open_cover", entity.entityId, null));
ConnectionManager().callService(
domain: entity.domain,
service: "open_cover",
entityId: entity.entityId
);
}
void _close(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "close_cover", entity.entityId, null));
ConnectionManager().callService(
domain: entity.domain,
service: "close_cover",
entityId: entity.entityId
);
}
void _stop(CoverEntity entity) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "stop_cover", entity.entityId, null));
ConnectionManager().callService(
domain: entity.domain,
service: "stop_cover",
entityId: entity.entityId
);
}
@override

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class DateTimeEntity extends Entity {
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
@ -35,8 +35,7 @@ class DateTimeEntity extends Entity {
return formattedState;
}
void setNewState(newValue) {
eventBus
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
void setNewState(Map newValue) {
ConnectionManager().callService(domain: domain, service: "set_datetime", entityId: entityId, data: newValue);
}
}

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class DateTimeStateWidget extends StatelessWidget {
@override
@ -9,10 +9,8 @@ class DateTimeStateWidget extends StatelessWidget {
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,
)),
textAlign: TextAlign.right
),
onTap: () => _handleStateTap(context, entity),
));
}

View File

@ -0,0 +1,73 @@
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();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Divider(),
Text(
"${entityModel.entityWrapper.entity.displayName}",
style: HAClientTheme().getLinkTextStyle(context).copyWith(
decoration: TextDecoration.none
)
)
],
);
}
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();
}
},
onDoubleTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleDoubleTap();
}
},
child: result,
);
} else {
return result;
}
}
}

View File

@ -27,7 +27,7 @@ class Entity {
"cold.on": "Cold",
"cold.off": "Normal",
"connectivity.on": "Connected",
"connectivity.off": "Diconnected",
"connectivity.off": "Disconnected",
"door.on": "Open",
"door.off": "Closed",
"garage_door.on": "Open",
@ -153,8 +153,8 @@ class Entity {
domain = rawData["entity_id"].split(".")[0];
entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state;
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
}
@ -211,31 +211,6 @@ class Entity {
);
}
Widget buildEntityPageWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding),
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
),
LastUpdatedWidget(),
Divider(),
_buildAdditionalControlsForPage(context),
Divider(),
buildHistoryWidget(),
EntityAttributesList()
]),
handleTap: false,
);
}
Widget buildHistoryWidget() {
return EntityHistoryWidget(
config: historyConfig,
);
}
Widget buildBadgeWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
@ -246,7 +221,7 @@ class Entity {
String getAttribute(String attributeName) {
if (attributes != null) {
return attributes["$attributeName"];
return attributes["$attributeName"].toString();
}
return null;
}

View File

@ -67,7 +67,7 @@ class EntityIcon extends StatelessWidget {
padding: padding,
child: buildIcon(
entityWrapper,
color ?? EntityColor.stateColor(entityWrapper.entity.state)
color ?? HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context)
),
);
}

View File

@ -15,26 +15,6 @@ class EntityModel extends InheritedWidget {
return context.inheritFromWidgetOfExactType(EntityModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}
class HomeAssistantModel extends InheritedWidget {
const HomeAssistantModel({
Key key,
@required this.homeAssistant,
@required Widget child,
}) : super(key: key, child: child);
final HomeAssistant homeAssistant;
static HomeAssistantModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;

View File

@ -5,18 +5,24 @@ class EntityName extends StatelessWidget {
final EdgeInsetsGeometry padding;
final TextOverflow textOverflow;
final bool wordsWrap;
final double fontSize;
final TextAlign textAlign;
final int maxLines;
final TextStyle textStyle;
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);
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.textStyle, this.wordsWrap: true, 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);
TextStyle tStyle;
if (textStyle == null) {
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
tStyle = HAClientTheme().getLinkTextStyle(context);
} else {
tStyle = Theme.of(context).textTheme.body1;
}
} else {
tStyle = textStyle;
}
return Padding(
padding: padding,
@ -25,7 +31,7 @@ class EntityName extends StatelessWidget {
overflow: textOverflow,
softWrap: wordsWrap,
maxLines: maxLines,
style: textStyle,
style: tStyle,
textAlign: textAlign,
),
);

View File

@ -0,0 +1,66 @@
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: Theme.of(context).primaryColor,
height: 40,
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8),
child: Text(
entity.displayName,
style: Theme.of(context).primaryTextTheme.headline
),
),
),
IconButton(
padding: EdgeInsets.all(0),
icon: Icon(Icons.close),
color: Theme.of(context).primaryTextTheme.headline.color,
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,71 @@
part of '../main.dart';
class EntityPicture extends StatelessWidget {
final EdgeInsetsGeometry padding;
final BoxFit fit;
const EntityPicture({Key key, this.padding: const EdgeInsets.all(0.0), this.fit: BoxFit.cover}) : 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, BuildContext context) {
if (data == null) {
return null;
}
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); //
}
Widget iconPicture = Container(
child: Center(
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: Sizes.largeIconSize,
color: HAClientTheme().getOffStateColor(context),
)
)
);
if (data.entityPicture != null) {
return CachedNetworkImage(
imageUrl: data.entityPicture,
fit: this.fit,
errorWidget: (context, _, __) => iconPicture,
placeholder: (context, _) => iconPicture,
);
}
return iconPicture;
}
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: buildIcon(
entityWrapper,
context
),
);
}
}

View File

@ -0,0 +1,155 @@
part of '../main.dart';
class EntityWrapper {
String overrideName;
final String overrideIcon;
EntityUIAction uiAction;
Entity entity;
String unitOfMeasurementOverride;
final List stateFilter;
String get icon => overrideIcon ?? entity.icon;
String get entityPicture => entity.entityPicture;
String get displayName => overrideName ?? entity.displayName;
String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement;
EntityWrapper({
this.entity,
this.overrideIcon,
this.overrideName,
this.uiAction,
this.stateFilter
}) {
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
if (uiAction == null) {
uiAction = EntityUIAction();
}
}
}
void handleTap() {
switch (uiAction.tapAction) {
case EntityUIAction.toggle: {
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
break;
}
case EntityUIAction.callService: {
if (uiAction.tapService != null) {
ConnectionManager().callService(
domain: uiAction.tapService.split(".")[0],
service: uiAction.tapService.split(".")[1],
data: uiAction.tapServiceData
);
}
break;
}
case EntityUIAction.none: {
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.tapService != null && 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(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
break;
}
case EntityUIAction.callService: {
if (uiAction.holdService != null) {
ConnectionManager().callService(
domain: uiAction.holdService.split(".")[0],
service: uiAction.holdService.split(".")[1],
data: uiAction.holdServiceData
);
}
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
Launcher.launchURL(uiAction.holdService);
}
break;
}
default: {
break;
}
}
}
void handleDoubleTap() {
switch (uiAction.doubleTapAction) {
case EntityUIAction.toggle: {
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
break;
}
case EntityUIAction.callService: {
if (uiAction.doubleTapService != null) {
ConnectionManager().callService(
domain: uiAction.doubleTapService.split(".")[0],
service: uiAction.doubleTapService.split(".")[1],
data: uiAction.doubleTapServiceData
);
}
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
Launcher.launchURL(uiAction.doubleTapService);
}
break;
}
default: {
break;
}
}
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class FanEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class FanControlsWidget extends StatefulWidget {
@ -24,9 +24,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
setState(() {
_tmpOscillate = oscillate;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
"fan", "oscillate", entity.entityId,
{"oscillating": oscillate}));
ConnectionManager().callService(
domain: "fan",
service: "oscillate",
entityId: entity.entityId,
data: {"oscillating": oscillate}
);
});
}
@ -34,9 +37,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
setState(() {
_tmpDirectionForward = forward;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
"fan", "set_direction", entity.entityId,
{"direction": forward ? "forward" : "reverse"}));
ConnectionManager().callService(
domain: "fan",
service: "set_direction",
entityId: entity.entityId,
data: {"direction": forward ? "forward" : "reverse"}
);
});
}
@ -44,9 +50,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
setState(() {
_tmpSpeed = value;
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
"fan", "set_speed", entity.entityId,
{"speed": value}));
ConnectionManager().callService(
domain: "fan",
service: "set_speed",
entityId: entity.entityId,
data: {"speed": value}
);
});
}

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../main.dart';
class FlatServiceButton extends StatelessWidget {
@ -6,7 +6,6 @@ class FlatServiceButton extends StatelessWidget {
final String serviceName;
final String entityId;
final String text;
final double fontSize;
FlatServiceButton({
Key key,
@ -14,17 +13,16 @@ class FlatServiceButton extends StatelessWidget {
@required this.serviceName,
@required this.entityId,
@required this.text,
this.fontSize: Sizes.stateFontSize
}) : super(key: key);
void _setNewState() {
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null));
ConnectionManager().callService(domain: serviceDomain, service: serviceName, entityId: entityId);
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: fontSize*2.5,
height: Theme.of(context).textTheme.subhead.fontSize*2.5,
child: FlatButton(
onPressed: (() {
_setNewState();
@ -32,8 +30,7 @@ class FlatServiceButton extends StatelessWidget {
child: Text(
text,
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: fontSize, color: Colors.blue),
style: HAClientTheme().getActionTextStyle(context),
),
)
);

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class GroupEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class LightEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class LightColorPicker extends StatefulWidget {
@ -27,7 +27,6 @@ class LightColorPickerState extends State<LightColorPicker> {
List<Widget> colorRows = [];
Border border;
bool isSomethingSelected = false;
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
List<Widget> rowChildren = [];
//Logger.d("$saturation");

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class LightControlsWidget extends StatefulWidget {
@ -17,7 +17,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
String _tmpEffect;
void _resetState(LightEntity entity) {
_tmpBrightness = entity.brightness ?? 0;
_tmpBrightness = entity.brightness ?? 1;
_tmpWhiteValue = entity.whiteValue ?? 0;
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
_tmpColor = entity.color ?? _tmpColor;
@ -28,15 +28,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() {
_tmpBrightness = value.round();
_changedHere = true;
if (_tmpBrightness > 0) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"brightness": _tmpBrightness}));
} else {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_off", entity.entityId,
null));
}
ConnectionManager().callService(
domain: entity.domain,
service: "turn_on",
entityId: entity.entityId,
data: {"brightness": _tmpBrightness}
);
});
}
@ -44,9 +41,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() {
_tmpWhiteValue = value.round();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"white_value": _tmpWhiteValue}));
ConnectionManager().callService(
domain: entity.domain,
service: "turn_on",
entityId: entity.entityId,
data: {"white_value": _tmpWhiteValue}
);
});
}
@ -55,9 +55,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() {
_tmpColorTemp = value.round();
_changedHere = true;
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"color_temp": _tmpColorTemp}));
ConnectionManager().callService(
domain: entity.domain,
service: "turn_on",
entityId: entity.entityId,
data: {"color_temp": _tmpColorTemp}
);
});
}
@ -65,10 +68,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
setState(() {
_tmpColor = color;
_changedHere = true;
Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"hs_color": [color.hue, color.saturation*100]}));
ConnectionManager().callService(
domain: entity.domain,
service: "turn_on",
entityId: entity.entityId,
data: {"hs_color": [color.hue, color.saturation*100]}
);
});
}
@ -77,9 +82,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpEffect = value;
_changedHere = true;
if (_tmpEffect != null) {
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
{"effect": "$value"}));
ConnectionManager().callService(
domain: entity.domain,
service: "turn_on",
entityId: entity.entityId,
data: {"effect": "$value"}
);
}
});
}
@ -106,7 +114,19 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}
Widget _buildBrightnessControl(LightEntity entity) {
if ((entity.supportBrightness) && (_tmpBrightness != null)) {
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(() {
@ -114,10 +134,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpBrightness = value.round();
});
},
min: 0.0,
min: 1.0,
max: 255.0,
onChangeEnd: (value) => _setBrightness(entity, value),
value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(),
value: val,
leading: Icon(Icons.brightness_5),
title: "Brightness",
);
@ -149,10 +169,22 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
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: _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(),
leading: Text("Cold", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.lightBlue)),
value: val,
onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds,
min: entity.minMireds,
@ -162,7 +194,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpColorTemp = value.round();
});
},
closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
closing: Text("Warm", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.amberAccent),),
);
} else {
return Container(width: 0.0, height: 0.0);
@ -171,7 +203,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
Widget _buildColorControl(LightEntity entity) {
if (entity.supportColor) {
HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor;
HSVColor savedColor = HomeAssistant().savedColor;
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -187,15 +219,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
child: Text('Copy color'),
onPressed: _tmpColor == null ? null : () {
setState(() {
HomeAssistantModel
.of(context)
.homeAssistant
.savedColor = _tmpColor;
HomeAssistant().savedColor = _tmpColor;
});
},
),
FlatButton(
color: savedColor?.toColor() ?? Colors.transparent,
color: savedColor?.toColor() ?? Theme.of(context).backgroundColor,
child: Text('Paste color'),
onPressed: savedColor == null ? null : () {
_setColor(entity, savedColor);
@ -212,10 +241,14 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
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: entity.effectList,
options: list,
value: _tmpEffect
);
} else {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class LockEntity extends Entity {
LockEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class LockStateWidget extends StatelessWidget {
@ -7,11 +7,11 @@ class LockStateWidget extends StatelessWidget {
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
void _lock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
ConnectionManager().callService(domain: "lock", service: "lock", entityId: entity.entityId);
}
void _unlock(Entity entity) {
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null));
ConnectionManager().callService(domain: "lock", service: "unlock", entityId: entity.entityId);
}
@override
@ -28,8 +28,7 @@ class LockStateWidget extends StatelessWidget {
onPressed: () => _unlock(entity),
child: Text("UNLOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
style: HAClientTheme().getActionTextStyle(context)
),
)
),
@ -39,8 +38,7 @@ class LockStateWidget extends StatelessWidget {
onPressed: () => _lock(entity),
child: Text("LOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
style: HAClientTheme().getActionTextStyle(context),
),
)
)
@ -56,8 +54,7 @@ class LockStateWidget extends StatelessWidget {
child: Text(
entity.isLocked ? "UNLOCK" : "LOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
style: HAClientTheme().getActionTextStyle(context),
),
)
);

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class MediaPlayerEntity extends Entity {
@ -74,10 +74,34 @@ class MediaPlayerEntity extends Entity {
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
List<String> get sourceList => getStringListAttributeValue("source_list");
DateTime get positionLastUpdated => DateTime.tryParse("${attributes["media_position_updated_at"]}")?.toLocal();
int get durationSeconds => _getIntAttributeValue("media_duration");
int get positionSeconds => _getIntAttributeValue("media_position");
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return MediaPlayerControls();
}
bool canCalculateActualPosition() {
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds > 0;
}
double getActualPosition() {
double result = 0;
Duration durationD;
Duration positionD;
durationD = Duration(seconds: durationSeconds);
positionD = Duration(
seconds: positionSeconds);
result = positionD.inSeconds.toDouble();
int differenceInSeconds = DateTime
.now()
.difference(positionLastUpdated)
.inSeconds;
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
return result;
}
}

View File

@ -0,0 +1,46 @@
part of '../../../main.dart';
class MediaPlayerProgressBar extends StatefulWidget {
@override
_MediaPlayerProgressBarState createState() => _MediaPlayerProgressBarState();
}
class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
Timer _timer;
@override
initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
setState(() {
});
});
}
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress = 0;
int currentPosition;
if (entity.canCalculateActualPosition()) {
currentPosition = entity.getActualPosition().toInt();
if (currentPosition > 0) {
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
}
}
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
valueColor: AlwaysStoppedAnimation<Color>(HAClientTheme().getOnStateColor(context)),
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}

View File

@ -0,0 +1,135 @@
part of '../../../main.dart';
class MediaPlayerSeekBar extends StatefulWidget {
@override
_MediaPlayerSeekBarState createState() => _MediaPlayerSeekBarState();
}
class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
Timer _timer;
bool _seekStarted = false;
bool _changedHere = false;
double _currentPosition = 0;
int _savedPosition = 0;
@override
initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
if (!_seekStarted && !_changedHere) {
setState(() {});
}
});
}
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
if (entity.canCalculateActualPosition() && entity.state != EntityState.idle) {
if (HomeAssistant().sendToPlayerId == entity.entityId && HomeAssistant().savedPlayerPosition != null) {
_savedPosition = HomeAssistant().savedPlayerPosition;
HomeAssistant().savedPlayerPosition = null;
HomeAssistant().sendToPlayerId = null;
}
if (entity.state == EntityState.playing && !_seekStarted &&
!_changedHere) {
_currentPosition = entity.getActualPosition();
} else if (entity.state == EntityState.paused) {
_currentPosition = entity.positionSeconds.toDouble();
} else if (_changedHere) {
_changedHere = false;
}
List<Widget> buttons = [];
if (_savedPosition > 0) {
buttons.add(
RaisedButton(
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
color: Theme.of(context).accentColor,
onPressed: () {
ConnectionManager().callService(
domain: "media_player",
service: "media_seek",
entityId: entity.entityId,
data: {"seek_position": _savedPosition}
);
setState(() {
_savedPosition = 0;
});
},
)
);
}
return Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 20, Sizes.rightWidgetPadding, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text("00:00"),
Expanded(
child: Text(
"${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.title.copyWith(
color: Colors.blue
)
),
),
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
],
),
Container(height: 10,),
Slider(
min: 0,
activeColor: Theme.of(context).accentColor,
max: entity.durationSeconds.toDouble(),
value: _currentPosition,
onChangeStart: (val) {
_seekStarted = true;
},
onChanged: (val) {
setState(() {
_currentPosition = val;
});
},
onChangeEnd: (val) {
_seekStarted = false;
Timer(Duration(milliseconds: 500), () {
if (!_seekStarted) {
ConnectionManager().callService(
domain: "media_player",
service: "media_seek",
entityId: entity.entityId,
data: {"seek_position": val}
);
setState(() {
_changedHere = true;
_currentPosition = val;
});
}
});
},
),
ButtonBar(
children: buttons,
)
],
),
);
} else {
return Container(width: 0, height: 0,);
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}

View File

@ -1,7 +1,7 @@
part of '../../main.dart';
part of '../../../main.dart';
class MediaPlayerWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
@ -12,21 +12,21 @@ class MediaPlayerWidget extends StatelessWidget {
Stack(
alignment: AlignmentDirectional.topEnd,
children: <Widget>[
_buildImage(entity),
_buildImage(entity, context),
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: Container(
color: Colors.black45,
child: _buildState(entity),
child: _buildState(entity, context),
),
),
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: MediaPlayerProgressWidget()
child: MediaPlayerProgressBar()
)
],
),
@ -35,12 +35,9 @@ class MediaPlayerWidget extends StatelessWidget {
);
}
Widget _buildState(MediaPlayerEntity entity) {
TextStyle style = TextStyle(
fontSize: 14.0,
color: Colors.white,
fontWeight: FontWeight.normal,
height: 1.2
Widget _buildState(MediaPlayerEntity entity, BuildContext context) {
TextStyle style = Theme.of(context).textTheme.body1.copyWith(
color: Colors.white
);
List<Widget> states = [];
states.add(Text("${entity.displayName}", style: style));
@ -71,7 +68,7 @@ class MediaPlayerWidget extends StatelessWidget {
);
}
Widget _buildImage(MediaPlayerEntity entity) {
Widget _buildImage(MediaPlayerEntity entity, BuildContext context) {
String state = entity.state;
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
return Container(
@ -97,7 +94,7 @@ class MediaPlayerWidget extends StatelessWidget {
Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
size: 150.0,
color: EntityColor.stateColor("$state"),
color: HAClientTheme().getColorByEntityState("$state", context),
)
],
);
@ -118,26 +115,28 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
void _setPower(MediaPlayerEntity entity) {
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
if (entity.state == EntityState.off) {
Logger.d("${entity.entityId} turn_on");
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_on", entity.entityId,
null));
ConnectionManager().callService(
domain: entity.domain,
service: "turn_on",
entityId: entity.entityId
);
} else {
Logger.d("${entity.entityId} turn_off");
eventBus.fire(new ServiceCallEvent(
entity.domain, "turn_off", entity.entityId,
null));
ConnectionManager().callService(
domain: entity.domain,
service: "turn_off",
entityId: entity.entityId
);
}
}
}
void _callAction(MediaPlayerEntity entity, String action) {
Logger.d("${entity.entityId} $action");
eventBus.fire(new ServiceCallEvent(
entity.domain, "$action", entity.entityId,
null));
ConnectionManager().callService(
domain: entity.domain,
service: "$action",
entityId: entity.entityId
);
}
@override
@ -229,7 +228,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: entity))
)
);
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
@ -264,27 +263,50 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
setState(() {
_changedHere = true;
_newVolumeLevel = value;
eventBus.fire(ServiceCallEvent("media_player", "volume_set", entityId, {"volume_level": value}));
ConnectionManager().callService(
domain: "media_player",
service: "volume_set",
entityId: entityId,
data: {"volume_level": value}
);
});
}
void _setVolumeMute(bool isMuted, String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_mute", entityId, {"is_volume_muted": isMuted}));
ConnectionManager().callService(
domain: "media_player",
service: "volume_mute",
entityId: entityId,
data: {"is_volume_muted": isMuted}
);
}
void _setVolumeUp(String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_up", entityId, null));
ConnectionManager().callService(
domain: "media_player",
service: "volume_up",
entityId: entityId
);
}
void _setVolumeDown(String entityId) {
eventBus.fire(ServiceCallEvent("media_player", "volume_down", entityId, null));
ConnectionManager().callService(
domain: "media_player",
service: "volume_down",
entityId: entityId
);
}
void _setSoundMode(String value, String entityId) {
setState(() {
_newSoundMode = value;
_changedHere = true;
eventBus.fire(ServiceCallEvent("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"}));
ConnectionManager().callService(
domain: "media_player",
service: "select_sound_mode",
entityId: entityId,
data: {"sound_mode": "$value"}
);
});
}
@ -292,7 +314,12 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
setState(() {
_newSource = source;
_changedHere = true;
eventBus.fire(ServiceCallEvent("media_player", "select_source", entityId, {"source": "$source"}));
ConnectionManager().callService(
domain: "media_player",
service: "select_source",
entityId: entityId,
data: {"source": "$source"}
);
});
}
@ -305,6 +332,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
)
];
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
if (entity.supportSeek) {
children.add(MediaPlayerSeekBar());
} else {
children.add(MediaPlayerProgressBar());
}
Widget muteWidget;
Widget volumeStepWidget;
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
@ -321,13 +353,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
volumeStepWidget = Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId)
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
onPressed: () => _setVolumeDown(entity.entityId)
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId)
)
],
);
@ -398,69 +430,47 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
)
);
}
if (entity.state == EntityState.playing || entity.state == EntityState.paused) {
children.add(
ButtonBar(
children: <Widget>[
RaisedButton(
child: Text("Duplicate to"),
color: Colors.blue,
textColor: Colors.white,
onPressed: () => _duplicateTo(entity),
),
RaisedButton(
child: Text("Switch to"),
color: Colors.blue,
textColor: Colors.white,
onPressed: () => _switchTo(entity),
)
],
)
);
}
}
return Column(
children: children,
);
}
}
class MediaPlayerProgressWidget extends StatefulWidget {
@override
_MediaPlayerProgressWidgetState createState() => _MediaPlayerProgressWidgetState();
}
class _MediaPlayerProgressWidgetState extends State<MediaPlayerProgressWidget> {
Timer _timer;
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress;
try {
DateTime lastUpdated = DateTime.parse(
entity.attributes["media_position_updated_at"]).toLocal();
Duration duration = Duration(seconds: entity._getIntAttributeValue("media_duration") ?? 1);
Duration position = Duration(seconds: entity._getIntAttributeValue("media_position") ?? 0);
int currentPosition = position.inSeconds;
if (entity.state == EntityState.playing) {
_timer?.cancel();
_timer = Timer(Duration(seconds: 1), () {
setState(() {
});
});
int differenceInSeconds = DateTime
.now()
.difference(lastUpdated)
.inSeconds;
currentPosition = currentPosition + differenceInSeconds;
} else {
_timer?.cancel();
}
progress = currentPosition / duration.inSeconds;
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
);
} catch (e) {
_timer?.cancel();
progress = 0.0;
void _duplicateTo(entity) {
if (entity.canCalculateActualPosition()) {
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
} else {
HomeAssistant().savedPlayerPosition = 0;
}
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
);
Navigator.of(context).pushNamed("/play-media", arguments: {
"url": entity.attributes["media_content_id"],
"type": entity.attributes["media_content_type"]
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
void _switchTo(entity) {
HomeAssistant().sendFromPlayerId = entity.entityId;
_duplicateTo(entity);
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class SelectEntity extends Entity {
List<String> get listOptions => attributes["options"] != null

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class SelectStateWidget extends StatefulWidget {
@ -11,8 +11,12 @@ class SelectStateWidget extends StatefulWidget {
class _SelectStateWidgetState extends State<SelectStateWidget> {
void setNewState(domain, entityId, newValue) {
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
{"option": "$newValue"}));
ConnectionManager().callService(
domain: domain,
service: "select_option",
entityId: entityId,
data: {"option": "$newValue"}
);
}
@override

View File

@ -1,8 +1,4 @@
part of '../main.dart';
class SunEntity extends Entity {
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
}
part of '../../main.dart';
class SensorEntity extends Entity {

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../main.dart';
class SimpleEntityState extends StatelessWidget {
@ -7,8 +7,10 @@ class SimpleEntityState extends StatelessWidget {
final EdgeInsetsGeometry padding;
final int maxLines;
final String customValue;
final TextStyle textStyle;
//final bool bold;
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
const SimpleEntityState({Key key,/*this.bold: false,*/ this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.textStyle, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -20,24 +22,31 @@ class SimpleEntityState extends StatelessWidget {
} else {
state = customValue;
}
TextStyle textStyle = TextStyle(
fontSize: Sizes.stateFontSize,
);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
textStyle = textStyle.apply(color: Colors.blue);
TextStyle tStyle;
if (textStyle != null) {
tStyle = textStyle;
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
tStyle = Theme.of(context).textTheme.subhead.copyWith(
color: Colors.blue
);
} else {
tStyle = Theme.of(context).textTheme.body1;
}
/*if (this.bold) {
textStyle = textStyle.apply(fontWeightDelta: 100);
}*/
while (state.contains(" ")){
state = state.replaceAll(" ", " ");
}
Widget result = Padding(
padding: padding,
child: Text(
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
"$state ${entityModel.entityWrapper.unitOfMeasurement}",
textAlign: textAlign,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: textStyle
style: tStyle
)
);
if (expanded) {

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class SliderEntity extends Entity {
SliderEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class SliderControlsWidget extends StatefulWidget {
@ -18,8 +18,12 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
_newValue = newValue;
_changedHere = true;
});
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
{"value": "${newValue.toString()}"}));
ConnectionManager().callService(
domain: domain,
service: "set_value",
entityId: entityId,
data: {"value": "${newValue.toString()}"}
);
}
@override
@ -58,8 +62,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
children: <Widget>[
Text(
"$_newValue",
style: TextStyle(
fontSize: Sizes.largeFontSize,
style: Theme.of(context).textTheme.display1.copyWith(
color: Colors.blue
),
),

View File

@ -0,0 +1,5 @@
part of '../../main.dart';
class SunEntity extends Entity {
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class SwitchEntity extends Entity {
SwitchEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class SwitchStateWidget extends StatefulWidget {
@ -38,8 +38,11 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
} else {
domain = entity.domain;
}
eventBus.fire(new ServiceCallEvent(
domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
ConnectionManager().callService(
domain: domain,
service: (newValue as bool) ? "turn_on" : "turn_off",
entityId: entity.entityId
);
}
@override

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class TextEntity extends Entity {
TextEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class TextInputStateWidget extends StatefulWidget {
@ -26,8 +26,12 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
void setNewState(newValue, domain, entityId) {
if (validate(newValue, _minLength, _maxLength)) {
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
{"value": "$newValue"}));
ConnectionManager().callService(
domain: domain,
service: "set_value",
entityId: entityId,
data: {"value": "$newValue"}
);
} else {
setState(() {
_tmpValue = _entityState;
@ -73,13 +77,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
child: TextField(
focusNode: _focusNode,
obscureText: entity.isPasswordField,
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _tmpValue,
selection:
new TextSelection.collapsed(offset: _tmpValue.length)
)
),
controller: TextEditingController.fromValue(TextEditingValue(text: _tmpValue)),
onChanged: (value) {
_tmpValue = value;
}),

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class TimerEntity extends Entity {
TimerEntity(Map rawData, String webHost) : super(rawData, webHost);

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../../../main.dart';
class TimerState extends StatefulWidget {
//final bool expanded;

View File

@ -1,4 +1,4 @@
part of '../../main.dart';
part of '../main.dart';
class UniversalSlider extends StatelessWidget {
@ -40,10 +40,7 @@ class UniversalSlider extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Sizes.rowPadding,),
Text(
"$title",
style: TextStyle(fontSize: Sizes.stateFontSize),
),
Text("$title"),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,

View File

@ -0,0 +1,98 @@
part of '../../main.dart';
class VacuumEntity extends Entity {
static const SUPPORT_TURN_ON = 1;
static const SUPPORT_TURN_OFF = 2;
static const SUPPORT_PAUSE = 4;
static const SUPPORT_STOP = 8;
static const SUPPORT_RETURN_HOME = 16;
static const SUPPORT_FAN_SPEED = 32;
static const SUPPORT_BATTERY = 64;
static const SUPPORT_STATUS = 128;
static const SUPPORT_SEND_COMMAND = 256;
static const SUPPORT_LOCATE = 512;
static const SUPPORT_CLEAN_SPOT = 1024;
static const SUPPORT_MAP = 2048;
static const SUPPORT_STATE = 4096;
static const SUPPORT_START = 8192;
VacuumEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportTurnOn => ((supportedFeatures &
VacuumEntity.SUPPORT_TURN_ON) ==
VacuumEntity.SUPPORT_TURN_ON);
bool get supportTurnOff => ((supportedFeatures &
VacuumEntity.SUPPORT_TURN_OFF) ==
VacuumEntity.SUPPORT_TURN_OFF);
bool get supportPause => ((supportedFeatures &
VacuumEntity.SUPPORT_PAUSE) ==
VacuumEntity.SUPPORT_PAUSE);
bool get supportStop => ((supportedFeatures &
VacuumEntity.SUPPORT_STOP) ==
VacuumEntity.SUPPORT_STOP);
bool get supportReturnHome => ((supportedFeatures &
VacuumEntity.SUPPORT_RETURN_HOME) ==
VacuumEntity.SUPPORT_RETURN_HOME);
bool get supportFanSpeed => ((supportedFeatures &
VacuumEntity.SUPPORT_FAN_SPEED) ==
VacuumEntity.SUPPORT_FAN_SPEED);
bool get supportBattery => ((supportedFeatures &
VacuumEntity.SUPPORT_BATTERY) ==
VacuumEntity.SUPPORT_BATTERY);
bool get supportStatus => ((supportedFeatures &
VacuumEntity.SUPPORT_STATUS) ==
VacuumEntity.SUPPORT_STATUS);
bool get supportSendCommand => ((supportedFeatures &
VacuumEntity.SUPPORT_SEND_COMMAND) ==
VacuumEntity.SUPPORT_SEND_COMMAND);
bool get supportLocate => ((supportedFeatures &
VacuumEntity.SUPPORT_LOCATE) ==
VacuumEntity.SUPPORT_LOCATE);
bool get supportCleanSpot => ((supportedFeatures &
VacuumEntity.SUPPORT_CLEAN_SPOT) ==
VacuumEntity.SUPPORT_CLEAN_SPOT);
bool get supportMap => ((supportedFeatures &
VacuumEntity.SUPPORT_MAP) ==
VacuumEntity.SUPPORT_MAP);
bool get supportState => ((supportedFeatures &
VacuumEntity.SUPPORT_STATE) ==
VacuumEntity.SUPPORT_STATE);
bool get supportStart => ((supportedFeatures &
VacuumEntity.SUPPORT_START) ==
VacuumEntity.SUPPORT_START);
List<String> get fanSpeedList => getStringListAttributeValue("fan_speed_list");
String get fanSpeed => getAttribute("fan_speed");
String get status => getAttribute("status");
int get batteryLevel => _getIntAttributeValue("battery_level");
String get batteryIcon => getAttribute("battery_icon");
double get cleanedArea => _getDoubleAttributeValue("cleaned_area");
@override
Widget _buildStatePart(BuildContext context) {
if (supportTurnOn || supportTurnOff) {
return SwitchStateWidget(
domainForService: "vacuum",
);
} else {
return SimpleEntityState();
}
}
@override
Widget _buildStatePartForPage(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(
entity: this
),
child: VacuumStateButton(),
handleTap: false,
);
}
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {
return VacuumControls();
}
}

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