Compare commits
	
		
			675 Commits
		
	
	
		
			0.5.4
			...
			1.2.0-beta
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | f84728b948 | ||
|  | 26a62d341e | ||
|  | 772bddeb9a | ||
|  | 5b55940ccf | ||
|  | 7683d18e81 | ||
|  | d09afc37b5 | ||
|  | 1c686402d0 | ||
|  | 5f4a3fbdfc | ||
|  | 312ed99e9f | ||
|  | 25e6d51c17 | ||
|  | b501574bab | ||
|  | 53b31d8e90 | ||
|  | 6d80420a9b | ||
|  | e977054139 | ||
|  | 6367d38524 | ||
|  | f9b2d7d84c | ||
|  | 44c28ad106 | ||
|  | fec3c525e1 | ||
|  | b1bbed6d80 | ||
|  | 13878cfc51 | ||
|  | be49180205 | ||
|  | c4a0b16553 | ||
|  | caacd5e9f4 | ||
|  | 5fa28abb6c | ||
|  | e0a28c0b59 | ||
|  | 096e714a04 | ||
|  | 78893ea01f | ||
|  | 90efb29be5 | ||
|  | fca323c56b | ||
|  | e5fe6af5f3 | ||
|  | f0090d522d | ||
|  | edbfd8359b | ||
|  | 2702bb254a | ||
|  | ca7b6ed550 | ||
|  | fb00b5d9ff | ||
|  | 7ffba397ce | ||
|  | 1080076e3b | ||
|  | e295a36465 | ||
|  | 9a09a83dc6 | ||
|  | 95ca80949f | ||
|  | 80b5763530 | ||
|  | 9a5e35b024 | ||
|  | 4493975676 | ||
|  | 141a68faf7 | ||
|  | a8efe7dbb6 | ||
|  | 9608983994 | ||
|  | 8eb15ab9a4 | ||
|  | aac0cfbb56 | ||
|  | 343494ece0 | ||
|  | b1e5e73278 | ||
|  | 9b5a0068fd | ||
|  | aa26212ddd | ||
|  | 1c45f96706 | ||
|  | c2d5192c51 | ||
|  | 88ae80507c | ||
|  | 55868d1dfe | ||
|  | 92a1230267 | ||
|  | d3f99fb262 | ||
|  | 3fdf016c39 | ||
|  | 8ce0e8aafa | ||
|  | 54f6fb28ef | ||
|  | d53825f140 | ||
|  | ea7e0f04ce | ||
|  | cf75989447 | ||
|  | f27d55869b | ||
|  | a287f597ad | ||
|  | 0698950f3d | ||
|  | aa58559ba6 | ||
|  | 94acc67383 | ||
|  | 701e6a46df | ||
|  | 1ed56ce8f1 | ||
|  | a6d1baca77 | ||
|  | dbeda6ea68 | ||
|  | 3dca28a7da | ||
|  | da4264a409 | ||
|  | 302451e118 | ||
|  | d19dbd389b | ||
|  | 05ae954b30 | ||
|  | 840e266381 | ||
|  | 01525a2929 | ||
|  | 2eb2596f37 | ||
|  | 699cab3498 | ||
|  | 353c80b6bc | ||
|  | e4d1a4f823 | ||
|  | 78d6b38b92 | ||
|  | 1499a91ef7 | ||
|  | 9b7f7aa380 | ||
|  | 5683ab5158 | ||
|  | a20dfaf05e | ||
|  | 24d42c9597 | ||
|  | 9078ad81e8 | ||
|  | 7cba6c8a10 | ||
|  | c1f9c8c16d | ||
|  | 1d1d132b33 | ||
|  | e258b3bc2c | ||
|  | 13508ee92f | ||
|  | 4fbf58e707 | ||
|  | a3442f84ca | ||
|  | 6a6ab3b2cb | ||
|  | d9fa553e2f | ||
|  | cde5d9b912 | ||
|  | 3468446b5b | ||
|  | 326434273a | ||
|  | 470d3be946 | ||
|  | d1032be6a6 | ||
|  | cffac8e1f8 | ||
|  | 870bc25dd9 | ||
|  | de713024f6 | ||
|  | 4d4add4581 | ||
|  | 1670c8e505 | ||
|  | 55eb1b5125 | ||
|  | dbeaaaf91e | ||
|  | 8166d8ce6d | ||
|  | 35bcf0c1fa | ||
|  | 9c1d240962 | ||
|  | a76652b552 | ||
|  | a140f993d0 | ||
|  | ded60a2867 | ||
|  | b86602bcdb | ||
|  | 02ea45469f | ||
|  | 90105c3b09 | ||
|  | 3d828914cc | ||
|  | 8cd5776bc6 | ||
|  | 17ec73b176 | ||
|  | e7cce01ca9 | ||
|  | 6c73f5d979 | ||
|  | f59cb5afbf | ||
|  | 5629215229 | ||
|  | 45fb637d48 | ||
|  | 7c473eb1ca | ||
|  | b40880c85a | ||
|  | 30329ea3ba | ||
|  | ca10401bee | ||
|  | 814e0a8b00 | ||
|  | b5fbe7b86f | ||
|  | fc9b6f05c0 | ||
|  | eadae4374b | ||
|  | 711cb04dcf | ||
|  | 1d39b7fc7d | ||
|  | 2fa640433a | ||
|  | 2445dc7869 | ||
|  | e3e114fe94 | ||
|  | 7a1603b423 | ||
|  | 4b831821da | ||
|  | 1ec54953d7 | ||
|  | 61571600d1 | ||
|  | 07a097aa50 | ||
|  | ce1cebaf64 | ||
|  | faf6f73b2a | ||
|  | db3b5d941e | ||
|  | cc60dc2b21 | ||
|  | 8aa0e03187 | ||
|  | 4492a08d6b | ||
|  | 792c0d0c84 | ||
|  | 8221eceb78 | ||
|  | 12ba5598e4 | ||
|  | 536cbf9445 | ||
|  | a87943da27 | ||
|  | 3fddc3b5a7 | ||
|  | 5bc0b0868a | ||
|  | e9ad612fec | ||
|  | c62e045dae | ||
|  | 725ec9291d | ||
|  | 96c8338890 | ||
|  | 0996fb94da | ||
|  | 5de2431a0f | ||
|  | 163338ea75 | ||
|  | f28e5493dc | ||
|  | 01c0a08fa8 | ||
|  | 1c461d2449 | ||
|  | 915e8045a3 | ||
|  | f10fc7eec1 | ||
|  | 320bc677e0 | ||
|  | 46ca1948e2 | ||
|  | 7a0ce93cfd | ||
|  | 3c0bd68b0a | ||
|  | b4ad3061e4 | ||
|  | d6b1fbec24 | ||
|  | cacdd0d304 | ||
|  | e3e1fa3499 | ||
|  | 58842d1ebb | ||
|  | 101569d6ee | ||
|  | 8a180c4c0e | ||
|  | ba343fbd98 | ||
|  | 1d528df341 | ||
|  | 51ea0b0afa | ||
|  | 9dbb697e58 | ||
|  | 947558bb3d | ||
|  | 8ba4cc85d8 | ||
|  | 0f604a6ce6 | ||
|  | 7e48c6535f | ||
|  | 1d2a8b613b | ||
|  | 89e833eb33 | ||
|  | b65a68e0c4 | ||
|  | bfb24b9d11 | ||
|  | 0792cae2b1 | ||
|  | a85fb3d03b | ||
|  | ddb9a9d4e9 | ||
|  | 29ee360ec4 | ||
|  | c0faaafd04 | ||
|  | bc045344a5 | ||
|  | 7d746fd546 | ||
|  | 3ff55f181e | ||
|  | 187e12dd79 | ||
|  | 10daf2d952 | ||
|  | 31c6509d13 | ||
|  | cb74108814 | ||
|  | 9efded2139 | ||
|  | 96b3e7c739 | ||
|  | b029146bf3 | ||
|  | d715aaf5e5 | ||
|  | 0dc12963f0 | ||
|  | 4da3b40d55 | ||
|  | f7d05a57ad | ||
|  | df01599fe0 | ||
|  | 2c3335ebf3 | ||
|  | 05c1427aa8 | ||
|  | 02bfaf7db6 | ||
|  | f488c0810b | ||
|  | 8dbfb91234 | ||
|  | aee99e3925 | ||
|  | 50d3280803 | ||
|  | a90eb5c4db | ||
|  | 16c06a2d48 | ||
|  | 513bf85cae | ||
|  | 82d7aeba02 | ||
|  | 12f7cb86de | ||
|  | b65c885467 | ||
|  | 2a828a1289 | ||
|  | 291f12ba97 | ||
|  | 6afbd37d71 | ||
|  | 0e8869878f | ||
|  | 7c2cfe3215 | ||
|  | c376c0e952 | ||
|  | da5f663396 | ||
|  | 0e92418a33 | ||
|  | 2eef7cfe5e | ||
|  | de4e0bfb3a | ||
|  | 8bf2d31e72 | ||
|  | 2125c46143 | ||
|  | 5402eb84df | ||
|  | ad5aa0898f | ||
|  | 040d40b614 | ||
|  | 8e58f22c56 | ||
|  | c91695fbe5 | ||
|  | c43741da49 | ||
|  | f2563a0397 | ||
|  | fba4017819 | ||
|  | 5f23e108a1 | ||
|  | 68d14bd13d | ||
|  | 022622522f | ||
|  | 89513ca4e5 | ||
|  | a934ee2335 | ||
|  | 49aeea634f | ||
|  | e18b9ebe14 | ||
|  | 08ee3f3d80 | ||
|  | 62d07bf8b9 | ||
|  | ab398cbdc3 | ||
|  | 007d12719c | ||
|  | 524d195800 | ||
|  | 405de64249 | ||
|  | f53554702e | ||
|  | 379e1a4a7e | ||
|  | d6f7096055 | ||
|  | 37c721e4f6 | ||
|  | d94235ef6d | ||
|  | eb4184713f | ||
|  | a0a0cb4612 | ||
|  | f448a20784 | ||
|  | 36eff26862 | ||
|  | 5b2a1163b9 | ||
|  | e627a8b963 | ||
|  | 4432124e8c | ||
|  | b8ba3c59e9 | ||
|  | c40a496b6b | ||
|  | a7c3b46061 | ||
|  | dfbaaeb06b | ||
|  | f6ab20c6e8 | ||
|  | 7625099d74 | ||
|  | 32c8e76855 | ||
|  | 0aa2c974d5 | ||
|  | 9524c8587b | ||
|  | c075db8b1a | ||
|  | d0b7cc1929 | ||
|  | d8df32f140 | ||
|  | 293b5e0242 | ||
|  | 2f517a3ad5 | ||
|  | 56d8e389db | ||
|  | 1377843350 | ||
|  | 8e31eaf8bb | ||
|  | 5ced01463f | ||
|  | a3548455eb | ||
|  | c40fceea4f | ||
|  | 6ad3938a91 | ||
|  | bc642f81ad | ||
|  | 14ce608bbb | ||
|  | c4c67747c5 | ||
|  | 5b3ceecb0e | ||
|  | bf53e4b9df | ||
|  | 7e09d92fdf | ||
|  | 1ba9106d0b | ||
|  | d727a29991 | ||
|  | c5d617477f | ||
|  | 244a1984cc | ||
|  | b00b745f27 | ||
|  | 959ff21b9b | ||
|  | e6a7fd2dfe | ||
|  | 216276e5f3 | ||
|  | 3e6229cf3e | ||
|  | fc4cb80b74 | ||
|  | b907ff1e82 | ||
|  | 7536a52771 | ||
|  | 73a8c111d1 | ||
|  | 86a19eeec2 | ||
|  | fba4459977 | ||
|  | 06f994a827 | ||
|  | 35d8607484 | ||
|  | 2f4c06e9b5 | ||
|  | 92e008a380 | ||
|  | 14c272af92 | ||
|  | 710de9f2b8 | ||
|  | d9ad3b3083 | ||
|  | b2686cb105 | ||
|  | 959e89de2b | ||
|  | 6e448d3458 | ||
|  | 6695756727 | ||
|  | ed732e9b77 | ||
|  | f495a6affc | ||
|  | c8d7e1a95f | ||
|  | e1ca2638e3 | ||
|  | 01226cb9eb | ||
|  | 8a80d0c5d1 | ||
|  | f26f3e87c7 | ||
|  | b750417415 | ||
|  | 2c35dd7c21 | ||
|  | cff4a4feed | ||
|  | 62174b0651 | ||
|  | d3ea4210c1 | ||
|  | 1c782bf64d | ||
|  | bc96dab339 | ||
|  | 0f7179b944 | ||
|  | 1e3bfa8ff7 | ||
|  | 2bce86f905 | ||
|  | 0be00acc3a | ||
|  | 4e61adaeb1 | ||
|  | 49a8f08153 | ||
|  | ce15658462 | ||
|  | 16d73ba7dd | ||
|  | 9f3e3c1917 | ||
|  | f29e382a19 | ||
|  | 073562373a | ||
|  | 4298ebcd66 | ||
|  | a121295bef | ||
|  | 9303e4c0a5 | ||
|  | 831fc98ab1 | ||
|  | 2003005e56 | ||
|  | fda8fb7182 | ||
|  | cf6039b279 | ||
|  | 41e552dce5 | ||
|  | 90043b5806 | ||
|  | 9eb74b5a8d | ||
|  | 9cc60a136b | ||
|  | 78eb1e779c | ||
|  | 8db2d8508e | ||
|  | 3f1ece26ec | ||
|  | d1912a44c6 | ||
|  | 36a05eb390 | ||
|  | 4f39ea1ad8 | ||
|  | a241cc1d61 | ||
|  | 8b4df98cb9 | ||
|  | 7d30c2f9d5 | ||
|  | 44acabadfe | ||
|  | 6f3a2bb78d | ||
|  | dd5f8b155d | ||
|  | cd81fc72fd | ||
|  | 890da650dc | ||
|  | 9897b6a44b | ||
|  | 7969f54d3b | ||
|  | 7c18454de3 | ||
|  | dcf5efddd1 | ||
|  | a6541134e0 | ||
|  | 90504047b4 | ||
|  | ca1eec6602 | ||
|  | edc01d14b7 | ||
|  | 6cb5463b13 | ||
|  | 63a789ebfb | ||
|  | a0994e9a60 | ||
|  | 8d1b728194 | ||
|  | 1a9fec8b98 | ||
|  | e634253282 | ||
|  | 64b23ec7cc | ||
|  | afe207a878 | ||
|  | 4bac0c092f | ||
|  | 74c8ae35a1 | ||
|  | 7856637456 | ||
|  | 965f80a6ca | ||
|  | 198c2ba49a | ||
|  | 4b9ec5ca6e | ||
|  | 5792652619 | ||
|  | 2c900333a5 | ||
|  | 1f782d7cd3 | ||
|  | 89cc1833de | ||
|  | 1262d8c9aa | ||
|  | 85b0c4f814 | ||
|  | 551a8dfa31 | ||
|  | 139533d2ca | ||
|  | 889682f771 | ||
|  | f16c98057f | ||
|  | 26ec807c25 | ||
|  | 45af6cbe3c | ||
|  | 5dd9cde12d | ||
|  | 472fb1d367 | ||
|  | 8b372fbc0b | ||
|  | 40d72eb6e1 | ||
|  | ced008a7c1 | ||
|  | d1f652282a | ||
|  | f656528d5b | ||
|  | bcdb2a648c | ||
|  | 8a78745aa7 | ||
|  | 2a3eaabbe4 | ||
|  | bcd175fbfb | ||
|  | f9f013636d | ||
|  | b34cc97080 | ||
|  | 327f623ef7 | ||
|  | 4d0877e5ae | ||
|  | 0eac217399 | ||
|  | 9c42ad687d | ||
|  | 5cda98da46 | ||
|  | 958f545f65 | ||
|  | 44165993b4 | ||
|  | 283ae6cfd4 | ||
|  | 4068b295bd | ||
|  | e36b33dcec | ||
|  | 4b12912697 | ||
|  | 49a21967cc | ||
|  | cf36406f2a | ||
|  | 872ad044f1 | ||
|  | 345301c03a | ||
|  | 117923413d | ||
|  | 24ccbc58c4 | ||
|  | 89c91b4b01 | ||
|  | 4494da1f4f | ||
|  | c263542c54 | ||
|  | c70f52a73d | ||
|  | 423813d6fb | ||
|  | ec6a86f4b0 | ||
|  | 64cf18cb23 | ||
|  | e0e064bc67 | ||
|  | 5cee6cbd9c | ||
|  | 43659b26f7 | ||
|  | 98e15ad429 | ||
|  | 90728cdf8b | ||
|  | d1ec4f36cc | ||
|  | 079070071e | ||
|  | 520fd6bc38 | ||
|  | 085aead36b | ||
|  | fcbaf298cc | ||
|  | eedc0c9b22 | ||
|  | f70c1e12ff | ||
|  | ec094a4362 | ||
|  | 11646c840e | ||
|  | 86987c57c9 | ||
|  | e4d6e842f5 | ||
|  | cfe4dd1c59 | ||
|  | 3387ab2850 | ||
|  | abd23e27ea | ||
|  | 2f110b20bb | ||
|  | f88e6f9b61 | ||
|  | 2836973dca | ||
|  | a4477e9f83 | ||
|  | 96fa7ece25 | ||
|  | b84caa4cc3 | ||
|  | 49c212632e | ||
|  | 92165aa7ed | ||
|  | cbbdb754aa | ||
|  | 7e3fe0608d | ||
|  | 781f39f281 | ||
|  | bfb80f6f8c | ||
|  | 801b8f9288 | ||
|  | b988fcfcdd | ||
|  | dff6457cb2 | ||
|  | f50f68f318 | ||
|  | c869ad41d9 | ||
|  | cd41f9a236 | ||
|  | 1dbe162bf0 | ||
|  | 1a52203bd7 | ||
|  | 753df3c724 | ||
|  | dc62a08da3 | ||
|  | 0c26aff498 | ||
|  | 6323f8f2e6 | ||
|  | 885c0b1316 | ||
|  | 14958d9165 | ||
|  | bf6a52e0b9 | ||
|  | 72aad5cc16 | ||
|  | 340e8569cc | ||
|  | 8fc7d0b61e | ||
|  | 5dcb27ada7 | ||
|  | db1a076132 | ||
|  | 6707201e23 | ||
|  | b8b92171a8 | ||
|  | 3dd7069292 | ||
|  | 7177419472 | ||
|  | c37313cf07 | ||
|  | a65f42d0fd | ||
|  | 78dd7df686 | ||
|  | 2ea7d9440c | ||
|  | abdcd49368 | ||
|  | 6da7a5ab90 | ||
|  | 20ffe03139 | ||
|  | a71213c589 | ||
|  | d61103ac42 | ||
|  | 298a64b7ae | ||
|  | 9e2c673966 | ||
|  | 092469d668 | ||
|  | bcf3dab0e2 | ||
|  | 7ecfc8a9ff | ||
|  | ecf0a696f7 | ||
|  | dc5db28e01 | ||
|  | 555f305c22 | ||
|  | 76bf07cfcd | ||
|  | c4663576d1 | ||
|  | a64aa73aae | ||
|  | a3a60dd707 | ||
|  | 9c28b0085b | ||
|  | d5baabdd53 | ||
|  | 56a333a852 | ||
|  | c5922368de | ||
|  | 8c2316a51a | ||
|  | e2e6c015de | ||
|  | 0a6ff4586d | ||
|  | fc228d85ae | ||
|  | 61823cb43b | ||
|  | 127e0b8182 | ||
|  | 38c37fa212 | ||
|  | dfaf2a2924 | ||
|  | c90c40c046 | ||
|  | d2049b726a | ||
|  | 6508f109f7 | ||
|  | 37e63637a7 | ||
|  | 6650c5c145 | ||
|  | 9160dbf7f2 | ||
|  | 243fcd7c49 | ||
|  | c114bcfb35 | ||
|  | 83defb08f1 | ||
|  | 57ebdbbe85 | ||
|  | c6aceed623 | ||
|  | ba4c88ec5d | ||
|  | ee1685e981 | ||
|  | 996fbf7bba | ||
|  | 56cd8963d7 | ||
|  | 5759aad0cb | ||
|  | 02717332f7 | ||
|  | 8d1b159f56 | ||
|  | fb335e1100 | ||
|  | 5f0bc83d67 | ||
|  | 6a8cee2cc2 | ||
|  | 0d2f1cf9aa | ||
|  | 8efeb3da8a | ||
|  | 620aa3b8d8 | ||
|  | ab5bf3b807 | ||
|  | 6663bcad72 | ||
|  | 113cd29f74 | ||
|  | f2fdfb0a32 | ||
|  | 691e48a36b | ||
|  | 2036cc117f | ||
|  | 389d28a1e1 | ||
|  | 27e6198d83 | ||
|  | de762a4878 | ||
|  | e8efefe25d | ||
|  | 21f3e8985a | ||
|  | 622543d405 | ||
|  | abdc0fc1c8 | ||
|  | 1ecb839042 | ||
|  | cece4d1e16 | ||
|  | 623634cb6e | ||
|  | f9c37f5084 | ||
|  | 3e12f4f8a4 | ||
|  | b07ff6fe71 | ||
|  | 5a3b57c28e | ||
|  | e858eee83b | ||
|  | 73f00d3bd7 | ||
|  | eea59cf11b | ||
|  | 61b459ed8a | ||
|  | dca8c309aa | ||
|  | be53500104 | ||
|  | bc1a791608 | ||
|  | b112ff980a | ||
|  | 7beab9ae93 | ||
|  | 8c0d1f90a3 | ||
|  | 05c05ba768 | ||
|  | 67e885e76a | ||
|  | 594bce0b8d | ||
|  | 7f6569e0db | ||
|  | 1c829c8364 | ||
|  | 7ca4b02e6d | ||
|  | fadfefd836 | ||
|  | 37155901ef | ||
|  | fbbb96409d | ||
|  | 5126c54914 | ||
|  | 916d0b7e3c | ||
|  | 0815840a9c | ||
|  | bc237796b2 | ||
|  | 7f44800f64 | ||
|  | 85ac746e9d | ||
|  | 8215175098 | ||
|  | 39ee8b1799 | ||
|  | c76d3d68c8 | ||
|  | cde257922b | ||
|  | be0c9d3372 | ||
|  | 66cd7ea307 | ||
|  | b704ce6984 | ||
|  | 247c856a41 | ||
|  | 9afaebfa12 | ||
|  | 929abea5d3 | ||
|  | 13102a6b04 | ||
|  | 57c3083f9f | ||
|  | 5c31ddd00f | ||
|  | 8f55be187d | ||
|  | 1fe82d8b0d | ||
|  | cbc56a8105 | ||
|  | b63cddfa46 | ||
|  | 91db82f730 | ||
|  | 0c4d1b78ff | ||
|  | 5af2fd0562 | ||
|  | 2375543ebf | ||
|  | de187f3ed5 | ||
|  | 9266ffacf3 | ||
|  | 3c0ca5d16d | ||
|  | caabf25260 | ||
|  | 0af2afbb80 | ||
|  | 12d226509d | ||
|  | 3417c38426 | ||
|  | c7fc5afbb8 | ||
|  | 11f565a9dc | ||
|  | 53240faac3 | ||
|  | 95d4878785 | ||
|  | ef15026203 | ||
|  | ad6355503b | ||
|  | 491c2b0dc0 | ||
|  | 5b99ade088 | ||
|  | e1d9d9f304 | ||
|  | 209ccd4f7f | ||
|  | 5a8a207f2e | ||
|  | 19c85d9c16 | ||
|  | a916ddfa50 | ||
|  | 8c1ad9c7f9 | ||
|  | 93af1eca7e | ||
|  | cabf836fa3 | ||
|  | 15b3d31a6f | ||
|  | 9b98689012 | ||
|  | 84ebd0c33c | ||
|  | ccd7774931 | ||
|  | b2773635f5 | ||
|  | 8b046b7313 | ||
|  | 885a516676 | ||
|  | 921b0e09b0 | ||
|  | a7cda2a35e | ||
|  | 102b10ade0 | ||
|  | 4e96b9adbb | ||
|  | b9581d3762 | ||
|  | 7c010359c3 | ||
|  | 4a75243994 | ||
|  | d29d7e5b3b | ||
|  | 5ebd25e0d1 | ||
|  | b7d5a53e86 | ||
|  | 20d3498bfd | ||
|  | 67d7bb45f5 | ||
|  | 6a03105d01 | ||
|  | 5ae580ecf1 | ||
|  | 0efef33e53 | ||
|  | ccb88884a7 | ||
|  | d70ba0a55a | ||
|  | 5140840d3a | ||
|  | 14759fd3c9 | ||
|  | fed35be517 | 
							
								
								
									
										22
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal 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] | ||||
							
								
								
									
										12
									
								
								.github/ISSUE_TEMPLATE/entity-support-request.md
									
									
									
									
										vendored
									
									
										Normal 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:** | ||||
							
								
								
									
										17
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal 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. | ||||
							
								
								
									
										12
									
								
								.github/ISSUE_TEMPLATE/lovelace-card-support-request.md
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| # Configuration for probot-no-response - https://github.com/probot/no-response | ||||
|  | ||||
| # Number of days of inactivity before an Issue is closed for lack of response | ||||
| daysUntilClose: 14 | ||||
| # Label requiring a response | ||||
| responseRequiredLabel: more info needed | ||||
| # Comment to post when closing an Issue for lack of response. Set to `false` to disable | ||||
| closeComment: > | ||||
|   This issue has been automatically closed because there has been no response | ||||
|   to our request for more information from the original author. If the issue still relevant | ||||
|   feel free to reopen this issue and add more information, or report a new one.  | ||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -9,5 +9,15 @@ build/ | ||||
| .flutter-plugins | ||||
|  | ||||
| .idea/ | ||||
| .vscode/ | ||||
| .theia/ | ||||
| .project/ | ||||
| .settings/ | ||||
|  | ||||
| flutter_export_environment.sh | ||||
| .flutter-plugins-dependencies | ||||
|  | ||||
| key.properties | ||||
| .secrets.dart | ||||
| pubspec.lock | ||||
| google-services.json | ||||
|   | ||||
							
								
								
									
										8
									
								
								.gitpod.dockerfile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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== | ||||
							
								
								
									
										
											BIN
										
									
								
								.gradle/6.0.1/fileChanges/last-build.bin
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								.gradle/6.0.1/fileHashes/fileHashes.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										0
									
								
								.gradle/6.0.1/gc.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										18
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,12 +1,18 @@ | ||||
| [](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart) | ||||
| # HA Client | ||||
| ## Native Android client for Home Assistant | ||||
| ### With Lovelace UI support | ||||
| ### With actionable notifications, location tracking 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/store/apps/details?id=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/u9vq7QE) 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) | ||||
| [](https://gitpod.io/#https://github.com/estevez-dev/ha_client)  | ||||
|  | ||||
| #### Last release build status | ||||
| [](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) | ||||
|  | ||||
| #### Projects used | ||||
| - [HANotify](https://github.com/Crewski/HANotify) by [Crewski](https://github.com/Crewski) | ||||
| - [hassalarm](https://github.com/Johboh/hassalarm) by [Johboh](https://github.com/Johboh) distributed under [MIT License](https://github.com/Johboh/hassalarm/blob/master/LICENSE) | ||||
|   | ||||
							
								
								
									
										1
									
								
								android/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -8,3 +8,4 @@ | ||||
| /build | ||||
| /captures | ||||
| GeneratedPluginRegistrant.java | ||||
| .project/ | ||||
							
								
								
									
										17
									
								
								android/.project
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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> | ||||
| @@ -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,7 +78,13 @@ flutter { | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation 'com.google.firebase:firebase-analytics:17.2.2' | ||||
|     implementation 'com.google.firebase:firebase-messaging:20.2.0' | ||||
|     implementation 'androidx.work:work-runtime:2.3.4' | ||||
|     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' | ||||
|   | ||||
| @@ -1,11 +1,15 @@ | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     package="com.keyboardcrumbs.hassclient"> | ||||
|     package="com.keyboardcrumbs.hassclient" | ||||
|     android:installLocation="auto"> | ||||
|  | ||||
|     <!-- 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"/> | ||||
|  | ||||
|  | ||||
|     <!-- io.flutter.app.FlutterApplication is an android.app.Application that | ||||
|          calls FlutterMain.startInitialization(this); in its onCreate method. | ||||
| @@ -13,9 +17,19 @@ | ||||
|          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:icon="@mipmap/ic_launcher" | ||||
|         android:roundIcon="@mipmap/ic_launcher_round" | ||||
|         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" | ||||
| @@ -23,17 +37,50 @@ | ||||
|             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="android.intent.action.MAIN"/> | ||||
|                 <category android:name="android.intent.category.LAUNCHER"/> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <service | ||||
|             android:name=".MessagingService" | ||||
|             android:exported="false"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="com.google.firebase.MESSAGING_EVENT" /> | ||||
|             </intent-filter> | ||||
|         </service> | ||||
|         <receiver android:name=".NotificationActionReceiver"  android:exported="true"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.BOOT_COMPLETED"/> | ||||
|                 <action android:name="android.intent.action.INPUT_METHOD_CHANGED" /> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|         <receiver android:name=".NextAlarmBroadcastReceiver"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.BOOT_COMPLETED" /> | ||||
|                 <action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" /> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|         <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"/> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|     </application> | ||||
| </manifest> | ||||
|   | ||||
| @@ -1,13 +1,71 @@ | ||||
| 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; | ||||
|  | ||||
| import android.content.Context; | ||||
|  | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.SharedPreferences.Editor; | ||||
| import android.os.Bundle; | ||||
|  | ||||
| import io.flutter.plugin.common.MethodCall; | ||||
| import io.flutter.plugin.common.MethodChannel; | ||||
|  | ||||
| import com.google.android.gms.tasks.OnCompleteListener; | ||||
| import com.google.android.gms.tasks.Task; | ||||
| import com.google.android.gms.common.GoogleApiAvailability; | ||||
| import com.google.android.gms.common.ConnectionResult; | ||||
| import com.google.firebase.iid.FirebaseInstanceId; | ||||
| import com.google.firebase.iid.InstanceIdResult; | ||||
| import com.google.firebase.messaging.FirebaseMessaging; | ||||
|  | ||||
| public class MainActivity extends FlutterActivity { | ||||
|  | ||||
|     private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native"; | ||||
|    | ||||
|     @Override | ||||
|     public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { | ||||
|         GeneratedPluginRegistrant.registerWith(flutterEngine); | ||||
|         new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler( | ||||
|             new MethodChannel.MethodCallHandler() { | ||||
|                 @Override | ||||
|                 public void onMethodCall(MethodCall call, MethodChannel.Result result) { | ||||
|                     Context context = getActivity(); | ||||
|                     if (call.method.equals("getFCMToken")) { | ||||
|                         if (checkPlayServices()) { | ||||
|                             FirebaseInstanceId.getInstance().getInstanceId() | ||||
|                             .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() { | ||||
|                                 @Override | ||||
|                                 public void onComplete(@NonNull Task<InstanceIdResult> task) { | ||||
|                                     if (task.isSuccessful()) { | ||||
|                                         String token = task.getResult().getToken(); | ||||
|                                         UpdateTokenTask updateTokenTask = new UpdateTokenTask(context); | ||||
|                                         updateTokenTask.execute(token); | ||||
|                                         result.success(token); | ||||
|                                     } else { | ||||
|                                         result.error("fcm_error", task.getException().getMessage(), null); | ||||
|                                     } | ||||
|                                 } | ||||
|                             }); | ||||
|                         } else { | ||||
|                             result.error("google_play_service_error", "Google Play Services unavailable", null); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private boolean checkPlayServices() { | ||||
|         return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|     GeneratedPluginRegistrant.registerWith(this); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,152 @@ | ||||
| package com.keyboardcrumbs.hassclient; | ||||
|  | ||||
| import java.util.Map; | ||||
| import java.net.URL; | ||||
| import java.net.URLConnection; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
|  | ||||
| import android.app.NotificationChannel; | ||||
| import android.app.NotificationManager; | ||||
| import android.app.PendingIntent; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.media.RingtoneManager; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import androidx.core.app.NotificationCompat; | ||||
|  | ||||
| import com.google.firebase.messaging.FirebaseMessagingService; | ||||
| import com.google.firebase.messaging.RemoteMessage; | ||||
|  | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.webkit.URLUtil; | ||||
|  | ||||
|  | ||||
| public class MessagingService extends FirebaseMessagingService { | ||||
|  | ||||
|     private static final String TAG = "MessagingService"; | ||||
|  | ||||
|     @Override | ||||
|     public void onMessageReceived(RemoteMessage remoteMessage) { | ||||
|         Map<String, String> data = remoteMessage.getData(); | ||||
|         if (data.size() > 0) { | ||||
|            if (data.containsKey("body") || data.containsKey("title")) { | ||||
|                 sendNotification(data); | ||||
|            } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onNewToken(String token) { | ||||
|         UpdateTokenTask updateTokenTask = new UpdateTokenTask(this); | ||||
|         updateTokenTask.execute(token); | ||||
|     } | ||||
|  | ||||
|     private void sendNotification(Map<String, String> data) { | ||||
|         String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription; | ||||
|         boolean autoCancel; | ||||
|         if (!data.containsKey("channelId")) { | ||||
|             channelId = "ha_notify"; | ||||
|             channelDescription = "Default notification channel"; | ||||
|         } else { | ||||
|             channelId = data.get("channelId"); | ||||
|             channelDescription = channelId; | ||||
|         } | ||||
|         if (!data.containsKey("body")) { | ||||
|             messageBody = ""; | ||||
|         } else { | ||||
|             messageBody = data.get("body"); | ||||
|         } | ||||
|         if (!data.containsKey("title")) { | ||||
|             messageTitle = "HA Client"; | ||||
|         } else { | ||||
|             messageTitle = data.get("title"); | ||||
|         } | ||||
|         if (!data.containsKey("tag")) { | ||||
|             nTag = String.valueOf(System.currentTimeMillis()); | ||||
|         } else { | ||||
|             nTag = data.get("tag"); | ||||
|         } | ||||
|         if (data.containsKey("dismiss")) { | ||||
|             try { | ||||
|                 boolean dismiss = Boolean.parseBoolean(data.get("dismiss")); | ||||
|                 if (dismiss) { | ||||
|                     NotificationManager notificationManager = | ||||
|                         (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|                     notificationManager.cancel(nTag, 0); | ||||
|                     return; | ||||
|                 } | ||||
|             } catch (Exception e) { | ||||
|                 //nope | ||||
|             } | ||||
|         } | ||||
|         if (data.containsKey("autoDismiss")) { | ||||
|             try { | ||||
|                 autoCancel = Boolean.parseBoolean(data.get("autoDismiss")); | ||||
|             } catch (Exception e) { | ||||
|                 autoCancel = true; | ||||
|             } | ||||
|         } else { | ||||
|             autoCancel = true; | ||||
|         } | ||||
|         imageUrl = data.get("image"); | ||||
|         Intent intent = new Intent(this, MainActivity.class); | ||||
|         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); | ||||
|         PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, | ||||
|                 PendingIntent.FLAG_ONE_SHOT); | ||||
|         Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); | ||||
|         NotificationCompat.Builder notificationBuilder = | ||||
|                 new NotificationCompat.Builder(this, channelId) | ||||
|                         .setSmallIcon(R.drawable.mini_icon) | ||||
|                         .setContentTitle(messageTitle) | ||||
|                         .setContentText(messageBody) | ||||
|                         .setAutoCancel(autoCancel) | ||||
|                         .setSound(defaultSoundUri) | ||||
|                         .setContentIntent(pendingIntent); | ||||
|         if (URLUtil.isValidUrl(imageUrl)) { | ||||
|             Bitmap image = getBitmapFromURL(imageUrl); | ||||
|             if (image != null) { | ||||
|                 notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon))); | ||||
|                 notificationBuilder.setLargeIcon(image); | ||||
|             } | ||||
|         } | ||||
|         for (int i = 1; i <= 3; i++) { | ||||
|             if (data.containsKey("action" + i)) { | ||||
|                 Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class); | ||||
|                 if (autoCancel) { | ||||
|                     broadcastIntent.putExtra("tag", nTag); | ||||
|                 } | ||||
|                 broadcastIntent.putExtra("actionData", data.get("action" + i + "_data")); | ||||
|                 PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT); | ||||
|                 notificationBuilder.addAction(R.drawable.mini_icon, data.get("action" + i), actionIntent); | ||||
|             }    | ||||
|         } | ||||
|         NotificationManager notificationManager = | ||||
|                 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|  | ||||
|         // Since android Oreo notification channel is needed. | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             NotificationChannel channel = new NotificationChannel(channelId, | ||||
|                     channelDescription, | ||||
|                     NotificationManager.IMPORTANCE_HIGH); | ||||
|             notificationManager.createNotificationChannel(channel); | ||||
|         } | ||||
|  | ||||
|         notificationManager.notify(nTag, 0 /* ID of notification */, notificationBuilder.build()); | ||||
|     } | ||||
|  | ||||
|     private Bitmap getBitmapFromURL(String imageUrl) { | ||||
|         try { | ||||
|             URL url = new URL(imageUrl); | ||||
|             URLConnection connection = url.openConnection(); | ||||
|             connection.setDoInput(true); | ||||
|             connection.connect(); | ||||
|             InputStream input = connection.getInputStream(); | ||||
|             return BitmapFactory.decodeStream(input); | ||||
|         } catch (Exception e) { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,51 @@ | ||||
| package com.keyboardcrumbs.hassclient; | ||||
|  | ||||
| import android.app.AlarmManager; | ||||
| import android.content.BroadcastReceiver; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
|  | ||||
| import androidx.work.BackoffPolicy; | ||||
| import androidx.work.Constraints; | ||||
| import androidx.work.ExistingWorkPolicy; | ||||
| import androidx.work.NetworkType; | ||||
| import androidx.work.OneTimeWorkRequest; | ||||
| import androidx.work.WorkManager; | ||||
| import androidx.work.WorkRequest; | ||||
|  | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | ||||
|  | ||||
| public class NextAlarmBroadcastReceiver extends BroadcastReceiver { | ||||
|  | ||||
|     private static final String TAG = "NextAlarmReceiver"; | ||||
|  | ||||
|     @Override | ||||
|     public void onReceive(Context context, Intent intent) { | ||||
|         if (intent == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final boolean isBootIntent = Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction()); | ||||
|         final boolean isNextAlarmIntent = AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equalsIgnoreCase(intent.getAction()); | ||||
|         if (!isBootIntent && !isNextAlarmIntent) { | ||||
|             return; | ||||
|         } | ||||
|         Constraints constraints = new Constraints.Builder() | ||||
|                 .setRequiredNetworkType(NetworkType.CONNECTED) | ||||
|                 .build(); | ||||
|  | ||||
|         OneTimeWorkRequest uploadWorkRequest = | ||||
|                 new OneTimeWorkRequest.Builder(UpdateNextAlarmWorker.class) | ||||
|                         .setBackoffCriteria( | ||||
|                                 BackoffPolicy.EXPONENTIAL, | ||||
|                                 10, | ||||
|                                 TimeUnit.SECONDS) | ||||
|                         .setConstraints(constraints) | ||||
|                         .build(); | ||||
|  | ||||
|         WorkManager | ||||
|                 .getInstance(context) | ||||
|                 .enqueueUniqueWork("NextAlarmUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,73 @@ | ||||
| package com.keyboardcrumbs.hassclient; | ||||
|  | ||||
| import android.app.AlarmManager; | ||||
| import android.content.Context; | ||||
| import android.util.Log; | ||||
| import android.content.BroadcastReceiver; | ||||
| import android.content.Intent; | ||||
|  | ||||
| import android.app.NotificationManager; | ||||
|  | ||||
| import android.webkit.URLUtil; | ||||
|  | ||||
| import org.json.JSONObject; | ||||
| import android.content.SharedPreferences; | ||||
|  | ||||
| public class NotificationActionReceiver extends BroadcastReceiver { | ||||
|  | ||||
|     private static final String TAG = "NotificationAction"; | ||||
|  | ||||
|     @Override | ||||
|     public void onReceive(Context context, Intent intent) { | ||||
|         if (intent == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         String rawActionData = intent.getStringExtra("actionData"); | ||||
|         if (intent.hasExtra("tag")) { | ||||
|             String notificationTag = intent.getStringExtra("tag"); | ||||
|             NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|             notificationManager.cancel(notificationTag, 0); | ||||
|         } | ||||
|         SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE); | ||||
|         String webhookId = prefs.getString("flutter.app-webhook-id", null); | ||||
|         if (webhookId != null) { | ||||
|             try { | ||||
|                 String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") + | ||||
|                     "://" + | ||||
|                     prefs.getString("flutter.hassio-domain", "") + | ||||
|                     ":" + | ||||
|                     prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId; | ||||
|                 JSONObject actionData = new JSONObject(rawActionData); | ||||
|                 if (URLUtil.isValidUrl(requestUrl)) { | ||||
|                     JSONObject dataToSend = new JSONObject(); | ||||
|                     JSONObject requestData = new JSONObject(); | ||||
|                     if (actionData.getString("action").equals("call-service")) { | ||||
|                         dataToSend.put("type", "call_service"); | ||||
|                         requestData.put("domain", actionData.getString("service").split("\\.")[0]); | ||||
|                         requestData.put("service", actionData.getString("service").split("\\.")[1]); | ||||
|                         if (actionData.has("service_data")) { | ||||
|                             requestData.put("service_data", actionData.get("service_data")); | ||||
|                         } | ||||
|                     } else { | ||||
|                         dataToSend.put("type", "fire_event"); | ||||
|                         requestData.put("event_type", "ha_client_event"); | ||||
|                         JSONObject eventData = new JSONObject(); | ||||
|                         eventData.put("action", actionData.getString("action")); | ||||
|                         requestData.put("event_data", eventData); | ||||
|                     } | ||||
|                     dataToSend.put("data", requestData); | ||||
|                     String stringRequest = dataToSend.toString(); | ||||
|                     SendTask sendTask = new SendTask(); | ||||
|                     sendTask.execute(requestUrl, stringRequest); | ||||
|                 } else { | ||||
|                     Log.w(TAG, "Invalid HA url"); | ||||
|                 } | ||||
|             } catch (Exception e) { | ||||
|                 Log.e(TAG, "Error handling notification action", e);     | ||||
|             } | ||||
|         } else { | ||||
|             Log.w(TAG, "Webhook id not found"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,46 @@ | ||||
| package com.keyboardcrumbs.hassclient; | ||||
|  | ||||
| import android.util.Log; | ||||
| import android.os.AsyncTask; | ||||
|  | ||||
| import java.net.URL; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.io.OutputStream; | ||||
|  | ||||
| public class SendTask extends AsyncTask<String, String, String> { | ||||
|  | ||||
|     private static final String TAG = "SendTask"; | ||||
|  | ||||
|     public SendTask(){ | ||||
|         //set context variables if required | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onPreExecute() { | ||||
|         super.onPreExecute(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String doInBackground(String... params) { | ||||
|         String urlString = params[0]; | ||||
|         String data = params[1]; | ||||
|  | ||||
|         try { | ||||
|             URL url = new URL(urlString); | ||||
|             HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); | ||||
|             urlConnection.setRequestMethod("POST"); | ||||
|             urlConnection.setRequestProperty("Content-Type", "application/json"); | ||||
|             urlConnection.setDoOutput(true); | ||||
|             byte[] outputBytes = data.getBytes("UTF-8"); | ||||
|             OutputStream os = urlConnection.getOutputStream(); | ||||
|             os.write(outputBytes); | ||||
|  | ||||
|             int responseCode = urlConnection.getResponseCode(); | ||||
|  | ||||
|             urlConnection.disconnect(); | ||||
|         } catch (Exception e) { | ||||
|             Log.e(TAG, "Error sending data", e);  | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,119 @@ | ||||
| package com.keyboardcrumbs.hassclient; | ||||
|  | ||||
| import android.app.AlarmManager; | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.util.Log; | ||||
| import android.webkit.URLUtil; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.work.Worker; | ||||
| import androidx.work.WorkerParameters; | ||||
|  | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONObject; | ||||
|  | ||||
| import java.io.OutputStream; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| import java.text.SimpleDateFormat; | ||||
| import java.util.Calendar; | ||||
| import java.util.Locale; | ||||
|  | ||||
| public class UpdateNextAlarmWorker extends Worker { | ||||
|  | ||||
|     private Context currentContext; | ||||
|     private static final String TAG = "NextAlarmWorker"; | ||||
|     private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH); | ||||
|     private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); | ||||
|     private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH); | ||||
|  | ||||
|     public UpdateNextAlarmWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { | ||||
|         super(context, workerParams); | ||||
|         currentContext = context; | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Result doWork() { | ||||
|         final AlarmManager alarmManager; | ||||
|         if (android.os.Build.VERSION.SDK_INT >= 23) { | ||||
|             alarmManager = currentContext.getSystemService(AlarmManager.class); | ||||
|         } else { | ||||
|             alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE); | ||||
|         } | ||||
|  | ||||
|         final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock(); | ||||
|  | ||||
|         SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE); | ||||
|         String webhookId = prefs.getString("flutter.app-webhook-id", null); | ||||
|         if (webhookId != null) { | ||||
|             try { | ||||
|                 String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") + | ||||
|                         "://" + | ||||
|                         prefs.getString("flutter.hassio-domain", "") + | ||||
|                         ":" + | ||||
|                         prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId; | ||||
|                 JSONObject dataToSend = new JSONObject(); | ||||
|                 if (URLUtil.isValidUrl(requestUrl)) { | ||||
|                     dataToSend.put("type", "update_sensor_states"); | ||||
|                     JSONArray dataArray = new JSONArray(); | ||||
|                     JSONObject sensorData = new JSONObject(); | ||||
|                     JSONObject sensorAttrs = new JSONObject(); | ||||
|                     sensorData.put("unique_id", "next_alarm"); | ||||
|                     sensorData.put("type", "sensor"); | ||||
|                     final long triggerTimestamp; | ||||
|                     if (alarmClockInfo != null) { | ||||
|                         triggerTimestamp = alarmClockInfo.getTriggerTime(); | ||||
|                         final Calendar calendar = Calendar.getInstance(); | ||||
|                         calendar.setTimeInMillis(triggerTimestamp); | ||||
|                         sensorData.put("state", DATE_TIME_FORMAT.format(calendar.getTime())); | ||||
|                         sensorAttrs.put("date", DATE_FORMAT.format(calendar.getTime())); | ||||
|                         sensorAttrs.put("time", TIME_FORMAT.format(calendar.getTime())); | ||||
|                         sensorAttrs.put("timestamp", triggerTimestamp); | ||||
|                     } else { | ||||
|                         sensorData.put("state", ""); | ||||
|                         sensorAttrs.put("date", ""); | ||||
|                         sensorAttrs.put("time", ""); | ||||
|                         sensorAttrs.put("timestamp", 0); | ||||
|                     } | ||||
|                     sensorData.put("icon", "mdi:alarm"); | ||||
|                     sensorData.put("attributes", sensorAttrs); | ||||
|                     dataArray.put(0, sensorData); | ||||
|                     dataToSend.put("data", dataArray); | ||||
|  | ||||
|                     String stringRequest = dataToSend.toString(); | ||||
|                     try { | ||||
|                         URL url = new URL(requestUrl); | ||||
|                         HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); | ||||
|                         urlConnection.setRequestMethod("POST"); | ||||
|                         urlConnection.setRequestProperty("Content-Type", "application/json"); | ||||
|                         urlConnection.setDoOutput(true); | ||||
|                         byte[] outputBytes = stringRequest.getBytes("UTF-8"); | ||||
|                         OutputStream os = urlConnection.getOutputStream(); | ||||
|                         os.write(outputBytes); | ||||
|  | ||||
|                         int responseCode = urlConnection.getResponseCode(); | ||||
|                         urlConnection.disconnect(); | ||||
|                         if (responseCode >= 300) { | ||||
|                             return Result.retry(); | ||||
|                         } | ||||
|                     } catch (Exception e) { | ||||
|                         Log.e(TAG, "Error sending data", e); | ||||
|                         return Result.retry(); | ||||
|                     } | ||||
|                 } else { | ||||
|                     Log.w(TAG, "Invalid HA url"); | ||||
|                     return Result.failure(); | ||||
|                 } | ||||
|             } catch (Exception e) { | ||||
|                 Log.e(TAG, "Error setting next alarm", e); | ||||
|                 return Result.failure(); | ||||
|             } | ||||
|         } else { | ||||
|             Log.w(TAG, "Webhook id not found"); | ||||
|             return Result.failure(); | ||||
|         } | ||||
|         return Result.success(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,46 @@ | ||||
| package com.keyboardcrumbs.hassclient; | ||||
|  | ||||
| import android.util.Log; | ||||
| import android.os.AsyncTask; | ||||
|  | ||||
| import java.net.URL; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.io.OutputStream; | ||||
|  | ||||
| import android.webkit.URLUtil; | ||||
|  | ||||
| import org.json.JSONObject; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.Context; | ||||
| import java.lang.ref.WeakReference; | ||||
|  | ||||
|  | ||||
| public class UpdateTokenTask extends AsyncTask<String, String, String> { | ||||
|  | ||||
|     private static final String TAG = "UpdateTokenTask"; | ||||
|  | ||||
|     private WeakReference<Context> contextRef; | ||||
|  | ||||
|     public UpdateTokenTask(Context context){ | ||||
|         contextRef = new WeakReference<>(context); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onPreExecute() { | ||||
|         super.onPreExecute(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String doInBackground(String... params) { | ||||
|         Log.d(TAG, "Updating push token"); | ||||
|         Context context = contextRef.get(); | ||||
|         if (context != null) { | ||||
|             String token = params[0]; | ||||
|             SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE); | ||||
|             SharedPreferences.Editor editor = prefs.edit(); | ||||
|             editor.putString("flutter.npush-token", token); | ||||
|             editor.commit(); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/blank_icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 461 B | 
							
								
								
									
										27
									
								
								android/app/src/main/res/drawable/ic_launcher_foreground.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportWidth="108" | ||||
|     android:viewportHeight="108"> | ||||
|  | ||||
|     <group> | ||||
|         <clip-path | ||||
|             android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" /> | ||||
|         <path | ||||
|             android:fillColor="#709ac1" | ||||
|             android:fillAlpha="0" | ||||
|             android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" /> | ||||
|         <path | ||||
|             android:fillColor="#000000" | ||||
|             android:fillAlpha="0.12" | ||||
|             android:pathData="M 70.506 38.389 L 108 72.466 L 108 108 L 77 108 L 35.066 72.466 L 38.373 63.769 L 36.268 50.216 L 43.335 44.523 L 51.841 34.578 L 63.096 42.478 L 68.586 42.478 L 70.506 38.389 Z" /> | ||||
|         <path | ||||
|             android:fillColor="#000000" | ||||
|             android:fillAlpha="0.12" | ||||
|             android:pathData="M 28.979 53.708 L 47.736 67.31 L 38.373 58.563 L 36.268 51.52 L 28.979 53.708 Z" /> | ||||
|         <path | ||||
|             android:fillColor="#ffffff" | ||||
|             android:pathData="M 77.131 54.24 L 72.878 54.24 L 72.878 72.415 L 56.339 72.415 L 56.339 64.85 L 62.931 58.511 L 64.609 58.784 C 67.349 58.784 69.57 56.649 69.57 54.013 C 69.57 51.378 67.349 49.242 64.609 49.242 C 61.868 49.242 59.647 51.378 59.647 54.013 L 59.883 55.626 L 56.339 59.079 L 56.339 46.63 C 57.898 45.812 58.938 44.244 58.938 42.427 C 58.938 39.792 56.717 37.656 53.976 37.656 C 51.236 37.656 49.015 39.792 49.015 42.427 C 49.015 44.244 50.054 45.812 51.614 46.63 L 51.614 59.079 L 48.07 55.626 L 48.306 54.013 C 48.306 51.378 46.084 49.242 43.344 49.242 C 40.604 49.242 38.383 51.378 38.383 54.013 C 38.383 56.648 40.604 58.784 43.344 58.784 L 45.022 58.511 L 51.614 64.85 L 51.614 72.415 L 35.075 72.415 L 35.075 54.24 L 30.94 54.24 C 29.948 54.24 28.979 54.24 28.979 53.763 C 29.003 53.263 29.995 52.309 31.011 51.332 L 51.614 31.522 C 52.393 30.772 53.197 30 53.976 30 C 54.756 30 55.559 30.772 56.339 31.522 L 65.79 40.609 L 65.79 38.338 L 70.515 38.338 L 70.515 45.153 L 77.084 51.469 C 78.029 52.377 78.997 53.309 79.021 53.786 C 79.021 54.24 78.076 54.24 77.131 54.24 Z M 43.344 51.969 C 43.908 51.969 44.449 52.184 44.848 52.567 C 45.247 52.951 45.471 53.471 45.471 54.013 C 45.471 54.555 45.247 55.076 44.848 55.459 C 44.449 55.842 43.908 56.058 43.344 56.058 C 42.78 56.058 42.239 55.842 41.841 55.459 C 41.442 55.076 41.218 54.555 41.218 54.013 C 41.218 53.471 41.442 52.951 41.841 52.567 C 42.239 52.184 42.78 51.969 43.344 51.969 Z M 64.609 51.969 C 65.79 51.969 66.735 52.877 66.735 54.013 C 66.735 55.149 65.79 56.058 64.609 56.058 C 64.045 56.058 63.504 55.842 63.105 55.459 C 62.706 55.076 62.482 54.555 62.482 54.013 C 62.482 53.471 62.706 52.951 63.105 52.567 C 63.504 52.184 64.045 51.969 64.609 51.969 Z M 53.976 40.382 C 55.158 40.382 56.103 41.291 56.103 42.427 C 56.103 43.563 55.158 44.472 53.976 44.472 C 52.795 44.472 51.85 43.563 51.85 42.427 C 51.85 41.291 52.795 40.382 53.976 40.382 Z" /> | ||||
|     </group> | ||||
| </vector> | ||||
| @@ -1,7 +1,7 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Modify this file to customize your launch splash screen --> | ||||
| <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <item android:drawable="@android:color/white" /> | ||||
|     <item android:drawable="@color/main_color" /> | ||||
|  | ||||
|     <!-- You can insert your own image assets here --> | ||||
|     <!-- <item> | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/mini_icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 612 B | 
| @@ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@color/main_color"/> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground" /> | ||||
| </adaptive-icon> | ||||
| @@ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@color/main_color"/> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground" /> | ||||
| </adaptive-icon> | ||||
| Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.1 KiB | 
| Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.3 KiB | 
| Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.0 KiB | 
| Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 9.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
| @@ -1,8 +1,12 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <color name="main_color">#709AC1</color> | ||||
|     <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> | ||||
|         <!-- Show a splash screen on the activity. Automatically removed when | ||||
|              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> | ||||
|   | ||||
| @@ -2,10 +2,15 @@ buildscript { | ||||
|     repositories { | ||||
|         google() | ||||
|         jcenter() | ||||
|         maven { | ||||
|             url 'https://maven.fabric.io/public' | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:3.1.2' | ||||
|         classpath 'com.android.tools.build:gradle:3.3.2' | ||||
|         classpath 'com.google.gms:google-services:4.3.3' | ||||
|         classpath 'io.fabric.tools:gradle:1.26.1' | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -13,6 +18,9 @@ allprojects { | ||||
|     repositories { | ||||
|         google() | ||||
|         jcenter() | ||||
|         maven { | ||||
|             url 'https://maven.fabric.io/public' | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| org.gradle.jvmargs=-Xmx2g | ||||
| org.gradle.daemon=true | ||||
| org.gradle.caching=true | ||||
| org.gradle.jvmargs=-Xmx512m | ||||
| android.useAndroidX=true | ||||
| android.enableJetifier=false | ||||
| android.enableJetifier=true | ||||
| android.enableR8=true | ||||
|   | ||||
| @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip | ||||
|   | ||||
							
								
								
									
										1
									
								
								android/settings_aar.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| include ':app' | ||||
							
								
								
									
										61
									
								
								assets/html/cameraLiveView.html
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										28
									
								
								assets/html/cameraView.html
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										37
									
								
								assets/js/externalAuth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,37 @@ | ||||
| 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); | ||||
|         }, 900); | ||||
|     } | ||||
| }; | ||||
| /* | ||||
| 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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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! | ||||
| ``` | ||||
							
								
								
									
										
											BIN
										
									
								
								fonts/materialdesignicons-webfont-4.5.95.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| Before Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 24 KiB | 
							
								
								
									
										62
									
								
								lib/cards/alarm_panel_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,62 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class AlarmPanelCard extends StatelessWidget { | ||||
|   final AlarmPanelCardData card; | ||||
|  | ||||
|   const AlarmPanelCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (card.entity.entity.statelessType == StatelessEntityType.missed) { | ||||
|       return EntityModel( | ||||
|         entityWrapper: card.entity, | ||||
|         child: MissedEntityWidget(), | ||||
|         handleTap: false, | ||||
|       ); | ||||
|     } | ||||
|     List<Widget> body = []; | ||||
|     body.add(CardHeader( | ||||
|       name: card.name ?? "", | ||||
|       subtitle: Text("${card.entity.entity.displayState}", | ||||
|     ), | ||||
|     trailing: Row( | ||||
|       mainAxisSize: MainAxisSize.min, | ||||
|       mainAxisAlignment: MainAxisAlignment.end, | ||||
|       children: [ | ||||
|         EntityIcon( | ||||
|           size: 50.0, | ||||
|         ), | ||||
|         Container( | ||||
|             width: 26.0, | ||||
|             child: IconButton( | ||||
|                 padding: EdgeInsets.all(0.0), | ||||
|                 alignment: Alignment.centerRight, | ||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||
|                     "mdi:dots-vertical")), | ||||
|                 onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: card.entity.entity.entityId)) | ||||
|             ) | ||||
|         ) | ||||
|       ] | ||||
|     ), | ||||
|     )); | ||||
|     body.add( | ||||
|         AlarmControlPanelControlsWidget( | ||||
|           extended: true, | ||||
|           states: card.states, | ||||
|         ) | ||||
|     ); | ||||
|     return CardWrapper( | ||||
|         child: EntityModel( | ||||
|             entityWrapper: card.entity, | ||||
|             handleTap: null, | ||||
|             child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 children: body | ||||
|             ) | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|    | ||||
| } | ||||
							
								
								
									
										196
									
								
								lib/cards/badges.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,196 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class Badges extends StatelessWidget { | ||||
|   final BadgesData badges; | ||||
|  | ||||
|   const Badges({Key key, this.badges}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow(); | ||||
|      | ||||
|     if (entitiesToShow.isNotEmpty) { | ||||
|       if (AppSettings().scrollBadges) { | ||||
|         return ConstrainedBox( | ||||
|           constraints: BoxConstraints.tightFor(height: 112), | ||||
|           child: SingleChildScrollView( | ||||
|             scrollDirection: Axis.horizontal, | ||||
|             child: Row( | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               mainAxisAlignment: MainAxisAlignment.center, | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: entitiesToShow.map((entity) => | ||||
|                 EntityModel( | ||||
|                   entityWrapper: entity, | ||||
|                   child: Padding( | ||||
|                     padding: EdgeInsets.fromLTRB(5, 10, 5, 10), | ||||
|                     child: BadgeWidget(), | ||||
|                   ), | ||||
|                   handleTap: true, | ||||
|                 )).toList() | ||||
|             ), | ||||
|           ) | ||||
|         ); | ||||
|       } else { | ||||
|         return Padding( | ||||
|           padding: EdgeInsets.fromLTRB(5, 10, 5, 10), | ||||
|           child: Wrap( | ||||
|             alignment: WrapAlignment.center, | ||||
|             spacing: 10.0, | ||||
|             runSpacing: 5, | ||||
|             children: entitiesToShow.map((entity) => | ||||
|                 EntityModel( | ||||
|                   entityWrapper: entity, | ||||
|                   child: BadgeWidget(), | ||||
|                   handleTap: true, | ||||
|                 )).toList(), | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|     return Container(height: 0.0, width: 0.0,); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class BadgeWidget extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     Widget badgeIcon; | ||||
|     String onBadgeTextValue; | ||||
|     Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain); | ||||
|     switch (entityModel.entityWrapper.entity.domain) { | ||||
|       case "sun": | ||||
|         { | ||||
|           IconData iconData; | ||||
|           if (entityModel.entityWrapper.entity.state == "below_horizon") { | ||||
|             iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc); | ||||
|           } else { | ||||
|             iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8); | ||||
|           } | ||||
|           badgeIcon = Padding( | ||||
|             padding: EdgeInsets.all(10), | ||||
|             child: Icon( | ||||
|               iconData, | ||||
|             ) | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "camera": | ||||
|       case "media_player": | ||||
|       case "binary_sensor": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|             imagePadding: EdgeInsets.all(0.0), | ||||
|             iconPadding: EdgeInsets.all(10), | ||||
|             color: Theme.of(context).textTheme.body2.color | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "device_tracker": | ||||
|       case "person": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|             imagePadding: EdgeInsets.all(0.0), | ||||
|             iconPadding: EdgeInsets.all(10), | ||||
|             color: Theme.of(context).textTheme.body2.color | ||||
|           ); | ||||
|           onBadgeTextValue = entityModel.entityWrapper.entity.displayState; | ||||
|           break; | ||||
|         } | ||||
|       default: | ||||
|         { | ||||
|           onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement; | ||||
|           badgeIcon = Padding( | ||||
|             padding: EdgeInsets.all(4), | ||||
|             child: Text( | ||||
|               "${entityModel.entityWrapper.entity.displayState}", | ||||
|               overflow: TextOverflow.fade, | ||||
|               softWrap: false, | ||||
|               textAlign: TextAlign.center, | ||||
|               style: Theme.of(context).textTheme.body1 | ||||
|             ) | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|     } | ||||
|     Widget onBadgeText; | ||||
|     if (onBadgeTextValue == null || onBadgeTextValue.length == 0) { | ||||
|       onBadgeText = Container(width: 0.0, height: 0.0); | ||||
|     } else { | ||||
|       onBadgeText = Container( | ||||
|         constraints: BoxConstraints(maxWidth: 50), | ||||
|         padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0), | ||||
|         child: Text("$onBadgeTextValue", | ||||
|             style: Theme.of(context).textTheme.overline.copyWith( | ||||
|               color: HAClientTheme().getOnBadgeTextColor() | ||||
|             ), | ||||
|             textAlign: TextAlign.center, | ||||
|             softWrap: false, | ||||
|             overflow: TextOverflow.ellipsis | ||||
|           ), | ||||
|         decoration: new BoxDecoration( | ||||
|           color: iconColor, | ||||
|           borderRadius: BorderRadius.circular(9.0), | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|     return GestureDetector( | ||||
|         child: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: <Widget>[ | ||||
|             Stack( | ||||
|               overflow: Overflow.visible, | ||||
|               alignment: Alignment.center, | ||||
|               children: <Widget>[ | ||||
|                 Container( | ||||
|                   width: 45, | ||||
|                   height: 45, | ||||
|                   decoration: new BoxDecoration( | ||||
|                     // Circle shape | ||||
|                     shape: BoxShape.circle, | ||||
|                     color: Theme.of(context).cardColor, | ||||
|                     // The border you want | ||||
|                     border: Border.all( | ||||
|                       width: 2.0, | ||||
|                       color: iconColor, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 SizedBox( | ||||
|                   width: 41, | ||||
|                   height: 41, | ||||
|                   child: FittedBox( | ||||
|                     fit: BoxFit.contain, | ||||
|                     alignment: Alignment.center, | ||||
|                     child: badgeIcon, | ||||
|                   ) | ||||
|                 ), | ||||
|                 Positioned( | ||||
|                   bottom: -6, | ||||
|                   child: onBadgeText | ||||
|                 ) | ||||
|               ], | ||||
|             ), | ||||
|             Container( | ||||
|               constraints: BoxConstraints(maxWidth: 45), | ||||
|               padding: EdgeInsets.only(top: 10), | ||||
|               child: Text( | ||||
|                 "${entityModel.entityWrapper.displayName}", | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: Theme.of(context).textTheme.caption.copyWith( | ||||
|                   fontSize: 10 | ||||
|                 ), | ||||
|                 softWrap: true, | ||||
|                 maxLines: 3, | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|         onTap: () => entityModel.entityWrapper.handleTap(), | ||||
|         onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(), | ||||
|         onLongPress: () => entityModel.entityWrapper.handleHold(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										682
									
								
								lib/cards/card.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,682 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class CardData { | ||||
|  | ||||
|   String type; | ||||
|   List<EntityWrapper> entities = []; | ||||
|   List conditions; | ||||
|   bool showEmpty; | ||||
|   List stateFilter; | ||||
|   bool stateColor = true; | ||||
|  | ||||
|   EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null; | ||||
|  | ||||
|   factory CardData.parse(rawData) { | ||||
|     try { | ||||
|       if (rawData['type'] == null) { | ||||
|         rawData['type'] = CardType.ENTITIES; | ||||
|       } else if (!(rawData['type'] is String)) { | ||||
|         return CardData(null); | ||||
|       } | ||||
|       switch (rawData['type']) { | ||||
|           case CardType.ENTITIES: | ||||
|           case CardType.HISTORY_GRAPH: | ||||
|           case CardType.MAP: | ||||
|           case CardType.PICTURE_GLANCE: | ||||
|           case CardType.SENSOR: | ||||
|           case CardType.ENTITY: | ||||
|           case CardType.WEATHER_FORECAST: | ||||
|           case CardType.PLANT_STATUS: | ||||
|             if (rawData['entity'] != null) { | ||||
|               rawData['entities'] = [rawData['entity']]; | ||||
|             } | ||||
|             return EntitiesCardData(rawData); | ||||
|             break; | ||||
|           case CardType.ALARM_PANEL: | ||||
|             return AlarmPanelCardData(rawData); | ||||
|             break; | ||||
|           case CardType.LIGHT: | ||||
|             return LightCardData(rawData); | ||||
|             break; | ||||
|           case CardType.PICTURE_ELEMENTS: | ||||
|             //TODO temporary solution  | ||||
|             if (rawData.containsKey('camera_image')) { | ||||
|               rawData['entity'] = rawData['camera_image']; | ||||
|               return ButtonCardData(rawData); | ||||
|             } else { | ||||
|               return CardData(null); | ||||
|             } | ||||
|             break; | ||||
|           case CardType.ENTITY_BUTTON: | ||||
|           case CardType.BUTTON: | ||||
|           case CardType.PICTURE_ENTITY: | ||||
|             return ButtonCardData(rawData); | ||||
|             break; | ||||
|           case CardType.CONDITIONAL: | ||||
|             return CardData.parse(rawData['card']); | ||||
|             break; | ||||
|           case CardType.ENTITY_FILTER: | ||||
|             Map cardData = Map.from(rawData); | ||||
|             cardData.remove('type'); | ||||
|             if (rawData.containsKey('card')) { | ||||
|               cardData.addAll(rawData['card']); | ||||
|             } | ||||
|             cardData['type'] ??= CardType.ENTITIES; | ||||
|             return CardData.parse(cardData); | ||||
|             break; | ||||
|           case CardType.GAUGE: | ||||
|             return GaugeCardData(rawData); | ||||
|             break; | ||||
|           case CardType.GLANCE: | ||||
|           case CardType.THERMOSTAT: | ||||
|             if (rawData['entity'] != null) { | ||||
|               rawData['entities'] = [rawData['entity']]; | ||||
|             } | ||||
|             return GlanceCardData(rawData); | ||||
|             break; | ||||
|           case CardType.HORIZONTAL_STACK: | ||||
|             return HorizontalStackCardData(rawData); | ||||
|             break; | ||||
|           case CardType.VERTICAL_STACK: | ||||
|             return VerticalStackCardData(rawData); | ||||
|             break; | ||||
|           case CardType.MARKDOWN: | ||||
|             return MarkdownCardData(rawData); | ||||
|             break; | ||||
|           case CardType.MEDIA_CONTROL: | ||||
|             return MediaControlCardData(rawData); | ||||
|             break; | ||||
|           case CardType.BADGES: | ||||
|             return BadgesData(rawData); | ||||
|             break; | ||||
|           default: | ||||
|             if (rawData.containsKey('entity')) { | ||||
|               rawData['entities'] = [rawData['entity']];  | ||||
|             } | ||||
|             if (rawData.containsKey('entities') && rawData['entities'] is List) { | ||||
|               return EntitiesCardData(rawData);  | ||||
|             } | ||||
|             return CardData(null); | ||||
|         } | ||||
|     } catch (error, stacktrace) { | ||||
|       Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace); | ||||
|       return ErrorCardData(rawData); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   CardData(rawData) { | ||||
|     if (rawData != null && rawData is Map) { | ||||
|       type = rawData['type']; | ||||
|       conditions = rawData['conditions'] ?? []; | ||||
|       showEmpty = rawData['show_empty'] ?? true; | ||||
|       if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) { | ||||
|         stateFilter = rawData['state_filter']; | ||||
|       } else { | ||||
|         stateFilter = []; | ||||
|       } | ||||
|     } else { | ||||
|       type = CardType.UNKNOWN; | ||||
|       conditions = []; | ||||
|       showEmpty = true; | ||||
|       stateFilter = []; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget buildCardWidget() { | ||||
|     return UnsupportedCard(card: this); | ||||
|   } | ||||
|  | ||||
|   List<EntityWrapper> getEntitiesToShow() { | ||||
|     return entities.where((entityWrapper) { | ||||
|       if (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, stacktrace) { | ||||
|             Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState: $e', stacktrace: stacktrace); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       return showByFilter; | ||||
|     }).toList(); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class BadgesData extends CardData { | ||||
|  | ||||
|   String title; | ||||
|   String icon; | ||||
|   bool showHeaderToggle; | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return Badges(badges: this); | ||||
|   } | ||||
|    | ||||
|   BadgesData(rawData) : super(rawData) { | ||||
|     if (rawData['badges'] is List) { | ||||
|       rawData['badges'].forEach((dynamic rawBadge) { | ||||
|         if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {   | ||||
|           entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge))); | ||||
|         } else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) { | ||||
|           entities.add( | ||||
|             EntityWrapper( | ||||
|               entity: HomeAssistant().entities.get(rawBadge['entity']), | ||||
|               overrideName: rawBadge["name"]?.toString(), | ||||
|               overrideIcon: rawBadge["icon"], | ||||
|             ) | ||||
|           ); | ||||
|         } else if (rawBadge is Map && rawBadge.containsKey('entities')) { | ||||
|           _parseEntities(rawBadge); | ||||
|         } | ||||
|       });     | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _parseEntities(rawData) { | ||||
|     var rawEntities = rawData['entities'] ?? []; | ||||
|     rawEntities.forEach((rawEntity) { | ||||
|       if (rawEntity is String) { | ||||
|         if (HomeAssistant().entities.isExist(rawEntity)) { | ||||
|           entities.add(EntityWrapper( | ||||
|             entity: HomeAssistant().entities.get(rawEntity), | ||||
|             stateFilter: rawData['state_filter'] ?? [], | ||||
|           )); | ||||
|         } | ||||
|       } else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) { | ||||
|         Entity e = HomeAssistant().entities.get(rawEntity["entity"]); | ||||
|         entities.add( | ||||
|           EntityWrapper( | ||||
|               entity: e, | ||||
|               overrideName: rawEntity["name"]?.toString(), | ||||
|               overrideIcon: rawEntity["icon"], | ||||
|               stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []), | ||||
|               uiAction: EntityUIAction(rawEntityData: rawEntity) | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class EntitiesCardData extends CardData { | ||||
|  | ||||
|   String title; | ||||
|   String icon; | ||||
|   bool showHeaderToggle; | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return EntitiesCard(card: this); | ||||
|   } | ||||
|    | ||||
|   EntitiesCardData(rawData) : super(rawData) { | ||||
|     //Parsing card data | ||||
|     title = rawData['title']?.toString(); | ||||
|     icon = rawData['icon'] is String ? rawData['icon'] : null; | ||||
|     stateColor = rawData['state_color'] ?? false; | ||||
|     showHeaderToggle = rawData['show_header_toggle'] ?? false; | ||||
|     //Parsing entities | ||||
|     var rawEntities = rawData['entities'] ?? []; | ||||
|     rawEntities.forEach((rawEntity) { | ||||
|       if (rawEntity is String) { | ||||
|         if (HomeAssistant().entities.isExist(rawEntity)) { | ||||
|           entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity))); | ||||
|         } else { | ||||
|           entities.add(EntityWrapper(entity: Entity.missed(rawEntity))); | ||||
|         } | ||||
|       } else { | ||||
|         if (rawEntity["type"] == "divider") { | ||||
|           entities.add(EntityWrapper(entity: Entity.divider())); | ||||
|         } else if (rawEntity["type"] == "section") { | ||||
|           entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? ""))); | ||||
|         } else if (rawEntity["type"] == "call-service") { | ||||
|           Map uiActionData = { | ||||
|             "tap_action": { | ||||
|               "action": EntityUIAction.callService, | ||||
|               "service": rawEntity["service"], | ||||
|               "service_data": rawEntity["service_data"] | ||||
|             }, | ||||
|             "hold_action": EntityUIAction.none | ||||
|           }; | ||||
|           entities.add( | ||||
|             EntityWrapper( | ||||
|               entity: Entity.callService( | ||||
|                 icon: rawEntity["icon"], | ||||
|                 name: rawEntity["name"]?.toString(), | ||||
|                 service: rawEntity["service"], | ||||
|                 actionName: rawEntity["action_name"] | ||||
|               ), | ||||
|               stateColor: rawEntity["state_color"] ?? stateColor, | ||||
|               uiAction: EntityUIAction(rawEntityData: uiActionData) | ||||
|             ) | ||||
|           ); | ||||
|         } else if (rawEntity["type"] == "weblink") { | ||||
|           Map uiActionData = { | ||||
|             "tap_action": { | ||||
|               "action": EntityUIAction.navigate, | ||||
|               "service": rawEntity["url"] | ||||
|             }, | ||||
|             "hold_action": EntityUIAction.none | ||||
|           }; | ||||
|           entities.add(EntityWrapper( | ||||
|               entity: Entity.weblink( | ||||
|                   icon: rawEntity["icon"], | ||||
|                   name: rawEntity["name"]?.toString(), | ||||
|                   url: rawEntity["url"] | ||||
|               ), | ||||
|               stateColor: rawEntity["state_color"] ?? stateColor, | ||||
|               uiAction: EntityUIAction(rawEntityData: uiActionData) | ||||
|           ) | ||||
|           ); | ||||
|         } else if (HomeAssistant().entities.isExist(rawEntity["entity"])) { | ||||
|           Entity e = HomeAssistant().entities.get(rawEntity["entity"]); | ||||
|           entities.add( | ||||
|             EntityWrapper( | ||||
|                 entity: e, | ||||
|                 stateColor: rawEntity["state_color"] ?? stateColor, | ||||
|                 overrideName: rawEntity["name"]?.toString(), | ||||
|                 overrideIcon: rawEntity["icon"], | ||||
|                 stateFilter: rawEntity['state_filter'] ?? [], | ||||
|                 uiAction: EntityUIAction(rawEntityData: rawEntity) | ||||
|             ) | ||||
|           ); | ||||
|         } else { | ||||
|           entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"]))); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class AlarmPanelCardData extends CardData { | ||||
|  | ||||
|   String name; | ||||
|   List<dynamic> states; | ||||
|    | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return AlarmPanelCard(card: this); | ||||
|   } | ||||
|    | ||||
|   AlarmPanelCardData(rawData) : super(rawData) { | ||||
|     //Parsing card data | ||||
|     name = rawData['name']?.toString(); | ||||
|     states = rawData['states']; | ||||
|     //Parsing entity | ||||
|     var entitiId = rawData["entity"]; | ||||
|     if (entitiId != null && entitiId is String) { | ||||
|       if (HomeAssistant().entities.isExist(entitiId)) { | ||||
|         entities.add(EntityWrapper( | ||||
|             entity: HomeAssistant().entities.get(entitiId), | ||||
|             stateColor: true, | ||||
|             overrideName: name | ||||
|         )); | ||||
|       } else { | ||||
|         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class LightCardData extends CardData { | ||||
|  | ||||
|   String name; | ||||
|   String icon; | ||||
|    | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     if (this.entity != null && this.entity.entity is LightEntity) { | ||||
|       return LightCard(card: this); | ||||
|     } | ||||
|     return ErrorCard( | ||||
|       errorText: 'Specify an entity from within the light domain.', | ||||
|       showReportButton: false, | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   LightCardData(rawData) : super(rawData) { | ||||
|     //Parsing card data | ||||
|     name = rawData['name']?.toString(); | ||||
|     icon = rawData['icon'] is String ? rawData['icon'] : null; | ||||
|     //Parsing entity | ||||
|     var entitiId = rawData["entity"]; | ||||
|     if (entitiId != null && entitiId is String) { | ||||
|       if (HomeAssistant().entities.isExist(entitiId)) { | ||||
|         entities.add(EntityWrapper( | ||||
|             entity: HomeAssistant().entities.get(entitiId), | ||||
|             overrideName: name, | ||||
|             overrideIcon: icon, | ||||
|             uiAction: EntityUIAction()..tapAction = EntityUIAction.toggle | ||||
|         )); | ||||
|       } else { | ||||
|         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||
|       } | ||||
|     } else { | ||||
|       entities.add(EntityWrapper(entity: Entity.missed('$entitiId'))); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ButtonCardData extends CardData { | ||||
|  | ||||
|   String name; | ||||
|   String icon; | ||||
|   bool showName; | ||||
|   bool showIcon; | ||||
|   double iconHeightPx = 0; | ||||
|   double iconHeightRem = 0; | ||||
|    | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return EntityButtonCard(card: this); | ||||
|   } | ||||
|    | ||||
|   ButtonCardData(rawData) : super(rawData) { | ||||
|     //Parsing card data | ||||
|     name = rawData['name']?.toString(); | ||||
|     icon = rawData['icon'] is String ? rawData['icon'] : null; | ||||
|     showName = rawData['show_name'] ?? true; | ||||
|     showIcon = rawData['show_icon'] ?? true; | ||||
|     stateColor = rawData['state_color'] ?? true; | ||||
|     var rawHeight = rawData['icon_height']; | ||||
|     if (rawHeight != null && rawHeight is String) { | ||||
|       if (rawHeight.contains('px')) { | ||||
|         iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0; | ||||
|       } else if (rawHeight.contains('rem')) { | ||||
|         iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;  | ||||
|       } else if (rawHeight.contains('em')) { | ||||
|         iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0; | ||||
|       } | ||||
|     } | ||||
|     //Parsing entity | ||||
|     var entitiId = rawData["entity"]; | ||||
|     if (entitiId != null && entitiId is String) { | ||||
|       if (HomeAssistant().entities.isExist(entitiId)) { | ||||
|         entities.add(EntityWrapper( | ||||
|             entity: HomeAssistant().entities.get(entitiId), | ||||
|             overrideName: name, | ||||
|             overrideIcon: icon, | ||||
|             stateColor: stateColor, | ||||
|             uiAction: EntityUIAction( | ||||
|               rawEntityData: rawData | ||||
|             ) | ||||
|         )); | ||||
|       } else { | ||||
|         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||
|       } | ||||
|     } else if (entitiId == null) { | ||||
|       entities.add( | ||||
|         EntityWrapper( | ||||
|           entity: Entity.ghost( | ||||
|             name, | ||||
|             icon, | ||||
|           ), | ||||
|           stateColor: stateColor, | ||||
|           uiAction: EntityUIAction( | ||||
|             rawEntityData: rawData | ||||
|           ) | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| class GaugeCardData extends CardData { | ||||
|  | ||||
|   String name; | ||||
|   String unit; | ||||
|   double min; | ||||
|   double max; | ||||
|   Map severity; | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return GaugeCard(card: this); | ||||
|   } | ||||
|    | ||||
|   GaugeCardData(rawData) : super(rawData) { | ||||
|     //Parsing card data | ||||
|     name = rawData['name']?.toString(); | ||||
|     unit = rawData['unit']; | ||||
|     if (rawData['min'] is int) { | ||||
|       min = rawData['min'].toDouble();   | ||||
|     } else if (rawData['min'] is double) { | ||||
|       min = rawData['min']; | ||||
|     } else { | ||||
|       min = 0; | ||||
|     } | ||||
|     if (rawData['max'] is int) { | ||||
|       max = rawData['max'].toDouble();   | ||||
|     } else if (rawData['max'] is double) { | ||||
|       max = rawData['max']; | ||||
|     } else { | ||||
|       max = 100; | ||||
|     } | ||||
|     severity = rawData['severity']; | ||||
|     //Parsing entity | ||||
|     var entitiId = rawData["entity"] is List ? rawData["entity"][0] : rawData["entity"]; | ||||
|     if (entitiId != null && entitiId is String) { | ||||
|       if (HomeAssistant().entities.isExist(entitiId)) { | ||||
|         entities.add(EntityWrapper( | ||||
|             entity: HomeAssistant().entities.get(entitiId), | ||||
|             overrideName: name | ||||
|         )); | ||||
|       } else { | ||||
|         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||
|       } | ||||
|     } else { | ||||
|       entities.add(EntityWrapper(entity: Entity.missed('$entitiId'))); | ||||
|     } | ||||
|      | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class GlanceCardData extends CardData { | ||||
|  | ||||
|   String title; | ||||
|   bool showName; | ||||
|   bool showIcon; | ||||
|   bool showState; | ||||
|   bool stateColor; | ||||
|   int columnsCount; | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return GlanceCard(card: this); | ||||
|   } | ||||
|    | ||||
|   GlanceCardData(rawData) : super(rawData) { | ||||
|     //Parsing card data | ||||
|     title = rawData["title"]?.toString(); | ||||
|     showName = rawData['show_name'] ?? true; | ||||
|     showIcon = rawData['show_icon'] ?? true; | ||||
|     showState = rawData['show_state'] ?? true; | ||||
|     stateColor = rawData['state_color'] ?? true; | ||||
|     columnsCount = rawData['columns'] ?? 4; | ||||
|     //Parsing entities | ||||
|     var rawEntities = rawData["entities"] ?? []; | ||||
|     rawEntities.forEach((rawEntity) { | ||||
|       if (rawEntity is String) { | ||||
|         if (HomeAssistant().entities.isExist(rawEntity)) { | ||||
|           entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity))); | ||||
|         } else { | ||||
|           entities.add(EntityWrapper(entity: Entity.missed(rawEntity))); | ||||
|         } | ||||
|       } else { | ||||
|         if (HomeAssistant().entities.isExist(rawEntity["entity"])) { | ||||
|           Entity e = HomeAssistant().entities.get(rawEntity["entity"]); | ||||
|           entities.add( | ||||
|             EntityWrapper( | ||||
|                 entity: e, | ||||
|                 stateColor: stateColor, | ||||
|                 overrideName: rawEntity["name"]?.toString(), | ||||
|                 overrideIcon: rawEntity["icon"], | ||||
|                 stateFilter: rawEntity['state_filter'] ?? [], | ||||
|                 uiAction: EntityUIAction(rawEntityData: rawEntity) | ||||
|             ) | ||||
|           ); | ||||
|         } else { | ||||
|           entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"]))); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class HorizontalStackCardData extends CardData { | ||||
|  | ||||
|   List<CardData> childCards; | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return HorizontalStackCard(card: this); | ||||
|   } | ||||
|    | ||||
|   HorizontalStackCardData(rawData) : super(rawData) { | ||||
|     if (rawData.containsKey('cards') && rawData['cards'] is List) { | ||||
|       childCards = rawData['cards'].map<CardData>((childCard) { | ||||
|         return CardData.parse(childCard); | ||||
|       }).toList(); | ||||
|     } else { | ||||
|       childCards = []; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class VerticalStackCardData extends CardData { | ||||
|  | ||||
|   List<CardData> childCards; | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return VerticalStackCard(card: this); | ||||
|   } | ||||
|    | ||||
|   VerticalStackCardData(rawData) : super(rawData) { | ||||
|     if (rawData.containsKey('cards') && rawData['cards'] is List) { | ||||
|       childCards = rawData['cards'].map<CardData>((childCard) { | ||||
|         return CardData.parse(childCard); | ||||
|       }).toList(); | ||||
|     } else { | ||||
|       childCards = []; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class MarkdownCardData extends CardData { | ||||
|  | ||||
|   String title; | ||||
|   String content; | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return MarkdownCard(card: this); | ||||
|   } | ||||
|    | ||||
|   MarkdownCardData(rawData) : super(rawData) { | ||||
|     //Parsing card data | ||||
|     title = rawData['title']; | ||||
|     content = rawData['content']; | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class MediaControlCardData extends CardData { | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return MediaControlsCard(card: this); | ||||
|   } | ||||
|  | ||||
|   MediaControlCardData(rawData) : super(rawData) { | ||||
|     var entitiId = rawData["entity"]; | ||||
|     if (entitiId != null && entitiId is String) { | ||||
|       if (HomeAssistant().entities.isExist(entitiId)) { | ||||
|         entities.add(EntityWrapper( | ||||
|             entity: HomeAssistant().entities.get(entitiId), | ||||
|         )); | ||||
|       } else { | ||||
|         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class ErrorCardData extends CardData { | ||||
|  | ||||
|   String cardConfig; | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return ErrorCard(card: this); | ||||
|   } | ||||
|  | ||||
|   ErrorCardData(rawData) : super(rawData) { | ||||
|     cardConfig = '$rawData'; | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										79
									
								
								lib/cards/entities_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,79 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntitiesCard extends StatelessWidget { | ||||
|   final EntitiesCardData card; | ||||
|  | ||||
|   const EntitiesCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     List<EntityWrapper> entitiesToShow = card.getEntitiesToShow(); | ||||
|     if (entitiesToShow.isEmpty && !card.showEmpty) { | ||||
|       return Container(height: 0.0, width: 0.0,); | ||||
|     } | ||||
|     List<Widget> body = []; | ||||
|     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.title, | ||||
|         trailing: headerSwitch, | ||||
|         emptyPadding: Sizes.rowPadding, | ||||
|         leading: card.icon != null ? Icon( | ||||
|           MaterialDesignIcons.getIconDataFromIconName(card.icon), | ||||
|           size: Sizes.iconSize, | ||||
|           color: Theme.of(context).textTheme.headline.color | ||||
|         ) : null, | ||||
|       ) | ||||
|     ); | ||||
|     body.addAll( | ||||
|       entitiesToShow.map((EntityWrapper entity) { | ||||
|         return Padding( | ||||
|             padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0), | ||||
|             child: EntityModel( | ||||
|                 entityWrapper: entity, | ||||
|                 handleTap: true, | ||||
|                 child: entity.entity.buildDefaultWidget(context) | ||||
|             ), | ||||
|           ); | ||||
|       })   | ||||
|     ); | ||||
|     return CardWrapper( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.only( | ||||
|             right: Sizes.rightWidgetPadding, | ||||
|             left: Sizes.leftWidgetPadding, | ||||
|             bottom: Sizes.rowPadding, | ||||
|           ), | ||||
|           child: Center( | ||||
|             child: Column( | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               mainAxisAlignment: MainAxisAlignment.start, | ||||
|               children: body | ||||
|             ) | ||||
|           ), | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|    | ||||
| } | ||||
							
								
								
									
										96
									
								
								lib/cards/entity_button_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,96 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityButtonCard extends StatelessWidget { | ||||
|  | ||||
|   final ButtonCardData card; | ||||
|  | ||||
|   EntityButtonCard({ | ||||
|     Key key, this.card | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     EntityWrapper entityWrapper = card.entity; | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.missed) { | ||||
|       return EntityModel( | ||||
|         entityWrapper: card.entity, | ||||
|         child: MissedEntityWidget(), | ||||
|         handleTap: false, | ||||
|       ); | ||||
|     } else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|      | ||||
|     double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize); | ||||
|      | ||||
|     Widget buttonIcon; | ||||
|     if (!card.showIcon) { | ||||
|       buttonIcon = Container(height: Sizes.rowPadding, width: 10); | ||||
|     } else if (iconSize > 0) { | ||||
|       buttonIcon = SizedBox( | ||||
|         height: iconSize, | ||||
|         child: FractionallySizedBox( | ||||
|           widthFactor: 0.5, | ||||
|           child: FittedBox( | ||||
|             fit: BoxFit.contain, | ||||
|             child: EntityIcon( | ||||
|               //padding: EdgeInsets.only(top: 6), | ||||
|             ), | ||||
|           ) | ||||
|         ), | ||||
|       ); | ||||
|     } else { | ||||
|       buttonIcon = AspectRatio( | ||||
|         aspectRatio: 2, | ||||
|         child: FractionallySizedBox( | ||||
|           widthFactor: 0.5, | ||||
|           child: FittedBox( | ||||
|             fit: BoxFit.fitWidth, | ||||
|             child: EntityIcon( | ||||
|               //padding: EdgeInsets.only(top: 6), | ||||
|             ), | ||||
|           ) | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return CardWrapper( | ||||
|       child: EntityModel( | ||||
|         entityWrapper: card.entity, | ||||
|         child: InkWell( | ||||
|           onTap: () => entityWrapper.handleTap(), | ||||
|           onLongPress: () => entityWrapper.handleHold(), | ||||
|           onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||
|           child: Center( | ||||
|             child: Padding( | ||||
|               padding: EdgeInsets.only(top: 5), | ||||
|               child: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                 children: <Widget>[ | ||||
|                   buttonIcon, | ||||
|                   _buildName(context) | ||||
|                 ], | ||||
|               ) | ||||
|             ) | ||||
|           ), | ||||
|         ), | ||||
|         handleTap: true | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildName(BuildContext context) { | ||||
|     if (card.showName) { | ||||
|       return EntityName( | ||||
|         padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding), | ||||
|         textOverflow: TextOverflow.ellipsis, | ||||
|         maxLines: 3, | ||||
|         textStyle: Theme.of(context).textTheme.subhead, | ||||
|         wordsWrap: true, | ||||
|         textAlign: TextAlign.center | ||||
|       ); | ||||
|     } | ||||
|     return Container(width: 0, height: 0); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										51
									
								
								lib/cards/error_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class ErrorCard extends StatelessWidget { | ||||
|   final ErrorCardData card; | ||||
|   final String errorText; | ||||
|   final bool showReportButton; | ||||
|  | ||||
|   const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     String error; | ||||
|     if (errorText == null) { | ||||
|       error = 'There was an error showing ${card?.type}'; | ||||
|     } else { | ||||
|       error = errorText; | ||||
|     } | ||||
|     return CardWrapper( | ||||
|       color: Theme.of(context).errorColor, | ||||
|       child: Padding( | ||||
|         padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||
|         child: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: <Widget>[ | ||||
|             Text( | ||||
|               error, | ||||
|               textAlign: TextAlign.center, | ||||
|             ), | ||||
|             card != null ? | ||||
|             RaisedButton( | ||||
|               onPressed: () { | ||||
|                 Clipboard.setData(new ClipboardData(text: card.cardConfig)); | ||||
|               }, | ||||
|               child: Text('Copy card config'), | ||||
|             ) : | ||||
|             Container(width: 0, height: 0), | ||||
|             showReportButton ? | ||||
|             RaisedButton( | ||||
|               onPressed: () { | ||||
|                 Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title="); | ||||
|               }, | ||||
|               child: Text('Report issue'), | ||||
|             ) : | ||||
|             Container(width: 0, height: 0) | ||||
|           ], | ||||
|         ), | ||||
|       ) | ||||
|     ); | ||||
|   }   | ||||
| } | ||||
							
								
								
									
										201
									
								
								lib/cards/gauge_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,201 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class GaugeCard extends StatelessWidget { | ||||
|  | ||||
|   final GaugeCardData card; | ||||
|  | ||||
|   GaugeCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     EntityWrapper entityWrapper = card.entity; | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.missed) { | ||||
|       return EntityModel( | ||||
|         entityWrapper: card.entity, | ||||
|         child: MissedEntityWidget(), | ||||
|         handleTap: false, | ||||
|       ); | ||||
|     } | ||||
|     entityWrapper.overrideName = card.name ?? | ||||
|         entityWrapper.displayName; | ||||
|     entityWrapper.unitOfMeasurementOverride = card.unit ?? | ||||
|         entityWrapper.unitOfMeasurement; | ||||
|     double fixedValue; | ||||
|     double value = entityWrapper.entity.doubleState; | ||||
|     if (value > card.max) { | ||||
|       fixedValue = card.max.toDouble(); | ||||
|     } else if (value < card.min) { | ||||
|       fixedValue = card.min.toDouble(); | ||||
|     } else { | ||||
|       fixedValue = value; | ||||
|     } | ||||
|      | ||||
|     List<GaugeRange> ranges; | ||||
|     Color currentColor; | ||||
|     if (card.severity != null && card.severity["green"] is int && card.severity["red"] is int && card.severity["yellow"] is int) { | ||||
|       List<RangeContainer> rangesList = <RangeContainer>[ | ||||
|         RangeContainer(card.severity["green"], HAClientTheme().getGreenGaugeColor()), | ||||
|         RangeContainer(card.severity["red"], HAClientTheme().getRedGaugeColor()), | ||||
|         RangeContainer(card.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: card.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: card.min.toDouble(), | ||||
|           endValue: card.max.toDouble(), | ||||
|           color: Theme.of(context).primaryColorDark.withOpacity(0.1), | ||||
|           sizeUnit: GaugeSizeUnit.factor, | ||||
|           endWidth: 0.3, | ||||
|           startWidth: 0.3, | ||||
|         ) | ||||
|       ]; | ||||
|     } | ||||
|  | ||||
|     return CardWrapper( | ||||
|       padding: EdgeInsets.all(4), | ||||
|       child: EntityModel( | ||||
|         entityWrapper: entityWrapper, | ||||
|         child: InkWell( | ||||
|           onTap: () => entityWrapper.handleTap(), | ||||
|           onLongPress: () => entityWrapper.handleHold(), | ||||
|           onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||
|           child: AspectRatio( | ||||
|             aspectRatio: 1.8, | ||||
|             child: Stack( | ||||
|               alignment: Alignment.bottomCenter, | ||||
|               children: <Widget>[ | ||||
|                 IgnorePointer( | ||||
|                   ignoring: true, | ||||
|                   child: SfRadialGauge( | ||||
|                     axes: <RadialAxis>[ | ||||
|                       RadialAxis( | ||||
|                         maximum: card.max.toDouble(), | ||||
|                         minimum: card.min.toDouble(), | ||||
|                         showLabels: false, | ||||
|                         useRangeColorForAxis: true, | ||||
|                         showTicks: false, | ||||
|                         canScaleToFit: true, | ||||
|                         ranges: ranges, | ||||
|                         axisLineStyle: AxisLineStyle( | ||||
|                           thickness: 0.3, | ||||
|                           thicknessUnit: GaugeSizeUnit.factor, | ||||
|                           color: Colors.transparent | ||||
|                         ), | ||||
|                         startAngle: 180, | ||||
|                         endAngle: 0, | ||||
|                         pointers: <GaugePointer>[ | ||||
|                           RangePointer( | ||||
|                             value: fixedValue, | ||||
|                             sizeUnit: GaugeSizeUnit.factor, | ||||
|                             width: 0.3, | ||||
|                             color: currentColor, | ||||
|                             enableAnimation: true, | ||||
|                             animationType: AnimationType.bounceOut, | ||||
|                           ) | ||||
|                         ] | ||||
|                       ) | ||||
|                     ], | ||||
|                   ) | ||||
|                 ), | ||||
|                 Column( | ||||
|                   mainAxisSize: MainAxisSize.max, | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   children: <Widget>[ | ||||
|                     Flexible( | ||||
|                       flex: 8, | ||||
|                       fit: FlexFit.tight, | ||||
|                       child: Container() | ||||
|                     ), | ||||
|                     Flexible( | ||||
|                       flex: 6, | ||||
|                       fit: FlexFit.tight, | ||||
|                       child: FractionallySizedBox( | ||||
|                         widthFactor: 0.4, | ||||
|                         child: FittedBox( | ||||
|                           fit: BoxFit.contain, | ||||
|                           alignment: Alignment.bottomCenter, | ||||
|                           child: SimpleEntityState( | ||||
|                             padding: EdgeInsets.all(0), | ||||
|                             expanded: false, | ||||
|                             maxLines: 1, | ||||
|                             textAlign: TextAlign.center | ||||
|                           ), | ||||
|                         ) | ||||
|                       ) | ||||
|                     ), | ||||
|                     Flexible( | ||||
|                       flex: 3, | ||||
|                       fit: FlexFit.tight, | ||||
|                       child: FittedBox( | ||||
|                         fit: BoxFit.contain, | ||||
|                         child: EntityName( | ||||
|                           padding: EdgeInsets.all(0), | ||||
|                           textStyle: Theme.of(context).textTheme.subhead | ||||
|                         ), | ||||
|                       ) | ||||
|                     ),   | ||||
|                   ], | ||||
|                 ) | ||||
|               ], | ||||
|             ) | ||||
|           ), | ||||
|         ), | ||||
|         handleTap: true | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|    | ||||
| } | ||||
|  | ||||
| class RangeContainer { | ||||
|   final int startFrom; | ||||
|   Color color; | ||||
|  | ||||
|   RangeContainer(this.startFrom, this.color); | ||||
| } | ||||
							
								
								
									
										126
									
								
								lib/cards/glance_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,126 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class GlanceCard extends StatelessWidget { | ||||
|   final GlanceCardData card; | ||||
|  | ||||
|   const GlanceCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     List<EntityWrapper> entitiesToShow = card.getEntitiesToShow(); | ||||
|     if (entitiesToShow.isEmpty && !card.showEmpty) { | ||||
|       return Container(height: 0.0, width: 0.0,); | ||||
|     } | ||||
|     int length = entitiesToShow.length; | ||||
|     int rowsCount; | ||||
|     int columnsCount; | ||||
|     if (length == 0) { | ||||
|       columnsCount = 0; | ||||
|       rowsCount = 0; | ||||
|     } else { | ||||
|       columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length; | ||||
|       rowsCount = (length / columnsCount).round(); | ||||
|     } | ||||
|     List<TableRow> rows = []; | ||||
|     for (int i = 0; i < rowsCount; i++) { | ||||
|       int start = i*columnsCount; | ||||
|       int end = start + math.min(columnsCount, length - start); | ||||
|       List<Widget> rowChildren = []; | ||||
|       rowChildren.addAll(entitiesToShow.sublist( | ||||
|           start, end | ||||
|         ).map( | ||||
|           (EntityWrapper entity){ | ||||
|             return Padding( | ||||
|               padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding), | ||||
|               child: EntityModel( | ||||
|                 entityWrapper: entity, | ||||
|                 child: _buildEntityContainer(context, entity), | ||||
|                 handleTap: true | ||||
|               ) | ||||
|             ); | ||||
|           } | ||||
|         ).toList() | ||||
|       ); | ||||
|       while (rowChildren.length < columnsCount) { | ||||
|         rowChildren.add( | ||||
|           Container() | ||||
|         ); | ||||
|       } | ||||
|       rows.add( | ||||
|         TableRow( | ||||
|           children: rowChildren | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|     return CardWrapper( | ||||
|       child: Center( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: <Widget>[ | ||||
|               CardHeader( | ||||
|                 name: card.title, | ||||
|                 emptyPadding: Sizes.rowPadding, | ||||
|               ), | ||||
|               Table( | ||||
|                 children: rows | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) { | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.missed) { | ||||
|       return MissedEntityWidget(); | ||||
|     } else if (entityWrapper.entity.statelessType != StatelessEntityType.none) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|     List<Widget> result = []; | ||||
|     if (card.showName) { | ||||
|       result.add(_buildName(context)); | ||||
|     } | ||||
|     result.add( | ||||
|         EntityIcon( | ||||
|           padding: EdgeInsets.all(0.0), | ||||
|           size: Sizes.iconSize, | ||||
|         ) | ||||
|     ); | ||||
|     if (card.showState) { | ||||
|       result.add(_buildState()); | ||||
|     } | ||||
|  | ||||
|     return InkResponse( | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: result, | ||||
|       ), | ||||
|       onTap: () => entityWrapper.handleTap(), | ||||
|       onLongPress: () => entityWrapper.handleHold(), | ||||
|       onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildName(BuildContext context) { | ||||
|     return EntityName( | ||||
|       padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|       textOverflow: TextOverflow.ellipsis, | ||||
|       wordsWrap: false, | ||||
|       textAlign: TextAlign.center, | ||||
|       textStyle: Theme.of(context).textTheme.body1, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildState() { | ||||
|     return SimpleEntityState( | ||||
|       textAlign: TextAlign.center, | ||||
|       expanded: false, | ||||
|       maxLines: 1, | ||||
|       padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										30
									
								
								lib/cards/horizontal_srack_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class HorizontalStackCard extends StatelessWidget { | ||||
|   final HorizontalStackCardData card; | ||||
|  | ||||
|   const HorizontalStackCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (card.childCards.isNotEmpty) { | ||||
|       List<Widget> children = []; | ||||
|       children = card.childCards.map((childCard) => Flexible( | ||||
|           fit: FlexFit.tight, | ||||
|           child: childCard.buildCardWidget() | ||||
|         ) | ||||
|       ).toList(); | ||||
|       return IntrinsicHeight( | ||||
|         child: Row( | ||||
|           mainAxisSize: MainAxisSize.max, | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|           children: children, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|     return Container(height: 0.0, width: 0.0,); | ||||
|   } | ||||
|  | ||||
|    | ||||
| } | ||||
							
								
								
									
										161
									
								
								lib/cards/light_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,161 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class LightCard extends StatefulWidget { | ||||
|  | ||||
|   final LightCardData card; | ||||
|  | ||||
|   LightCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   State<StatefulWidget> createState() { | ||||
|     return _LightCardState(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _LightCardState extends State<LightCard> { | ||||
|  | ||||
|   double _actualBrightness; | ||||
|   double _newBrightness; | ||||
|   bool _changedHere = false; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   void _setBrightness(double value, LightEntity entity) { | ||||
|     setState((){ | ||||
|       _newBrightness = value; | ||||
|       _changedHere = true; | ||||
|     }); | ||||
|     ConnectionManager().callService( | ||||
|       domain: entity.domain, | ||||
|       service: "turn_on", | ||||
|       entityId: entity.entityId, | ||||
|       data: {"brightness": value.round()} | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     EntityWrapper entityWrapper = widget.card.entity; | ||||
|     LightEntity entity = entityWrapper.entity; | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.missed) { | ||||
|       return EntityModel( | ||||
|         entityWrapper: widget.card.entity, | ||||
|         child: MissedEntityWidget(), | ||||
|         handleTap: false, | ||||
|       ); | ||||
|     } | ||||
|     entityWrapper.overrideName = widget.card.name ?? | ||||
|         entityWrapper.displayName; | ||||
|     entityWrapper.overrideIcon = widget.card.icon ?? | ||||
|         entityWrapper.icon; | ||||
|      | ||||
|     if (!_changedHere) { | ||||
|       _actualBrightness = (entity.brightness ?? 0).toDouble(); | ||||
|       _newBrightness = _actualBrightness; | ||||
|     } else { | ||||
|       _changedHere = false; | ||||
|     } | ||||
|     Color lightColor = entity.color?.toColor(); | ||||
|     Color color; | ||||
|     if (lightColor != null && lightColor != Colors.white) { | ||||
|       color = lightColor; | ||||
|     } else { | ||||
|       color = Theme.of(context).accentColor; | ||||
|     } | ||||
|  | ||||
|     return CardWrapper( | ||||
|       padding: EdgeInsets.all(4), | ||||
|       child: EntityModel( | ||||
|         entityWrapper: entityWrapper, | ||||
|         child: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: <Widget>[ | ||||
|             ConstrainedBox( | ||||
|               constraints: BoxConstraints.loose(Size(200, 200)), | ||||
|               child: AspectRatio( | ||||
|                 aspectRatio: 1, | ||||
|                 child: Stack( | ||||
|                   alignment: Alignment.center, | ||||
|                   children: <Widget>[ | ||||
|                     SfRadialGauge( | ||||
|                       axes: <RadialAxis>[ | ||||
|                         RadialAxis( | ||||
|                           onAxisTapped: (val) { | ||||
|                             _setBrightness(val, entity); | ||||
|                           }, | ||||
|                           maximum: 255, | ||||
|                           minimum: 0, | ||||
|                           showLabels: false, | ||||
|                           showTicks: false, | ||||
|                           axisLineStyle: AxisLineStyle( | ||||
|                             thickness: 0.05, | ||||
|                             thicknessUnit: GaugeSizeUnit.factor, | ||||
|                             color: HAClientTheme().getDisabledStateColor(context) | ||||
|                           ), | ||||
|                           pointers: <GaugePointer>[ | ||||
|                             RangePointer( | ||||
|                               value: _actualBrightness, | ||||
|                               sizeUnit: GaugeSizeUnit.factor, | ||||
|                               width: 0.05, | ||||
|                               color: color, | ||||
|                               enableAnimation: true, | ||||
|                               animationType: AnimationType.bounceOut, | ||||
|                             ), | ||||
|                             MarkerPointer( | ||||
|                               value: _newBrightness, | ||||
|                               markerType: MarkerType.circle, | ||||
|                               markerHeight: 20, | ||||
|                               markerWidth: 20, | ||||
|                               enableDragging: true, | ||||
|                               onValueChangeEnd: (val) { | ||||
|                                 _setBrightness(val, entity); | ||||
|                               }, | ||||
|                               color: HAClientTheme().getColorByEntityState(entity.state, context) | ||||
|                               //enableAnimation: true, | ||||
|                               //animationType: AnimationType.bounceOut, | ||||
|                             ) | ||||
|                           ] | ||||
|                         ) | ||||
|                       ], | ||||
|                     ), | ||||
|                     FractionallySizedBox( | ||||
|                       heightFactor: 0.4, | ||||
|                       widthFactor: 0.4, | ||||
|                       child: AspectRatio( | ||||
|                         aspectRatio: 1, | ||||
|                         child: InkResponse( | ||||
|                           onTap: () => entityWrapper.handleTap(), | ||||
|                           onLongPress: () => entityWrapper.handleHold(), | ||||
|                           child: FittedBox( | ||||
|                             fit: BoxFit.contain, | ||||
|                             child: EntityIcon( | ||||
|                               showBadge: false, | ||||
|                               padding: EdgeInsets.all(0) | ||||
|                             ) | ||||
|                           ) | ||||
|                         )  | ||||
|                       ) | ||||
|                     ) | ||||
|                   ], | ||||
|                 ) | ||||
|               ) | ||||
|             ), | ||||
|             EntityName( | ||||
|               padding: EdgeInsets.all(0), | ||||
|               wordsWrap: true, | ||||
|               maxLines: 3, | ||||
|               textOverflow: TextOverflow.ellipsis, | ||||
|               textAlign: TextAlign.center, | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|         handleTap: true | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|    | ||||
							
								
								
									
										33
									
								
								lib/cards/markdown_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class MarkdownCard extends StatelessWidget { | ||||
|   final MarkdownCardData card; | ||||
|  | ||||
|   const MarkdownCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (card.content == null) { | ||||
|       return Container(height: 0.0, width: 0.0,); | ||||
|     } else if (card.content == '***') { | ||||
|       return Container(height: Sizes.rowPadding, width: 0.0,); | ||||
|     } | ||||
|     return CardWrapper( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|             children: <Widget>[ | ||||
|               CardHeader(name: card.title), | ||||
|               MarkdownBody( | ||||
|                 data: card.content, | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|    | ||||
| } | ||||
							
								
								
									
										35
									
								
								lib/cards/media_control_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class MediaControlsCard extends StatelessWidget { | ||||
|   final MediaControlCardData card; | ||||
|  | ||||
|   const MediaControlsCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (card.entity.entity.statelessType == StatelessEntityType.missed) { | ||||
|       return EntityModel( | ||||
|         entityWrapper: card.entity, | ||||
|         child: MissedEntityWidget(), | ||||
|         handleTap: false, | ||||
|       ); | ||||
|     } else if (card.entity.entity.domain == null || card.entity.entity.domain != 'media_player') { | ||||
|       return EntityModel( | ||||
|         entityWrapper: card.entity, | ||||
|         child: ErrorEntityWidget( | ||||
|           text: '${card.entity.entity?.entityId} is not a media_player', | ||||
|         ), | ||||
|         handleTap: false, | ||||
|       ); | ||||
|     } | ||||
|     return CardWrapper( | ||||
|         child: EntityModel( | ||||
|             entityWrapper: card.entity, | ||||
|             handleTap: null, | ||||
|             child: MediaPlayerWidget() | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|    | ||||
| } | ||||
							
								
								
									
										12
									
								
								lib/cards/unsupported_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class UnsupportedCard extends StatelessWidget { | ||||
|   final CardData card; | ||||
|  | ||||
|   const UnsupportedCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container(height: 20); | ||||
|   }   | ||||
| } | ||||
							
								
								
									
										23
									
								
								lib/cards/vertical_stack_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class VerticalStackCard extends StatelessWidget { | ||||
|   final VerticalStackCardData card; | ||||
|  | ||||
|   const VerticalStackCard({Key key, this.card}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (card.childCards.isNotEmpty) { | ||||
|       return Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         mainAxisAlignment: MainAxisAlignment.start, | ||||
|         children: card.childCards.map<Widget>( | ||||
|           (childCard) => childCard.buildCardWidget() | ||||
|         ).toList(), | ||||
|       ); | ||||
|     } | ||||
|     return Container(height: 0.0, width: 0.0,); | ||||
|   } | ||||
|  | ||||
|    | ||||
| } | ||||
| @@ -1,12 +1,14 @@ | ||||
| part of '../main.dart'; | ||||
| part of '../../main.dart'; | ||||
| 
 | ||||
| class CardHeaderWidget extends StatelessWidget { | ||||
| class CardHeader extends StatelessWidget { | ||||
| 
 | ||||
|   final String name; | ||||
|   final Widget trailing; | ||||
|   final Widget leading; | ||||
|   final Widget subtitle; | ||||
|   final double emptyPadding; | ||||
| 
 | ||||
|   const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key); | ||||
|   const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key); | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -14,14 +16,15 @@ class CardHeaderWidget extends StatelessWidget { | ||||
|     if ((name != null) && (name.trim().length > 0)) { | ||||
|       result = new ListTile( | ||||
|         trailing: trailing, | ||||
|         leading: leading, | ||||
|         subtitle: subtitle, | ||||
|         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); | ||||
|       result = new Container(width: 0.0, height: emptyPadding); | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
							
								
								
									
										23
									
								
								lib/cards/widgets/card_wrapper.widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class CardWrapper extends StatelessWidget { | ||||
|    | ||||
|   final Widget child; | ||||
|   final EdgeInsets padding; | ||||
|   final Color color; | ||||
|  | ||||
|   const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       color: color, | ||||
|       child: Padding( | ||||
|         padding: padding, | ||||
|         child: child | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										87
									
								
								lib/const.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,87 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class EntityState { | ||||
|   static const on = 'on'; | ||||
|   static const off = 'off'; | ||||
|   static const home = 'home'; | ||||
|   static const not_home = 'not_home'; | ||||
|   static const unknown = 'unknown'; | ||||
|   static const open = 'open'; | ||||
|   static const opening = 'opening'; | ||||
|   static const closed = 'closed'; | ||||
|   static const closing = 'closing'; | ||||
|   static const playing = 'playing'; | ||||
|   static const paused = 'paused'; | ||||
|   static const idle = 'idle'; | ||||
|   static const standby = 'standby'; | ||||
|   static const alarm_disarmed = 'disarmed'; | ||||
|   static const alarm_armed_home = 'armed_home'; | ||||
|   static const alarm_armed_away = 'armed_away'; | ||||
|   static const alarm_armed_night = 'armed_night'; | ||||
|   static const alarm_armed_custom_bypass = 'armed_custom_bypass'; | ||||
|   static const alarm_pending = 'pending'; | ||||
|   static const alarm_arming = 'arming'; | ||||
|   static const alarm_disarming = 'disarming'; | ||||
|   static const alarm_triggered = 'triggered'; | ||||
|   static const locked = 'locked'; | ||||
|   static const unlocked = 'unlocked'; | ||||
|   static const unavailable = 'unavailable'; | ||||
|   static const ok = 'ok'; | ||||
|   static const problem = 'problem'; | ||||
|   static const active = 'active'; | ||||
|   static const cleaning = 'cleaning'; | ||||
|   static const docked = 'docked'; | ||||
|   static const returning = 'returning'; | ||||
|   static const error = 'error'; | ||||
|  | ||||
| } | ||||
|  | ||||
| class CardType { | ||||
|   static const HORIZONTAL_STACK = "horizontal-stack"; | ||||
|   static const VERTICAL_STACK = "vertical-stack"; | ||||
|   static const ENTITIES = "entities"; | ||||
|   static const GLANCE = "glance"; | ||||
|   static const MEDIA_CONTROL = "media-control"; | ||||
|   static const WEATHER_FORECAST = "weather-forecast"; | ||||
|   static const THERMOSTAT = "thermostat"; | ||||
|   static const SENSOR = "sensor"; | ||||
|   static const PLANT_STATUS = "plant-status"; | ||||
|   static const PICTURE_ENTITY = "picture-entity"; | ||||
|   static const PICTURE_ELEMENTS = "picture-elements"; | ||||
|   static const PICTURE = "picture"; | ||||
|   static const MAP = "map"; | ||||
|   static const IFRAME = "iframe"; | ||||
|   static const GAUGE = "gauge"; | ||||
|   static const ENTITY_BUTTON = "entity-button"; | ||||
|   static const ENTITY = "entity"; | ||||
|   static const BUTTON = "button"; | ||||
|   static const CONDITIONAL = "conditional"; | ||||
|   static const ALARM_PANEL = "alarm-panel"; | ||||
|   static const MARKDOWN = "markdown"; | ||||
|   static const LIGHT = "light"; | ||||
|   static const ENTITY_FILTER = "entity-filter"; | ||||
|   static const UNKNOWN = "unknown"; | ||||
|   static const HISTORY_GRAPH = "history-graph"; | ||||
|   static const PICTURE_GLANCE = "picture-glance"; | ||||
|   static const BADGES = "badges"; | ||||
| } | ||||
|  | ||||
| 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; | ||||
| } | ||||
| @@ -1,7 +1,8 @@ | ||||
| part of '../main.dart'; | ||||
| part of '../../main.dart'; | ||||
| 
 | ||||
| class AlarmControlPanelEntity extends Entity { | ||||
|   AlarmControlPanelEntity(Map rawData) : super(rawData); | ||||
|   AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
| 
 | ||||
| 
 | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
| @@ -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), | ||||
|         ) | ||||
| @@ -1,7 +1,8 @@ | ||||
| part of '../main.dart'; | ||||
| part of '../../main.dart'; | ||||
| 
 | ||||
| class AutomationEntity extends Entity { | ||||
|   AutomationEntity(Map rawData) : super(rawData); | ||||
|   AutomationEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
| 
 | ||||
| 
 | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
| @@ -1,7 +1,8 @@ | ||||
| part of '../main.dart'; | ||||
| part of '../../main.dart'; | ||||
| 
 | ||||
| class ButtonEntity extends Entity { | ||||
|   ButtonEntity(Map rawData) : super(rawData); | ||||
|   ButtonEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
| 
 | ||||
| 
 | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
| @@ -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) : super(rawData); | ||||
|   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) { | ||||
							
								
								
									
										190
									
								
								lib/entities/camera/widgets/camera_stream_view.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,190 @@ | ||||
| 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().isComponentEnabled('stream')) { | ||||
|       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}}', '${AppSettings().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 = '${AppSettings().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 ? () { | ||||
|               Navigator.of(context).pushReplacement( | ||||
|                 MaterialPageRoute( | ||||
|                   builder: (conext) => FullScreenPage( | ||||
|                     child: EntityModel( | ||||
|                       child: CameraStreamView( | ||||
|                         withControls: false | ||||
|                       ), | ||||
|                       handleTap: false, | ||||
|                       entityWrapper: EntityWrapper( | ||||
|                         entity: _entity | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   fullscreenDialog: true | ||||
|                 ) | ||||
|               ).then((_){ | ||||
|                 eventBus.fire(ShowEntityPageEvent(entityId: _entity.entityId)); | ||||
|               }); | ||||
|             } : 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(); | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| part of '../main.dart'; | ||||
| part of '../../main.dart'; | ||||
| 
 | ||||
| class ClimateEntity extends Entity { | ||||
| 
 | ||||
| @@ -10,69 +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; | ||||
| @@ -81,25 +69,22 @@ 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"; | ||||
| 
 | ||||
|   ClimateEntity(Map rawData) : super(rawData); | ||||
| 
 | ||||
|   @override | ||||
|   void update(Map rawData) { | ||||
|     super.update(rawData); | ||||
|   void update(Map rawData, String webHost) { | ||||
|     super.update(rawData, webHost); | ||||
|     if (supportTargetTemperature) { | ||||
|       historyConfig.numericAttributesToShow.add("temperature"); | ||||
|     } | ||||
|     if (supportTargetTemperatureHigh) { | ||||
|     if (supportTargetTemperatureRange) { | ||||
|       historyConfig.numericAttributesToShow.add("target_temp_high"); | ||||
|     } | ||||
|     if (supportTargetTemperatureLow) { | ||||
|       historyConfig.numericAttributesToShow.add("target_temp_low"); | ||||
|     } | ||||
|   } | ||||
| @@ -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) { | ||||
|     if (!_temperaturePending) { | ||||
|       _tmpTemperature = entity.temperature; | ||||
|       _tmpTargetHigh = entity.targetHigh; | ||||
|       _tmpTargetLow = entity.targetLow; | ||||
|     _tmpOperationMode = entity.operationMode; | ||||
|     } | ||||
|     _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"}); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| @@ -185,10 +192,9 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final ClimateEntity entity = entityModel.entityWrapper.entity; | ||||
|     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 +202,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 +239,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 +253,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 +279,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 +292,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 +311,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 +326,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 +340,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,16 +351,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildHumidityControls(ClimateEntity entity) { | ||||
|     List<Widget> result = []; | ||||
|   Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) { | ||||
|     if (entity.supportTargetHumidity) { | ||||
|       result.addAll(<Widget>[ | ||||
|         Text( | ||||
|           "$_tmpTargetHumidity%", | ||||
|           style: TextStyle(fontSize: Sizes.largeFontSize), | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Padding( | ||||
|             padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||
|             child: Text("Target humidity", style: Theme.of(context).textTheme.body1), | ||||
|           ), | ||||
|           UniversalSlider( | ||||
|             leading: Text( | ||||
|               "$_tmpTargetHumidity%", | ||||
|               style: Theme.of(context).textTheme.display1, | ||||
|             ), | ||||
|         Expanded( | ||||
|           child: Slider( | ||||
|             value: _tmpTargetHumidity, | ||||
|             max: entity.maxHumidity, | ||||
|             min: entity.minHumidity, | ||||
| @@ -369,24 +376,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|             }), | ||||
|             onChangeEnd: (double v) => _setTargetHumidity(entity, v), | ||||
|           ), | ||||
|         ) | ||||
|       ]); | ||||
|     } | ||||
|     if (result.isNotEmpty) { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Padding( | ||||
|             padding: EdgeInsets.fromLTRB( | ||||
|                 0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), | ||||
|             child: Text("Target humidity", style: TextStyle( | ||||
|                 fontSize: Sizes.stateFontSize | ||||
|             )), | ||||
|           ), | ||||
|           Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: result, | ||||
|           ), | ||||
|           Container( | ||||
|             height: Sizes.rowPadding, | ||||
|           ) | ||||
| @@ -403,57 +392,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(), | ||||
|             ) | ||||
|           ], | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -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) && | ||||
|     } else if ((entity.supportTargetTemperatureRange) && | ||||
|         (entity.targetLow != null) && | ||||
|         (entity.targetHigh != null)) { | ||||
|         targetTemp += " - ${entity.targetHigh}"; | ||||
|       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,) | ||||
|           ], | ||||
| @@ -1,23 +1,19 @@ | ||||
| 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; | ||||
| 
 | ||||
|   ModeSelectorWidget({ | ||||
|     Key key, | ||||
|     @required this.caption, | ||||
|     @required this.options, | ||||
|     this.options: const [], | ||||
|     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), | ||||
| @@ -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( | ||||
							
								
								
									
										47
									
								
								lib/entities/climate/widgets/temperature_control_widget.dart
									
									
									
									
									
										Normal 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(), | ||||
|             ) | ||||
|           ], | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| part of '../main.dart'; | ||||
| part of '../../main.dart'; | ||||
| 
 | ||||
| class CoverEntity extends Entity { | ||||
| 
 | ||||
| @@ -11,6 +11,8 @@ class CoverEntity extends Entity { | ||||
|   static const SUPPORT_STOP_TILT = 64; | ||||
|   static const SUPPORT_SET_TILT_POSITION = 128; | ||||
| 
 | ||||
|   CoverEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
| 
 | ||||
|   bool get supportOpen => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_OPEN) == | ||||
|       CoverEntity.SUPPORT_OPEN); | ||||
| @@ -38,15 +40,13 @@ class CoverEntity extends Entity { | ||||
|       CoverEntity.SUPPORT_SET_TILT_POSITION); | ||||
| 
 | ||||
| 
 | ||||
|   double get currentPosition => _getDoubleAttributeValue('current_position'); | ||||
|   double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position'); | ||||
|   double get currentPosition => _getDoubleAttributeValue('current_position') ?? 0; | ||||
|   double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position') ?? 0; | ||||
|   bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0); | ||||
|   bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed)); | ||||
|   bool get canTiltBeOpened => currentTiltPosition < 100; | ||||
|   bool get canTiltBeClosed => currentTiltPosition > 0; | ||||
| 
 | ||||
|   CoverEntity(Map rawData) : super(rawData); | ||||
| 
 | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return CoverStateWidget(); | ||||
| @@ -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()}); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| @@ -62,13 +62,10 @@ class _CoverControlWidgetState extends State<CoverControlWidget> { | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Padding( | ||||
|             padding: EdgeInsets.fromLTRB( | ||||
|                 0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), | ||||
|             child: Text("Position", style: TextStyle( | ||||
|                 fontSize: Sizes.stateFontSize | ||||
|             )), | ||||
|             padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||
|             child: Text("Position"), | ||||
|           ), | ||||
|           Slider( | ||||
|           UniversalSlider( | ||||
|             value: _tmpPosition, | ||||
|             min: 0.0, | ||||
|             max: 100.0, | ||||
| @@ -80,8 +77,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> { | ||||
|               }); | ||||
|             }, | ||||
|             onChangeEnd: (double value) => _setNewPosition(entity, value), | ||||
|           ), | ||||
|           Container(height: Sizes.rowPadding,) | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
| @@ -98,7 +94,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> { | ||||
|     } | ||||
|     if (entity.supportSetTiltPosition) { | ||||
|       controls.addAll(<Widget>[ | ||||
|         Slider( | ||||
|         UniversalSlider( | ||||
|           value: _tmpTiltPosition, | ||||
|           min: 0.0, | ||||
|           max: 100.0, | ||||
| @@ -117,10 +113,8 @@ class _CoverControlWidgetState extends State<CoverControlWidget> { | ||||
|     if (controls.isNotEmpty) { | ||||
|       controls.insert(0, Padding( | ||||
|         padding: EdgeInsets.fromLTRB( | ||||
|             0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), | ||||
|         child: Text("Tilt position", style: TextStyle( | ||||
|             fontSize: Sizes.stateFontSize | ||||
|         )), | ||||
|             0.0, Sizes.rowPadding, 0.0, 0), | ||||
|         child: Text("Tilt position"), | ||||
|       )); | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
| @@ -135,18 +129,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 | ||||
| @@ -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 | ||||
| @@ -1,6 +1,8 @@ | ||||
| part of '../main.dart'; | ||||
| part of '../../main.dart'; | ||||
| 
 | ||||
| class DateTimeEntity extends Entity { | ||||
|   DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
| 
 | ||||
|   bool get hasDate => attributes["has_date"] ?? false; | ||||
|   bool get hasTime => attributes["has_time"] ?? false; | ||||
|   int get year => attributes["year"] ?? 1970; | ||||
| @@ -12,8 +14,6 @@ class DateTimeEntity extends Entity { | ||||
|   String get formattedState => _getFormattedState(); | ||||
|   DateTime get dateTimeState => _getDateTimeState(); | ||||
| 
 | ||||
|   DateTimeEntity(Map rawData) : super(rawData); | ||||
| 
 | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return DateTimeStateWidget(); | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
| @@ -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), | ||||
|         )); | ||||
|   } | ||||
							
								
								
									
										72
									
								
								lib/entities/default_entity_container.widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,72 @@ | ||||
| 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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,13 +1,6 @@ | ||||
| part of '../main.dart'; | ||||
| 
 | ||||
| class StatelessEntityType { | ||||
|   static const NONE = 0; | ||||
|   static const MISSED = 1; | ||||
|   static const DIVIDER = 2; | ||||
|   static const SECTION = 3; | ||||
|   static const CALL_SERVICE = 4; | ||||
|   static const WEBLINK = 5; | ||||
| } | ||||
| enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink} | ||||
| 
 | ||||
| class Entity { | ||||
| 
 | ||||
| @@ -27,7 +20,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", | ||||
| @@ -73,10 +66,11 @@ class Entity { | ||||
|   Map attributes; | ||||
|   String domain; | ||||
|   String entityId; | ||||
|   String entityPicture; | ||||
|   String state; | ||||
|   String displayState; | ||||
|   DateTime _lastUpdated; | ||||
|   int statelessType = 0; | ||||
|   DateTime lastUpdatedTimestamp; | ||||
|   StatelessEntityType statelessType = StatelessEntityType.none; | ||||
| 
 | ||||
|   List<Entity> childEntities = []; | ||||
|   String deviceClass; | ||||
| @@ -84,8 +78,21 @@ class Entity { | ||||
|     chartType: EntityHistoryWidgetType.simple | ||||
|   ); | ||||
| 
 | ||||
|   String get displayName => | ||||
|       attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " ")); | ||||
|   String get displayName { | ||||
|     if (attributes.containsKey('friendly_name')) { | ||||
|       return attributes['friendly_name']; | ||||
|     } | ||||
|     if (attributes.containsKey('name')) {  | ||||
|       return attributes['name']; | ||||
|     } | ||||
|     if (entityId == null) { | ||||
|       return ""; | ||||
|     } | ||||
|     if (entityId.contains(".")) { | ||||
|       return entityId.split(".")[1].replaceAll("_", " "); | ||||
|     } | ||||
|     return entityId; | ||||
|   } | ||||
| 
 | ||||
|   bool get isView => | ||||
|       (domain == "group") && | ||||
| @@ -94,7 +101,6 @@ class Entity { | ||||
|   bool get isBadge => Entity.badgeDomains.contains(domain); | ||||
|   String get icon => attributes["icon"] ?? ""; | ||||
|   bool get isOn => state == EntityState.on; | ||||
|   String get entityPicture => _getEntityPictureUrl(); | ||||
|   String get unitOfMeasurement => attributes["unit_of_measurement"] ?? ""; | ||||
|   List get childEntityIds => attributes["entity_id"] ?? []; | ||||
|   String get lastUpdated => _getLastUpdatedFormatted(); | ||||
| @@ -102,60 +108,66 @@ class Entity { | ||||
|   double get doubleState => double.tryParse(state) ?? 0.0; | ||||
|   int get supportedFeatures => attributes["supported_features"] ?? 0; | ||||
| 
 | ||||
|   String _getEntityPictureUrl() { | ||||
|   String _getEntityPictureUrl(String webHost) { | ||||
|     String result = attributes["entity_picture"]; | ||||
|     if (result == null) return result; | ||||
|     if (!result.startsWith("http")) { | ||||
|       if (result.startsWith("/")) { | ||||
|         result = "$homeAssistantWebHost$result"; | ||||
|         result = "$webHost$result"; | ||||
|       } else { | ||||
|         result = "$homeAssistantWebHost/$result"; | ||||
|         result = "$webHost/$result"; | ||||
|       } | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   Entity(Map rawData) { | ||||
|     update(rawData); | ||||
|   Entity(Map rawData, String webHost) { | ||||
|     update(rawData, webHost); | ||||
|   } | ||||
| 
 | ||||
|   Entity.missed(String entityId) { | ||||
|     statelessType = StatelessEntityType.MISSED; | ||||
|     statelessType = StatelessEntityType.missed; | ||||
|     attributes = {"hidden": false}; | ||||
|     this.entityId = entityId; | ||||
|   } | ||||
| 
 | ||||
|   Entity.divider() { | ||||
|     statelessType = StatelessEntityType.DIVIDER; | ||||
|     statelessType = StatelessEntityType.divider; | ||||
|     attributes = {"hidden": false}; | ||||
|   } | ||||
| 
 | ||||
|   Entity.section(String label) { | ||||
|     statelessType = StatelessEntityType.SECTION; | ||||
|     statelessType = StatelessEntityType.section; | ||||
|     attributes = {"hidden": false, "friendly_name": "$label"}; | ||||
|   } | ||||
| 
 | ||||
|   Entity.ghost(String name, String icon) { | ||||
|     statelessType = StatelessEntityType.ghost; | ||||
|     attributes = {"icon": icon, "hidden": false, "friendly_name": name}; | ||||
|   } | ||||
| 
 | ||||
|   Entity.callService({String icon, String name, String service, String actionName}) { | ||||
|     statelessType = StatelessEntityType.CALL_SERVICE; | ||||
|     statelessType = StatelessEntityType.callService; | ||||
|     entityId = service; | ||||
|     displayState = actionName?.toUpperCase() ?? "RUN"; | ||||
|     attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"}; | ||||
|   } | ||||
| 
 | ||||
|   Entity.weblink({String url, String name, String icon}) { | ||||
|     statelessType = StatelessEntityType.WEBLINK; | ||||
|     entityId = "custom.custom"; //TODO wtf?? | ||||
|     statelessType = StatelessEntityType.webLink; | ||||
|     entityId = "custom.custom"; | ||||
|     attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; | ||||
|   } | ||||
| 
 | ||||
|   void update(Map rawData) { | ||||
|   void update(Map rawData, String webHost) { | ||||
|     attributes = rawData["attributes"] ?? {}; | ||||
|     domain = rawData["entity_id"].split(".")[0]; | ||||
|     domain = rawData["entity_id"] != null ? rawData["entity_id"].split(".")[0] : null; | ||||
|     entityId = rawData["entity_id"]; | ||||
|     deviceClass = attributes["device_class"]; | ||||
|     state = rawData["state"]; | ||||
|     displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state; | ||||
|     _lastUpdated = DateTime.tryParse(rawData["last_updated"]); | ||||
|     state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"]; | ||||
|     displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state); | ||||
|     lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]); | ||||
|     entityPicture = _getEntityPictureUrl(webHost); | ||||
|   } | ||||
| 
 | ||||
|   double _getDoubleAttributeValue(String attributeName) { | ||||
| @@ -210,52 +222,19 @@ 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), | ||||
|       child: BadgeWidget(), | ||||
|       handleTap: true, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   String getAttribute(String attributeName) { | ||||
|     if (attributes != null) { | ||||
|       return attributes["$attributeName"]; | ||||
|       return attributes["$attributeName"].toString(); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   String _getLastUpdatedFormatted() { | ||||
|     if (_lastUpdated == null) { | ||||
|     if (lastUpdatedTimestamp == null) { | ||||
|       return "-"; | ||||
|     } else { | ||||
|       DateTime now = DateTime.now(); | ||||
|       Duration d = now.difference(_lastUpdated); | ||||
|       Duration d = now.difference(lastUpdatedTimestamp); | ||||
|       String text; | ||||
|       int v; | ||||
|       if (d.inDays == 0) { | ||||
							
								
								
									
										138
									
								
								lib/entities/entity_icon.widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,138 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityIcon extends StatelessWidget { | ||||
|  | ||||
|   final EdgeInsetsGeometry padding; | ||||
|   final EdgeInsetsGeometry iconPadding; | ||||
|   final EdgeInsetsGeometry imagePadding; | ||||
|   final double size; | ||||
|   final Color color; | ||||
|   final bool showBadge; | ||||
|  | ||||
|   const EntityIcon({Key key, this.color, this.showBadge: true, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key); | ||||
|  | ||||
|   int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { | ||||
|     if (entityId == null) { | ||||
|       return 0; | ||||
|     } | ||||
|     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; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     Color iconColor; | ||||
|     if (color != null) { | ||||
|       iconColor = color; | ||||
|     } else if (entityWrapper.stateColor) { | ||||
|       iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context); | ||||
|     } else { | ||||
|       iconColor = HAClientTheme().getOffStateColor(context); | ||||
|     } | ||||
|     Widget iconWidget; | ||||
|     bool isPicture = false; | ||||
|     if (entityWrapper == null) { | ||||
|       iconWidget = Container( | ||||
|         width: size, | ||||
|         height: size, | ||||
|       ); | ||||
|     } else { | ||||
|       if (entityWrapper.entityPicture != null) { | ||||
|         iconWidget = ClipOval( | ||||
|           child: CachedNetworkImage( | ||||
|             imageUrl: '${entityWrapper.entityPicture}', | ||||
|             width: size+12, | ||||
|             fit: BoxFit.cover, | ||||
|             height: size+12, | ||||
|             errorWidget: (context, str, dyn) { | ||||
|               return Padding( | ||||
|                 padding: iconPadding ?? padding, | ||||
|                 child: _buildIcon(entityWrapper, iconColor) | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|         ); | ||||
|         isPicture = true; | ||||
|       } else { | ||||
|         iconWidget = _buildIcon(entityWrapper, iconColor); | ||||
|       } | ||||
|     } | ||||
|     EdgeInsetsGeometry computedPadding; | ||||
|     if (isPicture && imagePadding != null) { | ||||
|       computedPadding = imagePadding; | ||||
|     } else if (!isPicture && iconPadding != null) { | ||||
|       computedPadding = iconPadding; | ||||
|     } else { | ||||
|       computedPadding = padding; | ||||
|     } | ||||
|     return Padding( | ||||
|       padding: computedPadding, | ||||
|       child: iconWidget, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildIcon(EntityWrapper entityWrapper, Color iconColor) { | ||||
|     Widget iconWidget; | ||||
|     String iconName = entityWrapper.icon; | ||||
|     int iconCode = 0; | ||||
|     if (iconName.length > 0) { | ||||
|       iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName); | ||||
|     } else { | ||||
|       iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId, | ||||
|           entityWrapper.entity.deviceClass, entityWrapper.entity.state); // | ||||
|     } | ||||
|     if (showBadge && entityWrapper.entity is LightEntity && | ||||
|       (entityWrapper.entity as LightEntity).supportColor && | ||||
|       (entityWrapper.entity as LightEntity).color != null && | ||||
|       (entityWrapper.entity as LightEntity).color.toColor() != Colors.white | ||||
|       ) { | ||||
|       Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();   | ||||
|       iconWidget = Stack( | ||||
|         children: <Widget>[ | ||||
|           Icon( | ||||
|             IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||
|             size: size, | ||||
|             color: iconColor, | ||||
|           ), | ||||
|           Positioned( | ||||
|             bottom: 0, | ||||
|             right: 0, | ||||
|             child: Container( | ||||
|               width: size / 3, | ||||
|               height: size / 3, | ||||
|               decoration: BoxDecoration( | ||||
|                 color: lightColor, | ||||
|                 shape: BoxShape.circle, | ||||
|                 boxShadow: <BoxShadow>[ | ||||
|                   BoxShadow( | ||||
|                     spreadRadius: 0, | ||||
|                     blurRadius: 0, | ||||
|                     offset: Offset(0.3, 0.3) | ||||
|                   ) | ||||
|                 ] | ||||
|               ), | ||||
|             ), | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       iconWidget = Icon( | ||||
|         IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||
|         size: size, | ||||
|         color: iconColor, | ||||
|       ); | ||||
|     } | ||||
|     return iconWidget; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										22
									
								
								lib/entities/entity_model.widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityModel extends InheritedWidget { | ||||
|   const EntityModel({ | ||||
|     Key key, | ||||
|     @required this.entityWrapper, | ||||
|     @required this.handleTap, | ||||
|     @required Widget child, | ||||
|   }) : super(key: key, child: child); | ||||
|  | ||||
|   final EntityWrapper entityWrapper; | ||||
|   final bool handleTap; | ||||
|  | ||||
|   static EntityModel of(BuildContext context) { | ||||
|     return context.dependOnInheritedWidgetOfExactType<EntityModel>(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool updateShouldNotify(EntityModel oldWidget) { | ||||
|     return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp; | ||||
|   } | ||||
| } | ||||
| @@ -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, | ||||
|       ), | ||||
|     ); | ||||
							
								
								
									
										37
									
								
								lib/entities/entity_page_layout.widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,37 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityPageLayout extends StatelessWidget { | ||||
|  | ||||
|   final Entity entity; | ||||
|  | ||||
|   EntityPageLayout({Key key, this.entity}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return EntityModel( | ||||
|       entityWrapper: EntityWrapper(entity: entity), | ||||
|       child: ListView( | ||||
|           padding: EdgeInsets.all(0), | ||||
|           children: <Widget>[ | ||||
|             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, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,12 +1,11 @@ | ||||
| part of '../main.dart'; | ||||
| 
 | ||||
| class EntityIcon extends StatelessWidget { | ||||
| class EntityPicture extends StatelessWidget { | ||||
| 
 | ||||
|   final EdgeInsetsGeometry padding; | ||||
|   final double size; | ||||
|   final Color color; | ||||
|   final BoxFit fit; | ||||
|    | ||||
|   const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key); | ||||
|   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]; | ||||
| @@ -23,25 +22,10 @@ class EntityIcon extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Widget buildIcon(EntityWrapper data, Color color) { | ||||
|   Widget buildIcon(EntityWrapper data, BuildContext context) { | ||||
|     if (data == null) { | ||||
|       return null; | ||||
|     } | ||||
|     if (data.entityPicture != null) { | ||||
|       return Container( | ||||
|         height: size+12, | ||||
|         width: size+12, | ||||
|         decoration: BoxDecoration( | ||||
|             shape: BoxShape.circle, | ||||
|             image: DecorationImage( | ||||
|               fit:BoxFit.cover, | ||||
|               image: CachedNetworkImageProvider( | ||||
|                 "${data.entityPicture}" | ||||
|               ), | ||||
|             ) | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|     String iconName = data.icon; | ||||
|     int iconCode = 0; | ||||
|     if (iconName.length > 0) { | ||||
| @@ -50,14 +34,27 @@ class EntityIcon extends StatelessWidget { | ||||
|       iconCode = getDefaultIconByEntityId(data.entity.entityId, | ||||
|           data.entity.deviceClass, data.entity.state); // | ||||
|     } | ||||
|     return Padding( | ||||
|         padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0), | ||||
|     Widget iconPicture = Container( | ||||
|       child: Center( | ||||
|         child: Icon( | ||||
|           IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||
|           size: size, | ||||
|           color: color, | ||||
|           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 | ||||
| @@ -67,7 +64,7 @@ class EntityIcon extends StatelessWidget { | ||||
|       padding: padding, | ||||
|       child: buildIcon( | ||||
|           entityWrapper, | ||||
|           color ?? EntityColor.stateColor(entityWrapper.entity.state) | ||||
|           context | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
							
								
								
									
										217
									
								
								lib/entities/entity_wrapper.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,217 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityWrapper { | ||||
|  | ||||
|   String overrideName; | ||||
|   String overrideIcon; | ||||
|   final bool stateColor; | ||||
|   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.stateColor: true, | ||||
|     this.uiAction, | ||||
|     this.stateFilter | ||||
|   }) { | ||||
|     if (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || 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(entityId: entity.entityId)); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case EntityUIAction.navigate: { | ||||
|         if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) { | ||||
|           //TODO handle local urls | ||||
|           Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.tapService}'); | ||||
|         } else { | ||||
|           Launcher.launchURLInBrowser(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(entityId: entity.entityId)); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case EntityUIAction.navigate: { | ||||
|           if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) { | ||||
|             //TODO handle local urls | ||||
|             Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.holdService}'); | ||||
|           } else { | ||||
|             Launcher.launchURLInBrowser(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(entityId: entity.entityId)); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case EntityUIAction.navigate: { | ||||
|           if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) { | ||||
|             //TODO handle local urls | ||||
|             Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.doubleTapService}'); | ||||
|           } else { | ||||
|             Launcher.launchURLInBrowser(uiAction.doubleTapService); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         default: { | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class EntityUIAction { | ||||
|   static const moreInfo = 'more-info'; | ||||
|   static const toggle = 'toggle'; | ||||
|   static const callService = 'call-service'; | ||||
|   static const navigate = 'navigate'; | ||||
|   static const none = 'none'; | ||||
|  | ||||
|   String tapAction = EntityUIAction.moreInfo; | ||||
|   String tapNavigationPath; | ||||
|   String tapService; | ||||
|   Map<String, dynamic> tapServiceData; | ||||
|   String holdAction = EntityUIAction.moreInfo; | ||||
|   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) { | ||||
|       if (rawEntityData["tap_action"] != null) { | ||||
|         if (rawEntityData["tap_action"] is String) { | ||||
|           tapAction = rawEntityData["tap_action"]; | ||||
|         } else { | ||||
|           tapAction = | ||||
|               rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo; | ||||
|           tapNavigationPath = rawEntityData["tap_action"]["navigation_path"]; | ||||
|           tapService = rawEntityData["tap_action"]["service"]; | ||||
|           tapServiceData = rawEntityData["tap_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|       if (rawEntityData["hold_action"] != null) { | ||||
|         if (rawEntityData["hold_action"] is String) { | ||||
|           holdAction = rawEntityData["hold_action"]; | ||||
|         } else { | ||||
|           holdAction = | ||||
|               rawEntityData["hold_action"]["action"] ?? EntityUIAction.none; | ||||
|           holdNavigationPath = rawEntityData["hold_action"]["navigation_path"]; | ||||
|           holdService = rawEntityData["hold_action"]["service"]; | ||||
|           holdServiceData = rawEntityData["hold_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|       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"]; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										23
									
								
								lib/entities/error_entity_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class ErrorEntityWidget extends StatelessWidget { | ||||
|    | ||||
|   final String text; | ||||
|  | ||||
|   ErrorEntityWidget({ | ||||
|     Key key, this.text | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityModel entityModel = EntityModel.of(context); | ||||
|     String errorText = text ?? "Entity error: ${entityModel.entityWrapper.entity?.entityId}"; | ||||
|     return Container( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.all(5.0), | ||||
|           child: Text(errorText), | ||||
|         ), | ||||
|         color: Theme.of(context).errorColor, | ||||
|     ); | ||||
|   } | ||||
| } | ||||